投影操作(C#)

投影是指将对象转换为新的形式,这种形式通常只包含随后需要使用的属性。 通过使用投影,可以构造从每个对象生成的新类型。 可以投影属性并对该属性执行数学函数。 还可以投影原始对象,而无需更改它。

重要

这些示例使用 System.Collections.Generic.IEnumerable<T> 数据源。 基于System.Linq.IQueryProvider的数据源使用System.Linq.IQueryable<T> 数据源和表达式树。 表达式树对允许的 C# 语法有 限制 。 此外,每个 IQueryProvider 数据源(如 EF Core )可能会施加更多的限制。 查看数据源的文档。

以下部分列出了执行投影的标准查询运算符方法。

方法

方法名称 DESCRIPTION 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(phrase => phrase.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 方法投影一个元组,但这次包含三个元素。

第三个重载接受一个参数,该参数充当结果选择器。 可以从压缩的序列中投影新的生成序列。

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 重载时,指定的函数将被应用于相应的元素 numberletter,从而生成结果序列 string

SelectSelectMany

SelectSelectMany 的工作是从源值生成结果值(或多个值)。 Select 为每个源值生成一个结果值。 因此,总体结果是一个与源集合具有相同数量的元素的集合。 相比之下, SelectMany 生成一个包含来自每个源值的串联子集合的整体结果。 作为参数 SelectMany 传递的转换函数必须为每个源值返回可枚举的值序列。 SelectMany 连接这些可枚举序列以创建一个大序列。

以下两个图示显示了这两种方法的作之间的概念差异。 在每个情况下,假设选择器(转换)函数从每个源值中选择鲜花数组。

此图描述了如何 Select 返回与源集合具有相同数量的元素的集合。

显示 Select() 作用的图形

此图描述了如何将 SelectMany 数组的中间序列串联成一个最终结果值,其中包含每个中间数组中的每个值。

显示 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);
    }
}

另请参阅