Introducción a las consultas LINQ en C#

Una consulta es una expresión que recupera datos de un origen de datos. Los distintos orígenes de datos tienen diferentes lenguajes de consulta nativos, por ejemplo SQL para bases de datos relacionales y XQuery para XML. Los programadores deben aprender un lenguaje de consultas nuevo para cada tipo de origen de datos o formato de datos que deben admitir. LINQ simplifica esta situación al ofrecer un modelo de lenguaje C# coherente para tipos de orígenes de datos y formatos. En una consulta LINQ, siempre se trabaja con objetos de C#. Use los mismos patrones de codificación básicos para consultar y transformar datos en documentos XML, bases de datos SQL, colecciones de .NET y cualquier otro formato cuando un proveedor LINQ esté disponible.

Las tres partes de una operación de consulta

Todas las operaciones de consulta LINQ constan de tres acciones distintas:

  1. Obtener el origen de datos.
  2. Crear la consulta.
  3. Ejecutar la consulta.

En el siguiente ejemplo se muestra cómo se expresan las tres partes de una operación de consulta en código fuente. En el ejemplo se usa una matriz de enteros como origen de datos para su comodidad, aunque se aplican los mismos conceptos a otros orígenes de datos. En el resto de este tema se hará referencia a este artículo.

// The Three Parts of a LINQ Query:
// 1. Data source.
int[] numbers = [ 0, 1, 2, 3, 4, 5, 6 ];

// 2. Query creation.
// numQuery is an IEnumerable<int>
var numQuery =
    from num in numbers
    where (num % 2) == 0
    select num;

// 3. Query execution.
foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}

En la siguiente ilustración se muestra toda la operación de consulta. En LINQ, la ejecución de la consulta es distinta de la propia consulta. En otras palabras, no se recupera ningún dato mediante la creación de una variable de consulta.

Diagrama de la operación de consulta LINQ completa.

El origen de datos

El origen de datos del ejemplo anterior es una matriz, que admite la interfaz genérica IEnumerable<T>. Este hecho implica que se puede consultar con LINQ. Se ejecuta una consulta en una instrucción foreach, y foreach requiere IEnumerable o bien IEnumerable<T>. Los tipos compatibles con IEnumerable<T> o una interfaz derivada, como la interfaz genérica IQueryable<T>, se denominan tipos consultables.

Un tipo consultable no requiere ninguna modificación ni ningún tratamiento especial para actuar como origen de datos de LINQ. Si el origen de datos no está en la memoria como tipo consultable, el proveedor de LINQ debe representarlo como tal. Por ejemplo, LINQ to XML carga un documento XML en un tipo consultable XElement:

// Create a data source from an XML document.
// using System.Xml.Linq;
XElement contacts = XElement.Load(@"c:\myContactList.xml");

Con EntityFramework, se crea una asignación relacional de objetos entre las clases de C# y el esquema de la base de datos. Después, se escriben las consultas en los objetos y, en tiempo de ejecución, EntityFramework controla la comunicación con la base de datos. En el ejemplo siguiente, Customers representa una tabla específica en una base de datos, y el tipo del resultado de la consulta, IQueryable<T>, se deriva de IEnumerable<T>.

Northwnd db = new Northwnd(@"c:\northwnd.mdf");

// Query for customers in London.
IQueryable<Customer> custQuery =
    from cust in db.Customers
    where cust.City == "London"
    select cust;

Para obtener más información sobre cómo crear tipos específicos de orígenes de datos, consulte la documentación de los distintos proveedores de LINQ. Aun así, la regla básica es sencilla: un origen de datos de LINQ es cualquier objeto que admita la interfaz genérica IEnumerable<T> o una interfaz que la haya heredado, normalmente IQueryable<T>.

Nota:

Los tipos como ArrayList, que admiten la interfaz no genérica IEnumerable, también se pueden usar como origen de datos de LINQ. Para más información, consulte el procedimiento para consultar un objeto ArrayList con LINQ (C#).

Consulta

La consulta especifica la información que se debe recuperar de los orígenes de datos. Opcionalmente, una consulta también especifica cómo se debe ordenar, agrupar y dar forma a esa información antes de devolverse. Las consultas se almacenan en una variable de consulta y se inicializan con una expresión de consulta. Use sintaxis de consulta de C# para escribir consultas.

La consulta del ejemplo anterior devuelve todos los números pares de la matriz de enteros. La expresión de consulta contiene tres cláusulas: from, where y select. (Si está familiarizado con SQL, ha observado que la ordenación de las cláusulas se invierte del orden en SQL). La cláusula from especifica el origen de datos, la cláusula where aplica el filtro y la cláusula select especifica el tipo de los elementos devueltos. Todas las cláusulas de consulta se describen en detalle en esta sección. Por ahora, lo importante es que en LINQ la variable de consulta no efectúa ninguna acción y no devuelve ningún dato. Lo único que hace es almacenar la información necesaria para generar los resultados cuando se ejecuta la consulta en algún momento posterior. Para obtener más información sobre cómo se construyen las consultas, consulte Información general sobre operadores de consulta estándar (C#).

Nota:

Las consultas también se pueden expresar empleando una sintaxis de método. Para obtener más información, vea Query Syntax and Method Syntax in LINQ (Sintaxis de consulta y sintaxis de método en LINQ).

Clasificación de operadores de consulta estándar por modo de ejecución

Las implementaciones de LINQ to Objects de los métodos de operador de consulta estándar se ejecutan de una de dos formas principales: inmediata o aplazada. Los operadores de consulta que usan la ejecución aplazada se pueden dividir además en dos categorías: de streaming y de no streaming.

Inmediato

La ejecución inmediata significa que se lee el origen de datos y que la operación se realiza una vez. Todos los operadores de consulta estándar que devuelven un resultado escalar se ejecutan de manera inmediata. Ejemplos de estas consultas son Count, Max, Average y First. Estos métodos se ejecutan sin una instrucción foreach explícita porque la propia consulta debe usar foreach para devolver un resultado. Estas consultas devuelven un único valor, y no una colección IEnumerable. Puede forzar que cualquier consulta se ejecute inmediatamente si usa los métodos Enumerable.ToList o Enumerable.ToArray. La ejecución inmediata permite reutilizar los resultados de la consulta, no su declaración. Los resultados se recuperan una vez y, después, se almacenan para usarlos en el futuro. La consulta siguiente devuelve un recuento de los números pares de la matriz de origen:

var evenNumQuery =
    from num in numbers
    where (num % 2) == 0
    select num;

int evenNumCount = evenNumQuery.Count();

Para forzar la ejecución inmediata de cualquier consulta y almacenar en caché los resultados correspondientes, puede llamar a los métodos ToList o ToArray.

List<int> numQuery2 =
    (from num in numbers
        where (num % 2) == 0
        select num).ToList();

// or like this:
// numQuery3 is still an int[]

var numQuery3 =
    (from num in numbers
        where (num % 2) == 0
        select num).ToArray();

También puede forzar la ejecución colocando el bucle foreach justo después de la expresión de consulta, aunque, si se llama a ToList o a ToArray, también se almacenan en caché todos los datos de un objeto de colección.

Aplazado

La ejecución aplazada significa que la operación no se realiza en el punto en el código donde se declara la consulta. La operación se realiza solo cuando se enumera la variable de consulta, por ejemplo, mediante una instrucción foreach. Los resultados de ejecutar la consulta dependen del contenido del origen de datos cuando se ejecuta la consulta en lugar de cuando se define la consulta. Si la variable de consulta se enumera varias veces, es posible que los resultados difieran cada vez. Casi todos los operadores de consulta estándar cuyo tipo de valor devuelto es IEnumerable<T> o IOrderedEnumerable<TElement> se ejecutan de una manera diferida. La ejecución diferida aporta la facilidad de reutilizar las consultas, ya que la consulta captura los datos actualizados del origen de datos cada vez que se iteran los resultados de la consulta. En el código siguiente se muestra un ejemplo de ejecución aplazada:

foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}

La instrucción foreach es también donde se recuperan los resultados de la consulta. Por ejemplo, en la consulta anterior, la variable de iteración num contiene cada valor (de uno en uno) en la secuencia devuelta.

Dado que la propia variable de consulta nunca contiene los resultados de la consulta, puede ejecutarla repetidamente para recuperar los datos actualizados. Por ejemplo, una aplicación independiente podría actualizar una base de datos continuamente. En la aplicación, podría crear una consulta que recupere los datos más recientes y podría ejecutarla a intervalos para recuperar los resultados actualizados.

Los operadores de consulta que usan la ejecución aplazada se pueden clasificar además como de streaming o de no streaming.

Streaming

Los operadores de streaming no deben leer todos los datos de origen antes de que generen elementos. En el momento de la ejecución, un operador de streaming realiza su operación en cada elemento de origen mientras se lee y proporciona el elemento si es necesario. Un operador de streaming continúa leyendo los elementos de origen hasta que se puede generar un elemento de resultado. Esto significa que es posible leer más de un elemento de origen para generar un elemento de resultado.

De no streaming

Los operadores de no streaming deben leer todos los datos de origen antes de poder generar un elemento de resultado. Las operaciones como la ordenación o la agrupación pertenecen a esta categoría. En tiempo de ejecución, los operadores de consulta de no streaming leen todos los datos de origen, los colocan en una estructura de datos, realizan la operación y generan los elementos resultantes.

Tabla de clasificación

En la tabla siguiente se clasifica cada método de operador de consulta estándar según su método de ejecución.

Nota:

Si un operador se marca en dos columnas, dos secuencias de entrada intervienen en la operación, y cada secuencia se evalúa de manera diferente. En estos casos, siempre es la primera secuencia de la lista de parámetros la que se evalúa en un modo de transmisión diferido.

Operador de consulta estándar Tipo de valor devuelto Ejecución inmediata Ejecución aplazada de streaming Ejecución aplazada de no streaming
Aggregate TSource X
All Boolean X
Any Boolean X
AsEnumerable IEnumerable<T> X
Average Valor numérico único x
Cast IEnumerable<T> X
Concat IEnumerable<T> X
Contains Boolean X
Count Int32 X
DefaultIfEmpty IEnumerable<T> X
Distinct IEnumerable<T> X
ElementAt TSource X
ElementAtOrDefault TSource? X
Empty IEnumerable<T> X
Except IEnumerable<T> X X
First TSource X
FirstOrDefault TSource? X
GroupBy IEnumerable<T> X
GroupJoin IEnumerable<T> X X
Intersect IEnumerable<T> X X
Join IEnumerable<T> X X
Last TSource X
LastOrDefault TSource? X
LongCount Int64 X
Max Valor numérico único, TSource o TResult? x
Min Valor numérico único, TSource o TResult? X
OfType IEnumerable<T> X
OrderBy IOrderedEnumerable<TElement> X
OrderByDescending IOrderedEnumerable<TElement> X
Range IEnumerable<T> X
Repeat IEnumerable<T> X
Reverse IEnumerable<T> X
Select IEnumerable<T> X
SelectMany IEnumerable<T> X
SequenceEqual Boolean X
Single TSource X
SingleOrDefault TSource? X
Skip IEnumerable<T> X
SkipWhile IEnumerable<T> X
Sum Valor numérico único x
Take IEnumerable<T> X
TakeWhile IEnumerable<T> X
ThenBy IOrderedEnumerable<TElement> X
ThenByDescending IOrderedEnumerable<TElement> X
ToArray Matriz TSource[] x
ToDictionary Dictionary<TKey,TValue> X
ToList IList<T> X
ToLookup ILookup<TKey,TElement> X
Union IEnumerable<T> X
Where IEnumerable<T> X

LINQ to Objects

"LINQ to Objects" hace referencia al uso de consultas LINQ con cualquier colección IEnumerable o IEnumerable<T> directamente. Puede usar LINQ para consultar cualquier colección enumerable, como List<T>, Array o Dictionary<TKey,TValue>. La colección puede haberla definido el usuario, o bien puede que la haya devuelto una API de .NET. En el enfoque de LINQ, se escribe código declarativo que describe qué se quiere recuperar. LINQ to Objects proporciona una excelente introducción a la programación con LINQ.

Las consultas LINQ ofrecen tres ventajas principales con respecto a los bucles foreach tradicionales:

  • Son más concisas y legibles, especialmente cuando se filtran varias condiciones.
  • Proporcionan funcionalidades eficaces para filtrar, ordenar y agrupar con un código de aplicación mínimo.
  • Se pueden migrar a otros orígenes de datos con muy poca o ninguna modificación.

Cuanto más compleja sea la operación que se quiere realizar en los datos, más ventajas se obtienen al usar LINQ en lugar de las técnicas de iteración tradicionales.

Almacenar los resultados de una consulta en memoria

Una consulta es básicamente un conjunto de instrucciones sobre cómo recuperar y organizar los datos. Las consultas se ejecutan de forma diferida, ya que se solicita cada elemento subsiguiente del resultado. Cuando se usa foreach para iterar los resultados, los elementos se devuelven a medida que se tiene acceso a ellos. Para evaluar una consulta y almacenar los resultados sin ejecutar un bucle foreach, simplemente llame a uno de los métodos siguientes en la variable de consulta:

Debe asignar el objeto de colección devuelto a una nueva variable al almacenar los resultados de consulta, como se muestra en el ejemplo siguiente:

List<int> numbers = [1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20];

IEnumerable<int> queryFactorsOfFour =
    from num in numbers
    where num % 4 == 0
    select num;

// Store the results in a new variable
// without executing a foreach loop.
var factorsofFourList = queryFactorsOfFour.ToList();

// Read and write from the newly created list to demonstrate that it holds data.
Console.WriteLine(factorsofFourList[2]);
factorsofFourList[2] = 0;
Console.WriteLine(factorsofFourList[2]);

Consulte también