LINQ (Language Integrated Query) es una biblioteca de métodos para filtrar, transformar, ordenar, agrupar y resumir secuencias. Añade using System.Linq; y todo array, List<T>, Dictionary y demás IEnumerable<T> gana métodos como Where, Select y OrderBy, cada uno de los cuales recibe una lambda que dice qué hacer con un elemento.
Una primera consulta: sintaxis de métodos y sintaxis de consulta
LINQ tiene dos notaciones. La sintaxis de métodos encadena llamadas; la sintaxis de consulta se lee como SQL. El compilador convierte la sintaxis de consulta en las mismas llamadas a métodos, así que producen resultados idénticos.
Salida:
95, 91, 88
95, 91, 88
La sintaxis de métodos es más habitual en el código moderno, y algunos operadores (Take, Distinct, Count, First, ToList) solo existen como métodos. La sintaxis de consulta brilla en los joins y en las consultas que introducen variables intermedias con let. El resto de esta página usa la sintaxis de métodos y muestra la sintaxis de consulta donde aporta algo.
Where y Select: filtrar y proyectar
Where conserva los elementos que cumplen una condición. Select convierte cada elemento en otra cosa. Juntos cubren la mayoría de las consultas del día a día.
Salida:
Mug, Kettle
MUG $8.50 | LAMP $32.00 | KETTLE $27.00 | RUG $120.00
Lamp: 38.40
Rug: 144.00
1. Mug 2. Lamp 3. Kettle 4. Rug
Where nunca cambia los elementos, y Select nunca cambia cuántos hay. new { p.Name, WithTax = ... } crea un tipo anónimo, una clase que el compilador escribe por ti, cuyas propiedades lees con la sintaxis de punto normal. No tiene un nombre que puedas escribir, así que una variable que contenga uno debe declararse con var, y encaja con resultados que se quedan dentro de un método.
OrderBy, ThenBy y ordenar por varias claves
Salida:
Ana 60
Ana 15
Ben 90
Ben 40
Chloe 25
Ben, Ana, Ben, Chloe, Ana
OrderBy es estable: los elementos con claves iguales conservan su orden original. Usa ThenBy o ThenByDescending para una segunda clave. Un segundo OrderBy volvería a ordenarlo todo por su propia clave y dejaría la primera clave como simple desempate: la prioridad contraria. En la sintaxis de consulta, las claves se separan con comas: orderby o.Customer, o.Total descending.
A diferencia de List<T>.Sort, OrderBy no toca el origen y devuelve una secuencia nueva.
First, Single, Last y sus versiones OrDefault
Estos devuelven un elemento en lugar de una secuencia. Las versiones simples lanzan una excepción cuando no hay nada que devolver; las versiones OrDefault devuelven el valor por defecto del tipo.
Salida:
Ana
Ben
Bea
True
First: InvalidOperationException
Single: InvalidOperationException
0
La excepción de First sobre una secuencia vacía es InvalidOperationException con el mensaje "Sequence contains no elements" (o "Sequence contains no matching element" cuando un predicado no coincidió con nada), uno de los errores de C# más buscados. Usa FirstOrDefault cuando "ninguno" sea un resultado normal y comprueba si es null.
La última línea muestra la trampa de los tipos de valor: en una secuencia de int, FirstOrDefault devuelve 0 cuando nada coincide, lo que no se distingue de un 0 real. Filtra con Where y comprueba antes Any(), o en .NET 6 y posteriores pasa tu propio valor por defecto: scores.FirstOrDefault(s => s > 100, -1).
Single es para "exactamente uno": lanza una excepción si no hay ninguno o si hay más de uno, lo que lo convierte tanto en una aserción como en una consulta.
Any, All, Count y Contains
Salida:
True
True
True
3
True
True
Any() se detiene en la primera coincidencia, así que items.Any() es la forma correcta de preguntar "¿hay algo?". items.Count() > 0 lee la propiedad Count cuando el origen es una colección como una lista o un array, pero en una consulta u otra secuencia perezosa recorre antes todos los elementos. All sobre una secuencia vacía devuelve true, porque no hay ningún elemento que incumpla la condición.
Sum, Average, Min y Max
Salida:
Total: 28.25
Items: 7
Priciest: 7.25
Average price: 4.92
0
Average: InvalidOperationException
Sum de una secuencia vacía es 0, pero Average, Min y Max de una secuencia vacía de números lanzan InvalidOperationException: no hay una respuesta con sentido. Protégete con Any(), o usa antes DefaultIfEmpty(): empty.DefaultIfEmpty(0).Average().
Distinct, Take y Skip
Salida:
home, shop, cart
Page 1: 1 2 3 4 5 6 7 8 9 10
Page 2: 11 12 13 14 15 16 17 18 19 20
Page 3: 21 22 23
12, 15
Distinct conserva en orden la primera aparición de cada valor. Skip(n).Take(m) es el patrón habitual de paginación, y TakeWhile/SkipWhile se detienen o empiezan en el primer elemento que incumple una condición. Enumerable.Range(start, count) genera una secuencia de enteros sin necesidad de un array.
GroupBy
GroupBy reparte los elementos en cubos según una clave. Cada grupo tiene una Key y es a su vez una secuencia, así que puedes agregarlo.
Salida:
North: 3 sales, total 600
South: 2 sales, total 400
East: 1 sales, total 350
Top rep: Ana 400
North -> Ana/Chloe/Ana; South -> Ben/Eli; East -> Dev
Los grupos salen en el orden en que sus claves aparecen por primera vez en el origen (North, South, East), no ordenados; añade OrderBy(g => g.Key) si lo necesitas. En la sintaxis de consulta, group X by K elige qué va en cada grupo (aquí solo el nombre del comercial) e into g da nombre al grupo para que la consulta pueda continuar.
ToList, ToArray y ToDictionary
Los métodos To... ejecutan la consulta y guardan los resultados.
Salida:
ben@x.com
2
ToDictionary: ArgumentException
ToDictionary lanza ArgumentException cuando dos elementos producen la misma clave, igual que Dictionary.Add. Si se esperan duplicados, agrupa primero (como hace wordCounts) o usa ToLookup, que asocia cada clave a una secuencia de valores.
Ejecución diferida
Where, Select, OrderBy, GroupBy y la mayoría de los demás operadores devuelven un objeto consulta y no hacen ningún trabajo hasta que algo lo enumera. Eso tiene dos consecuencias que conviene ver:
Salida:
25, 40, 99
converting 10
converting 25
converting 40
converting 99
Count: 4
converting 10
converting 25
converting 40
converting 99
Max: 9900
converting 10
converting 25
converting 40
converting 99
Cached: 4, 9900
La primera consulta vio el 99 aunque se añadió después de escribir la consulta: el filtro se ejecutó cuando string.Join la enumeró. La segunda consulta imprimió sus conversiones dos veces porque Count() y Max() la enumeraron cada uno desde cero. Con una lambda barata, eso solo es trabajo desperdiciado; con una consulta a una base de datos, una lectura de archivo o un número aleatorio dentro de la lambda, las dos pasadas pueden devolver datos distintos. Cuando vayas a usar un resultado más de una vez, materialízalo con ToList() o ToArray().
Los operadores que devuelven un solo valor (Count, Sum, First, Any, Max) y los métodos To... se ejecutan de inmediato. Consulta IEnumerable y yield para ver cómo se construye esa pereza.
Referencia rápida
| Operador | Hace | Devuelve |
|---|---|---|
Where(x => cond) | Filtrar | Secuencia (diferida) |
Select(x => expr) | Transformar cada uno | Secuencia (diferida) |
SelectMany(x => seq) | Aplanar secuencias anidadas | Secuencia (diferida) |
OrderBy, ThenBy (+Descending) | Ordenar, estable | Secuencia (diferida) |
GroupBy(x => key) | Repartir por clave | Secuencia de grupos (diferida) |
Distinct, Take, Skip | Quitar duplicados, paginar | Secuencia (diferida) |
First, Single, Last | Un elemento, excepción si no hay | Elemento |
FirstOrDefault etc. | Un elemento o el valor por defecto | Elemento |
Any, All, Contains | Comprobar | bool |
Count, Sum, Min, Max, Average | Agregar | Número |
ToList, ToArray, ToDictionary | Ejecutar y guardar | Colección |
Errores comunes
First()sobre una secuencia que puede estar vacía. Lanza "Sequence contains no elements"; usaFirstOrDefaulty comprueba.- Dos llamadas a
OrderBy. La segunda clave pasa a ser la principal; usaThenBy. - Enumerar una consulta varias veces. El trabajo se repite; llama a
ToList()una vez. - Esperar que una consulta haga una instantánea de su origen. Lee el origen cuando se enumera, no cuando se escribe.
Average,Min,Maxsobre una secuencia vacía. Lanzan una excepción; protégete conAny()oDefaultIfEmpty.Count() > 0para comprobar si hay elementos.Any()se detiene en el primero.
Preguntas frecuentes
¿Qué es LINQ en C#?
LINQ (Language Integrated Query) es un conjunto de métodos de extensión de System.Linq, como Where, Select, OrderBy y GroupBy, que funcionan con cualquier IEnumerable<T>: arrays, listas, diccionarios y resultados de otras consultas. C# tiene también una sintaxis de consulta (from x in items where ... select ...) que el compilador traduce a las mismas llamadas a métodos.
¿Qué diferencia hay entre Select y Where en LINQ?
Where filtra: conserva los elementos para los que la lambda devuelve true y los deja sin cambios. Select proyecta: transforma cada elemento en otra cosa, como una propiedad o un objeto nuevo, y mantiene el mismo número de elementos. Suelen encadenarse: people.Where(p => p.Age >= 18).Select(p => p.Name).
¿Qué diferencia hay entre First y FirstOrDefault?
First() devuelve el primer elemento (o la primera coincidencia de un predicado) y lanza InvalidOperationException ("Sequence contains no elements" o "no matching element") cuando no hay ninguno. FirstOrDefault() devuelve en su lugar el valor por defecto del tipo: null en las clases y 0 en int, así que comprueba el resultado antes de usarlo.
¿Cómo ordeno por dos campos con LINQ?
Encadena ThenBy después de OrderBy: orders.OrderBy(o => o.Customer).ThenByDescending(o => o.Total). Llamar dos veces a OrderBy no funciona: la segunda llamada vuelve a ordenar toda la secuencia, así que su clave pasa a ser la principal y la primera solo deshace empates, lo contrario de lo que se pretendía.
¿Cómo funciona GroupBy en LINQ?
GroupBy(x => key) devuelve un grupo por cada clave distinta. Cada grupo es un IGrouping<TKey, TElement>: tiene una propiedad Key y es a su vez una secuencia de los elementos con esa clave, así que puedes llamar a Count(), Sum() o Select sobre él. Los grupos salen en el orden en que aparecen por primera vez sus claves.
¿Qué es la ejecución diferida en LINQ?
La mayoría de los operadores de LINQ no se ejecutan cuando los llamas; devuelven una consulta que se ejecuta cada vez que se enumera (con foreach, ToList(), Count(), etc.). Por eso una consulta ve los cambios hechos en el origen después de definirla, y enumerarla dos veces hace el trabajo dos veces. Llama a ToList() o ToArray() para ejecutarla una vez y conservar los resultados.