Поделиться через


Операции проекции (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
Zip-архив Создает последовательность кортежей с элементами, полученными из 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 существует несколько перегрузок. Все методы работают с последовательностями двух или более возможно разнородных типов. Первые две перегрузки возвращают кортежи с соответствующим позиционным типом из заданных последовательностей.

Рассмотрим следующие коллекции:

// 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 перегрузке указанная функция применяется к соответствующим элементам number и letter, создавая последовательность результатов string.

Select и SelectMany

Работа обоих Select и SelectMany заключается в создании результирующих значений (или значений) из исходных значений. Select создает одно значение результата для каждого исходного значения. Таким образом, общий результат представляет собой коллекцию, которая имеет то же количество элементов, что и исходная коллекция. В отличие от этого, SelectMany создаёт один общий результат, содержащий объединенные подколлекции из каждого исходного значения. Функция преобразования, передаваемая в качестве аргумента SelectMany , должна возвращать перечисленную последовательность значений для каждого исходного значения. SelectMany объединяет эти перечисленные последовательности для создания одной большой последовательности.

На следующих двух иллюстрациях показано концептуальное различие между действиями этих двух методов. В каждом случае предположим, что функция селектора (преобразования) выбирает массив цветов из каждого исходного значения.

На этом рисунке показано, как Select возвращает коллекцию с таким же количеством элементов, что и исходная коллекция.

Рисунок, показывающий действие Select()

На этом рисунке показано, как SelectMany объединяет промежуточную последовательность массивов в одно конечное значение результата, содержащее каждое значение из каждого промежуточного массива.

Рисунок, показывающий действие 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);
    }
}

См. также