投影作業 (C#)

投影是指將物件轉換成新形式的作業,而該新形式通常只包含隨後將使用的那些屬性。 透過使用投影,您可以建構根據每個物件所建立的新型別。 您可以投影屬性並對其執行數學函式。 您也可以投影原始物件,而不進行任何變更。

執行投影的標準查詢運算子方法詳列於下一節。

方法

方法名稱 描述 C# 查詢運算式語法 其他相關資訊
選取 投影以轉換函式為基礎的值。 select Enumerable.Select
Queryable.Select
SelectMany 投影一連串以轉換函式為基礎的值,然後將這些值壓平合併成一個序列。 使用多個 from 子句 Enumerable.SelectMany
Queryable.SelectMany
郵遞區號 從 2-3 個指定序列的元素,產生元組序列。 不適用。 Enumerable.Zip
Queryable.Zip

Select

下列範例使用 select 子句,來投影字串清單中每個字串的第一個字母。

List<string> words = ["an", "apple", "a", "day"];

var query = from word in words
            select word.Substring(0, 1);

foreach (string s in query)
{
    Console.WriteLine(s);
}

/* This code produces the following output:

    a
    a
    a
    d
*/

使用方法語法的對等查詢會顯示在下列程式碼中:

List<string> words = ["an", "apple", "a", "day"];

var query = words.Select(word => word.Substring(0, 1));

foreach (string s in query)
{
    Console.WriteLine(s);
}

/* This code produces the following output:

    a
    a
    a
    d
*/

SelectMany

下列範例使用多個 from 子句,來投影字串清單中每個字串的每個字。

List<string> phrases = ["an apple a day", "the quick brown fox"];

var query = from phrase in phrases
            from word in phrase.Split(' ')
            select word;

foreach (string s in query)
{
    Console.WriteLine(s);
}

/* This code produces the following output:

    an
    apple
    a
    day
    the
    quick
    brown
    fox
*/

使用方法語法的對等查詢會顯示在下列程式碼中:

List<string> phrases = ["an apple a day", "the quick brown fox"];

var query = phrases.SelectMany(phrases => phrases.Split(' '));

foreach (string s in query)
{
    Console.WriteLine(s);
}

/* This code produces the following output:

    an
    apple
    a
    day
    the
    quick
    brown
    fox
*/

SelectMany 方法也可以形成將第一個序列中的每個項目與第二個序列中的每個項目配對的組合:

var query = from number in numbers
            from letter in letters
            select (number, letter);

foreach (var item in query)
{
    Console.WriteLine(item);
}

使用方法語法的對等查詢會顯示在下列程式碼中:

var method = numbers
    .SelectMany(number => letters,
    (number, letter) => (number, letter));

foreach (var item in method)
{
    Console.WriteLine(item);
}

Zip

Zip 投影運算子有數個多載。 所有 Zip 方法適用於兩個或多個可能異質類型的序列。 前兩個多載會傳回元組,包含對應的指定序列位置類型。

請考慮下列集合:

// An int array with 7 elements.
IEnumerable<int> numbers = [1, 2, 3, 4, 5, 6, 7];
// A char array with 6 elements.
IEnumerable<char> letters = ['A', 'B', 'C', 'D', 'E', 'F'];

若要一起投影這些序列,請使用 Enumerable.Zip<TFirst,TSecond>(IEnumerable<TFirst>, IEnumerable<TSecond>) 運算子:

foreach ((int number, char letter) in numbers.Zip(letters))
{
    Console.WriteLine($"Number: {number} zipped with letter: '{letter}'");
}
// This code produces the following output:
//     Number: 1 zipped with letter: 'A'
//     Number: 2 zipped with letter: 'B'
//     Number: 3 zipped with letter: 'C'
//     Number: 4 zipped with letter: 'D'
//     Number: 5 zipped with letter: 'E'
//     Number: 6 zipped with letter: 'F'

重要

ZIP 作業產生的序列長度永遠不超過最短序列。 numbersletters 集合的長度不同,所以產生的序列會省略 numbers 集合的最後一個元素,因為此元素不必壓縮。

第二個多載接受 third 序列。 接著建立另一個集合,亦即 emoji

// A string array with 8 elements.
IEnumerable<string> emoji = [ "🤓", "🔥", "🎉", "👀", "⭐", "💜", "✔", "💯"];

若要一起投影這些序列,請使用 Enumerable.Zip<TFirst,TSecond,TThird>(IEnumerable<TFirst>, IEnumerable<TSecond>, IEnumerable<TThird>) 運算子:

foreach ((int number, char letter, string em) in numbers.Zip(letters, emoji))
{
    Console.WriteLine(
        $"Number: {number} is zipped with letter: '{letter}' and emoji: {em}");
}
// This code produces the following output:
//     Number: 1 is zipped with letter: 'A' and emoji: 🤓
//     Number: 2 is zipped with letter: 'B' and emoji: 🔥
//     Number: 3 is zipped with letter: 'C' and emoji: 🎉
//     Number: 4 is zipped with letter: 'D' and emoji: 👀
//     Number: 5 is zipped with letter: 'E' and emoji: ⭐
//     Number: 6 is zipped with letter: 'F' and emoji: 💜

類似於之前的多載,Zip 方法會投影元組,但這次有三個元素。

第三個多載接受 Func<TFirst, TSecond, TResult> 引數作為結果選取器。 您可以從正在壓縮的序列中投影出新的結果序列。

foreach (string result in
    numbers.Zip(letters, (number, letter) => $"{number} = {letter} ({(int)letter})"))
{
    Console.WriteLine(result);
}
// This code produces the following output:
//     1 = A (65)
//     2 = B (66)
//     3 = C (67)
//     4 = D (68)
//     5 = E (69)
//     6 = F (70)

使用上述 Zip 多載時,指定的函式會套用至對應的元素 numbersletter,產生 string 結果的序列。

SelectSelectMany

SelectSelectMany 的工作是從來源值產生一或多個結果值。 Select 會針對每個來源值產生一個結果值。 因此,整體結果是集合與來源集合中的項目數相同。 相較之下,SelectMany 會產生單一整體結果,其中包含了從每個來源值中串連的子集合。 當做引數傳遞至 SelectMany 的轉換函式必須傳回每個來源值的可列舉值序列。 SelectMany 會串連這些可列舉的序列以建立一個大型的序列。

下列兩個圖顯示這兩個方法的動作之間的概念差異。 在每個案例中,假設選取器 (轉換) 函式會從每個來源值選取花朵陣列。

下圖描述 Select 如何傳回其中的項目數與來源集合相同的集合。

Graphic that shows the action of Select()

下圖描述 SelectMany 如何將中繼陣列序列串連成一個最終結果值,其中包含每個中繼陣列中的所有值。

Graphic showing the action of SelectMany()

程式碼範例

下列範例會比較 SelectSelectMany 的行為。 程式碼會從來源集合中的每個花名清單取得項目,然後建立「花束」。 在下列範例中,轉換函式 Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,TResult>) 所使用的「單一值」就是值的集合。 此範例需要額外的 foreach 迴圈,才能列舉每個子序列中的每個字串。

class Bouquet
{
    public required List<string> Flowers { get; init; }
}

static void SelectVsSelectMany()
{
    List<Bouquet> bouquets =
    [
        new Bouquet { Flowers = ["sunflower", "daisy", "daffodil", "larkspur"] },
        new Bouquet { Flowers = ["tulip", "rose", "orchid"] },
        new Bouquet { Flowers = ["gladiolis", "lily", "snapdragon", "aster", "protea"] },
        new Bouquet { Flowers = ["larkspur", "lilac", "iris", "dahlia"] }
    ];

    IEnumerable<List<string>> query1 = bouquets.Select(bq => bq.Flowers);

    IEnumerable<string> query2 = bouquets.SelectMany(bq => bq.Flowers);

    Console.WriteLine("Results by using Select():");
    // Note the extra foreach loop here.
    foreach (IEnumerable<string> collection in query1)
    {
        foreach (string item in collection)
        {
            Console.WriteLine(item);
        }
    }

    Console.WriteLine("\nResults by using SelectMany():");
    foreach (string item in query2)
    {
        Console.WriteLine(item);
    }
}

另請參閱