Operaciones de proyección (C#)

El término "proyección" hace referencia a la operación de transformar un objeto en una nueva forma que, a menudo, consta solo de aquellas propiedades usadas posteriormente. Utilizando la proyección, puede construir un tipo nuevo creado a partir de cada objeto. Se puede proyectar una propiedad y realizar una función matemática en ella. También puede proyectar el objeto original sin cambiarlo.

Los métodos del operador de consulta estándar que realizan proyecciones se indican en la sección siguiente.

Métodos

Nombres de método Descripción Sintaxis de la expresión de consulta de C# Información adicional
Seleccionar Proyecta valores basados en una función de transformación. select Enumerable.Select
Queryable.Select
SelectMany Proyecta secuencias de valores que se basan en una función de transformación y después los convierte en una secuencia. Use varias cláusulas from Enumerable.SelectMany
Queryable.SelectMany
Zip Genera una secuencia de tuplas con elementos a partir de dos o tres secuencias especificadas. No aplicable. Enumerable.Zip
Queryable.Zip

Select

En el ejemplo siguiente se usa la cláusula select para proyectar la primera letra de cada cadena de una lista de cadenas.

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
*/

La consulta equivalente mediante la sintaxis del método se muestra en el código siguiente:

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

En el ejemplo siguiente se usan varias cláusulas from para proyectar cada palabra de todas las cadenas de una lista de cadenas.

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
*/

La consulta equivalente mediante la sintaxis del método se muestra en el código siguiente:

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
*/

El método SelectMany también puede formar la combinación de hacer coincidir todos los elementos de la primera secuencia con cada elemento de la segunda secuencia:

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

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

La consulta equivalente mediante la sintaxis del método se muestra en el código siguiente:

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

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

Zip

Hay varias sobrecargas para el operador Zip de proyección. Todos los métodos Zip funcionan en secuencias de dos o más tipos posiblemente heterogéneos. Las dos primeras sobrecargas devuelven tuplas, con el tipo posicional correspondiente de las secuencias dadas.

Observe las siguientes colecciones:

// 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'];

Para proyectar estas secuencias juntas, use el operador 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'

Importante

La secuencia resultante de una operación zip nunca tiene más longitud que la secuencia más corta. Las colecciones numbers y letters difieren en longitud, y la secuencia resultante omite el último elemento de la colección numbers, ya que no tiene nada con que comprimir.

La segunda sobrecarga acepta una secuencia third. Vamos a crear otra colección, concretamente emoji:

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

Para proyectar estas secuencias juntas, use el operador 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: 💜

Al igual que la sobrecarga anterior, el método Zip proyecta una tupla, pero esta vez con tres elementos.

La tercera sobrecarga acepta un argumento Func<TFirst, TSecond, TResult> que actúa como selector de resultados. Puede proyectar una nueva secuencia resultante de las secuencias que se comprimen.

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)

Con la sobrecarga Zip anterior, la función especificada se aplica a los elementos correspondientes numbers y letter, lo que genera una secuencia de los resultados string.

Diferencias entre Select y SelectMany

La función tanto de Select como de SelectMany consiste en generar un valor (o valores) de resultado a partir de valores de origen. Select genera un valor de resultado para cada valor de origen. El resultado global, por tanto, es una colección que tiene el mismo número de elementos que la colección de origen. En cambio, SelectMany genera un resultado global único que contiene subcolecciones concatenadas procedentes de cada valor de origen. La función de transformación que se pasa como argumento a SelectMany debe devolver una secuencia enumerable de valores para cada valor de origen. SelectMany concatena estas secuencias enumerables para crear una secuencia grande.

Las dos ilustraciones siguientes muestran la diferencia conceptual entre las acciones de estos dos métodos. En cada caso, se supone que la función de selector (transformación) selecciona la matriz de flores de cada valor de origen.

En esta ilustración se muestra cómo Select devuelve una colección que tiene el mismo número de elementos que la colección de origen.

Graphic that shows the action of Select()

En esta ilustración se muestra cómo SelectMany concatena la secuencia intermedia de matrices en un valor de resultado final que contiene cada uno de los valores de todas las matrices intermedias.

Graphic showing the action of SelectMany()

Ejemplo de código

En el ejemplo siguiente se compara el comportamiento de Select y SelectMany. El código crea un "ramo" de flores tomando los elementos de cada lista de nombres de flores de la colección de origen. En el siguiente ejemplo, el "valor único" que usa la función de transformación Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,TResult>) es una colección de valores. Este ejemplo requiere el bucle adicional foreach a fin de enumerar cada una de las cadenas de cada subsecuencia.

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

Consulte también