投影作業 (C#)
投影是指將物件轉換成新形式的作業,而該新形式通常只包含隨後將使用的那些屬性。 透過使用投影,您可以建構根據每個物件所建立的新型別。 您可以投影屬性並對其執行數學函式。 您也可以投影原始物件,而不進行任何變更。
重要
這些範例會使用 System.Collections.Generic.IEnumerable<T> 資料來源。 根據 System.Linq.IQueryProvider 的資料來源會使用 System.Linq.IQueryable<T> 資料來源和運算式樹狀架構。 運算式樹狀架構在允許的 C# 語法方面有限制。 此外,每個 IQueryProvider
資料來源 (例如 EF Core) 可能會施加更多限制。 檢查資料來源的文件。
執行投影的標準查詢運算子方法詳列於下一節。
方法
方法名稱 | 描述 | 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 作業產生的序列長度永遠不超過最短序列。 numbers
和 letters
集合的長度不同,所以產生的序列會省略 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
多載時,指定的函式會套用至對應的元素 numbers
和 letter
,產生 string
結果的序列。
Select
與 SelectMany
Select
和 SelectMany
的工作是從來源值產生一或多個結果值。 Select
會針對每個來源值產生一個結果值。 因此,整體結果是集合與來源集合中的項目數相同。 相較之下,SelectMany
會產生單一整體結果,其中包含了從每個來源值中串連的子集合。 當做引數傳遞至 SelectMany
的轉換函式必須傳回每個來源值的可列舉值序列。 SelectMany
會串連這些可列舉的序列以建立一個大型的序列。
下列兩個圖顯示這兩個方法的動作之間的概念差異。 在每個案例中,假設選取器 (轉換) 函式會從每個來源值選取花朵陣列。
下圖描述 Select
如何傳回其中的項目數與來源集合相同的集合。
下圖描述 SelectMany
如何將中繼陣列序列串連成一個最終結果值,其中包含每個中繼陣列中的所有值。
程式碼範例
下列範例會比較 Select
和 SelectMany
的行為。 程式碼會從來源集合中的每個花名清單取得項目,然後建立「花束」。 在下列範例中,轉換函式 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);
}
}