Select() transforma cada elemento en un único elemento nuevo. SelectMany() transforma cada elemento en una secuencia de elementos y después concatena esas secuencias en un único resultado plano.

Esa diferencia se nota en el recuento. Si pasamos diez departamentos por Select(), obtenemos diez resultados. Si pasamos diez departamentos por SelectMany(), obtenemos todos los empleados de todos ellos. Así que la pregunta que hay que hacerse nunca es qué método es mejor. La pregunta es si proyectamos cada elemento en un solo valor o en varios.

Para descargar el código fuente de este artículo, puedes visitar nuestro repositorio de GitHub.

Cómo funciona el método Select

Cuando hablamos de LINQ (Language Integrated Query) en C#, el método Select() es una de las primeras herramientas que nos vienen a la mente.

Podemos usar una de las sobrecargas disponibles:

public static IEnumerable<TResult> Select<TSource,TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector);

Este método de extensión acepta la función de transformación con un único parámetro, que es el elemento de la colección. Es el método que la mayoría de los desarrolladores de C# usamos a diario.

Como alternativa, podemos usar la otra sobrecarga:

public static IEnumerable<TResult> Select<TSource,TResult> (this IEnumerable<TSource> source, Func<TSource,int,TResult> selector);

Esta vez, el método acepta la función de transformación con dos parámetros.

El segundo parámetro representa el índice del elemento de la colección. Puede ser útil cuando queremos usar el índice del elemento en la función de transformación.

Ambos métodos son una parte fundamental de LINQ y nos permiten hacer proyecciones sobre los datos.

Simplificar la proyección de datos

En el contexto de LINQ, la proyección es la transformación de cada elemento de una colección en una nueva forma.

Eso es lo que hace el método Select(). Toma cada elemento de una colección y le aplica una función, lo que da como resultado una nueva colección en la que cada elemento es la versión transformada del original.

Para visualizarlo, tomemos un ejemplo:

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var doubledNumbers = numbers.Select(num => num * 2);

Aquí, usamos la expresión lambda que describe cómo se debe transformar cada elemento num de la lista numbers.

El resultado es una nueva colección doubledNumbers en la que cada número es el doble de su valor original.

Del mismo modo, podemos usar la segunda sobrecarga:

var transformedNumbers = numbers.Select((num, index) => num * index);

Como vemos, igual que en el ejemplo anterior, usamos el método Select() para transformar una colección de enteros, pero esta vez multiplicamos cada elemento de la colección por su índice.

En este caso, el resultado también es una nueva colección transformedNumbers.

Cómo funciona el método SelectMany

Ahora que sabemos usar el método Select() en LINQ, pasemos a otra potente herramienta del arsenal de LINQ: el método SelectMany().

Como en el caso anterior, LINQ nos ofrece varios métodos sobrecargados entre los que elegir:

public static IEnumerable<TResult> SelectMany<TSource,TCollection,TResult> (this IEnumerable<TSource> source, Func<TSource,IEnumerable<TCollection>> collectionSelector, Func<TSource,TCollection,TResult> resultSelector);

Este método de extensión acepta la función collectionSelector, que transforma la colección, y la función resultSelector, que transforma cada elemento de la colección aplanada.

También podemos usar esta otra sobrecarga:

public static IEnumerable<TResult> SelectMany<TSource,TCollection,TResult> (this IEnumerable<TSource> source, Func<TSource,int,IEnumerable<TCollection>> collectionSelector, Func<TSource,TCollection,TResult> resultSelector);

Igual que con el método Select(), también podemos usar una sobrecarga que tiene en cuenta el índice del elemento en la colección.

A continuación tenemos la sobrecarga que podemos usar para aplanar la colección sin transformar sus elementos:

public static IEnumerable<TResult> SelectMany<TSource,TResult> (this IEnumerable<TSource> source, Func<TSource,IEnumerable<TResult>> selector);

Esta vez, el método de extensión solo acepta una función selector, que se usa para aplanar la colección.

Por último, tenemos una variante de esa sobrecarga:

public static IEnumerable<TResult> SelectMany<TSource,TResult> (this IEnumerable<TSource> source, Func<TSource,int,IEnumerable<TResult>> selector);

En esta sobrecarga, también se acepta una función selector como parámetro, pero añadimos un segundo parámetro que representa el índice del elemento.

Aplanar colecciones

El método SelectMany() está pensado para los casos complejos en los que trabajamos con colecciones de colecciones.

Hace algo más que transformar cada elemento de una colección. Aplana varias colecciones anidadas en una única secuencia unidimensional.

Este aplanamiento es lo que distingue al método SelectMany() del método Select().

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

Los siguientes ejemplos usan dos clases y una enumeración, que colocamos en un nuevo archivo Models.cs junto a Program.cs:

public class Department
{
    public string Name { get; set; }
    public List<Employee> Employees { get; set; }
}

public class Employee
{
    public string Name { get; set; }
    public JobPosition JobPosition { get; set; }
}

public enum JobPosition
{
    Admin,
    Developer,
    HumanResources
}

Para entender mejor el método SelectMany(), definamos una lista:

var listOfDepartments = new List<Department>
{
    new Department
    {
        Name = "TechSupport",
        Employees = new()
        {
            new Employee { Name = "Thomas", JobPosition = JobPosition.Admin },
            new Employee { Name = "Cynthia", JobPosition = JobPosition.Admin }
        }
    },
    new Department
    {
        Name = "Development",
        Employees = new()
        {
            new Employee { Name = "Eric", JobPosition = JobPosition.Developer },
            new Employee { Name = "Laura", JobPosition = JobPosition.Developer },
            new Employee { Name = "Cedric", JobPosition = JobPosition.Developer }
        }
    },
    new Department
    {
        Name = "BackOffice",
        Employees = new()
        {
            new Employee { Name = "Monica", JobPosition = JobPosition.HumanResources }
        }
    }
};

Aquí, tenemos una lista de instancias de una clase Department. La clase Department contiene una propiedad Name con el nombre del departamento y una lista de instancias de Employee en una propiedad de colección llamada Employees.

Por último, la clase Employee contiene las propiedades Name y JobPosition, que describen al empleado.

Ahora, veamos cómo usar el método SelectMany() para aplanarla:

var listOfEmployees = listOfDepartments.SelectMany(department => department.Employees);

Aquí, usamos el método SelectMany() para extraer las listas de Employee de cada elemento de la lista listOfDepartments y combinarlas en una sola secuencia. Después de esta operación, la variable listOfEmployees contendrá una consulta que devuelve todos los empleados.

Como alternativa, podemos usar una de las sobrecargas para enriquecer los datos de los empleados con los datos del departamento:

var detailedListOfEmployees = listOfDepartments.SelectMany(department => department.Employees,
        (department, employee) => $"{employee.Name} | {department.Name} | {employee.JobPosition}")
    .ToList();

Primero, aplanamos la colección de objetos Department en una lista de objetos Employee.

Después, modificamos cada elemento resultante de esa operación y lo transformamos en una cadena con datos de los dos objetos, Department y Employee.

Un operador no es la única forma de obtener una secuencia plana a partir de datos anidados. Nuestra guía sobre cómo aplanar colecciones anidadas menciona AutoMapper para los casos en los que no queremos una consulta.

¿Qué es SelectMany en C# y qué hace?

La referencia de Enumerable.SelectMany de Microsoft lo define en una sola línea: el método “[p]royecta cada elemento de una secuencia en un IEnumerable<T> y aplana las secuencias resultantes en una sola secuencia”. Es el operador de aplanamiento de LINQ.

Dada una lista de departamentos en la que cada departamento contiene una lista de empleados, Select() devuelve una secuencia de listas de empleados. SelectMany() devuelve una secuencia de empleados.

Aplana exactamente un nivel. Una lista de listas se convierte en una lista. Una lista de listas de listas se convierte en una lista de listas y necesita una segunda llamada.

Su segunda forma útil es la sobrecarga con selector de resultados, que mantiene el elemento exterior dentro del ámbito mientras proyecta el interior. Así es como obtenemos “el empleado y el departamento del que procede” en una sola pasada, sin un join.

El compilador de C# también se apoya en él. Cada cláusula from adicional de una expresión de consulta se convierte en una llamada a SelectMany().

Los tipos lo dicen todo:

var employeeLists = listOfDepartments.Select(d => d.Employees);      // IEnumerable<List<Employee>>
var employees = listOfDepartments.SelectMany(d => d.Employees);      // IEnumerable<Employee>

Select vs SelectMany: ¿cuál es la diferencia?

Ahora que conocemos los métodos Select() y SelectMany() de LINQ, es importante entender en qué se diferencian y cuándo usar cada uno. Aunque a primera vista puedan parecer similares, tienen propósitos distintos y se aprovechan mejor en casos diferentes.

Los dos métodos proyectan. La diferencia está en lo que devuelve la proyección y en lo que LINQ hace después con ello.

Select() recibe un selector que devuelve un valor y produce un resultado por cada elemento de origen. La forma del origen se conserva: entran diez, salen diez.

SelectMany() recibe un selector que devuelve una secuencia y produce, en orden, todos los elementos de cada una de esas secuencias. Si entran diez, obtenemos tantos como sumen entre todas las secuencias internas, que pueden ser más, menos o ninguno.

Equivocarse aquí tiene un síntoma reconocible. Proyectar una propiedad de colección con Select() produce un IEnumerable<List<T>>, una secuencia de listas, y la siguiente línea no compila, o un foreach nos devuelve listas donde esperábamos elementos.

Los dos usan ejecución diferida y el patrón iterador, así que ninguno hace nada hasta que algo enumera el resultado.

Select()SelectMany()
El selector devuelveUn valor por elementoUna secuencia por elemento
Número de resultadosEl mismo que el origenLa suma de las longitudes de las secuencias internas
Forma del resultadoIEnumerable<TResult>IEnumerable<TResult>, aplanado un nivel
Proyectar una propiedad de colecciónIEnumerable<List<Employee>>, anidadoIEnumerable<Employee>, plano
Niveles aplanadosNingunoExactamente uno
Equivalente en sintaxis de consultaUna sola cláusula selectUna segunda cláusula from
Sobrecarga con el elemento de origenNo se aplicaSelector de resultados (source, inner) => ...
Sobrecarga con índiceSíSí
EjecuciónDiferidaDiferida
Úsalo cuandoCada elemento se corresponde con un elementoCada elemento contiene elementos que quieres por separado

El compilador también recurre a SelectMany() por nosotros. Esta expresión de consulta produce la misma secuencia plana que la llamada al método:

employees = from department in listOfDepartments
            from employee in department.Employees
            select employee;

No es una coincidencia de estilo. La referencia de LINQ de Microsoft recoge la regla: “En la sintaxis de expresiones de consulta, cada cláusula from (C#) o From (Visual Basic) posterior a la inicial se traduce en una invocación de SelectMany”.

Casos de uso

Para empezar, veamos cuándo conviene usar el método Select() y cuándo el método SelectMany().

El método Select() sirve para:

  • Convertir una lista de un tipo en otro tipo
  • Obtener una propiedad concreta de los objetos de una colección
  • Hacer cálculos, como en nuestro ejemplo, en el que duplicamos cada número

Por otro lado, el método SelectMany() resulta especialmente útil en los casos en los que necesitamos:

  • Aplanar colecciones o arrays anidados
  • Hacer operaciones que implican combinar o fusionar datos de varias colecciones
  • Trabajar con estructuras de datos jerárquicas o relacionales en las que hay elementos anidados dentro de otros elementos

Como vemos, cada uno de los métodos tiene su propio propósito. Además, la proyección rara vez va sola: el método Where() acota el origen antes de proyectarlo, y nuestro artículo sobre cómo seleccionar varios registros con LINQ aplica el mismo filtrado a una lista de claves.

Estructura de la colección resultante

Otro punto clave es la diferencia en la estructura de las colecciones resultantes.

El método Select() conserva de forma predeterminada la estructura de la colección original. Si partimos de una colección de 10 elementos, acabamos con otra colección de 10 elementos, aunque transformados.

En cambio, el método SelectMany() cambia la estructura de la colección. Combina los elementos de las colecciones anidadas, y el resultado suele ser una colección más grande que la original, según los elementos anidados.

Lógica interna del método Select

Ahora, veamos el interesante funcionamiento interno del método Select():

Select proyecta cada elemento en un resultado y conserva el número de elementos

Cuando usamos el método Select() para proyectar la colección en una nueva forma, no se ejecuta de inmediato.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

Lo que hace es crear una instancia de IEnumerable que contiene todos los detalles de la operación que queremos realizar. Esto es lo que llamamos ejecución diferida.

Cuando empezamos a recorrer este IEnumerable, entra en juego el patrón iterador. Este patrón le permite procesar cada elemento uno a uno, igual que lo hace un iterador con yield return. Nuestro artículo sobre cómo yield return hace posible la ejecución diferida explica paso a paso su mecánica.

Cada elemento pasa por la transformación definida por la expresión lambda que le pasamos al método Select(). Esa expresión indica qué hay que hacer con cada elemento.

Lógica interna del método SelectMany

Con todo esto en mente, veamos ahora qué lógica aplica el método SelectMany():

SelectMany proyecta cada elemento en una secuencia y las concatena en un único resultado plano

Igual que su primo, el método Select(), el método SelectMany() adopta la ejecución diferida y el patrón iterador.

Cuando su iterador se pone en marcha, procesa cada elemento de la colección de origen con la expresión lambda que le proporcionamos.

Para cada elemento de la colección de origen, el método SelectMany() no se limita a iterar sobre el IEnumerable que proporciona la lambda. Va tomando uno a uno los elementos de esas colecciones anidadas y nos los devuelve.

Eso es lo que nos da una única secuencia aplanada.

El paso Apply transformation (aplicar la transformación) del diagrama solo se ejecuta en las sobrecargas que reciben un selector de resultados, como la que usamos para construir detailedListOfEmployees. La sobrecarga que usamos para listOfEmployees llama a nuestra lambda una vez por departamento y devuelve cada empleado sin cambios.

¿Cuándo conviene usar SelectMany en lugar de Select?

Usa SelectMany() cuando cada elemento contiene varias cosas que quieres tratar por separado, y Select() cuando cada elemento se corresponde exactamente con una sola cosa.

La prueba práctica es el tipo que devuelve el selector. Si la respuesta es una colección, el operador adecuado es SelectMany(). Si es un solo valor, es Select().

Recurre a la sobrecarga con selector de resultados cuando el elemento aplanado por sí solo no basta, cuando la salida tiene que indicar de qué elemento padre procede cada elemento.

Dos llamadas son mejores que una cuando el anidamiento tiene dos niveles, pero encadenarlas una y otra vez es señal de que lo que nos pone trabas es el modelo, no el operador.

Elegir entre los dos métodos no es una decisión de rendimiento. Los dos son diferidos y los dos trabajan en streaming, así que el costo es el trabajo de la lambda y el número de elementos y secuencias internas que se producen. SelectMany() produce más solo porque le pedimos más.

Uso eficiente de Select y SelectMany

Tanto Select() como SelectMany() pueden añadir un costo adicional, sobre todo cuando trabajamos con colecciones grandes.

Por eso, deberíamos reducir al mínimo el número de transformaciones y operaciones de aplanamiento. Si es posible, deberíamos combinar varias transformaciones en una sola llamada a Select() o SelectMany().

Tenemos que recordar que las consultas LINQ no se ejecutan hasta que iteramos sobre ellas. Esto puede ser tanto una ventaja para la eficiencia como una fuente de confusión si no se gestiona bien.

Por eso, deberíamos planificar cuándo se consultan y se transforman los datos.

Aunque la ejecución diferida puede mejorar el rendimiento porque no procesa los datos hasta que es necesario, también puede provocar caídas de rendimiento inesperadas si la misma consulta se itera varias veces.

Por ese motivo, tenemos que plantearnos almacenar los resultados en caché si queremos reutilizarlos.

Consideraciones de rendimiento

Otra cosa que hay que tener en cuenta es el costo adicional en rendimiento de ambos métodos.

En general, el método Select() es eficiente, pero el rendimiento puede empeorar si la función de transformación es compleja o si se usa con colecciones muy grandes.

La diferencia de costo se debe sobre todo al número de elementos, no al operador. Los dos son diferidos y los dos ejecutan un paso del iterador por cada elemento que producen. SelectMany() produce más elementos porque eso es lo que le pedimos, y además abre un enumerador por cada secuencia interna, lo que se nota cuando las secuencias internas son pequeñas.

Es en los datos muy anidados donde se nota el número de elementos: más elementos significan más memoria ocupada si materializamos el resultado, y más trabajo en los pasos posteriores.

Aunque es importante tener en cuenta estas consideraciones de rendimiento, deberíamos evitar centrarnos demasiado en la optimización del rendimiento desde el principio, ya que puede llevar a un código complejo y difícil de leer.

Para evitarlo, deberíamos escribir siempre primero un código claro y legible, y después optimizar en función del profiling y de las necesidades reales de rendimiento.

Conclusión

Aquí hemos visto los dos operadores de proyección: qué devuelve cada uno, las sobrecargas que ofrece cada uno, la forma de expresión de consulta que el compilador reescribe como SelectMany() y la lógica de iteradores que hay detrás de ellos.

Mientras que el método Select es ideal para transformaciones sencillas de los elementos de una colección, el método SelectMany destaca al aplanar colecciones anidadas, lo que lo hace muy valioso para estructuras de datos más complejas.

Si entendemos los métodos Select y SelectMany y seguimos las buenas prácticas de eficiencia y rendimiento, podemos escribir código C# más eficiente, legible y fácil de mantener. LINQ también sigue evolucionando, y nuestro artículo sobre lo que ha cambiado en LINQ explica los operadores que llegaron con .NET 6.

Probado con .NET 10.0.10.