投影运算 (C#)
投影是指将对象转换为一种新形式的操作,该形式通常只包含那些将随后使用的属性。 通过使用投影,您可以构造从每个对象生成的新类型。 可以投影属性,并对该属性执行数学函数。 还可以在不更改原始对象的情况下投影该对象。
下面一节列出了执行投影的标准查询运算符方法。
方法
方法名称 | 说明 | C# 查询表达式语法 | 详细信息 |
---|---|---|---|
选择 | 投影基于转换函数的值。 | select |
Enumerable.Select Queryable.Select |
SelectMany | 投影基于转换函数的值序列,然后将它们展平为一个序列。 | 使用多个 from 子句 |
Enumerable.SelectMany Queryable.SelectMany |
Zip | 使用 2-3 个指定序列中的元素生成元组序列。 | 不适用。 | Enumerable.Zip Queryable.Zip |
Select
下面的示例使用 select
子句来投影字符串列表中每个字符串的第一个字母。
List<string> words = new() { "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
*/
SelectMany
下面的示例使用多个 from
子句来投影字符串列表中每个字符串中的每个单词。
List<string> phrases = new() { "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
*/
Zip
Zip
投影运算符有多个重载。 所有 Zip
方法都处理两个或更多可能是异构类型的序列。 前两个重载返回元组,具有来自给定序列的相应位置类型。
请考虑下列集合:
// An int array with 7 elements.
IEnumerable<int> numbers = new[]
{
1, 2, 3, 4, 5, 6, 7
};
// A char array with 6 elements.
IEnumerable<char> letters = new[]
{
'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 = new[]
{
"🤓", "🔥", "🎉", "👀", "⭐", "💜", "✔", "💯"
};
若要将这些序列一起投影,请使用 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
的行为。 代码通过从源集合的每个花卉名称列表中提取项来创建一个“花束”。 此示例中,transform 函数 Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,TResult>) 使用的“单值”本身即是值的集合。 这需要额外的 foreach
循环,以便枚举每个子序列中的每个字符串。
class Bouquet
{
public List<string> Flowers { get; set; }
}
static void SelectVsSelectMany()
{
List<Bouquet> bouquets = new()
{
new Bouquet { Flowers = new List<string> { "sunflower", "daisy", "daffodil", "larkspur" }},
new Bouquet { Flowers = new List<string> { "tulip", "rose", "orchid" }},
new Bouquet { Flowers = new List<string> { "gladiolis", "lily", "snapdragon", "aster", "protea" }},
new Bouquet { Flowers = new List<string> { "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);
/* This code produces the following output:
Results by using Select():
sunflower
daisy
daffodil
larkspur
tulip
rose
orchid
gladiolis
lily
snapdragon
aster
protea
larkspur
lilac
iris
dahlia
Results by using SelectMany():
sunflower
daisy
daffodil
larkspur
tulip
rose
orchid
gladiolis
lily
snapdragon
aster
protea
larkspur
lilac
iris
dahlia
*/
}