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.
Importante
Estos ejemplos usan un origen de datos System.Collections.Generic.IEnumerable<T>. Los orígenes de datos basados en System.Linq.IQueryProvider usanSystem.Linq.IQueryable<T> orígenes de datos y árboles de expresiones. Los árboles de expresión tienen limitaciones en la sintaxis de C# permitida. Además, cada origen de datos de IQueryProvider
, como EF Core puede imponer más restricciones. Compruebe la documentación del origen de datos.
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.
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.
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);
}
}