linq

En términos sencillos, LINQ es una serie de extensiones de lenguaje que admite la consulta de datos de forma segura; se presentará en la próxima versión de Visual Studio con nombre en código “Orcas”.

Yo era un gran aficionado de la serie Connections que presentaba James Burke, cuando se emitía en el Discovery Channel. Su premisa básica: la forma en que los descubrimientos aparentemente no relacionados influían en otros descubrimientos, lo que en último término se plasmó en algún avance del mundo moderno. La moraleja, por decirlo así, es que los avances no se consiguen de manera aislada. Lo mismo se puede aplicar a LINQ (Language Integrated Query).En términos sencillos, LINQ es una serie de extensiones de lenguaje que admite la consulta de datos de forma segura; se presentará en la próxima versión de Visual Studio con nombre en código “Orcas”. Los datos que se deben consultar pueden adoptar la forma de XML (LINQ sobre XML), bases de datos (ADO.NET habilitado para LINQ, que incluye LINQ sobre SQL, LINQ sobre Dataset y LINQ sobre Entidades), objetos (LINQ sobre Objetos), etc. La arquitectura de LINQ se muestra en la figura 1.

Figura 1 Arquitectura de LINQ
Figura 1 Arquitectura de LINQ

Miremos algún código. Un ejemplo de una consulta LINQ en la próxima versión “Orcas” de C# podría tener el aspecto siguiente:

var overdrawnQuery = from account in db.Accounts
where account.Balance < 0
select new { account.Name, account.Address };

Cuando se crean iteraciones de los resultados de esta consulta mediante foreach, cada elemento devuelto consistiría en un nombre y dirección de una cuenta que tiene un saldo inferior a 0.

Resulta obvio a partir del ejemplo anterior que la sintaxis es como la de SQL. Hace varios años, Anders Hejlsberg (diseñador principal de C#) y Peter Golde pensaron en ampliar C# para integrar mejor la consulta de datos. Peter, que en ese momento era el jefe de desarrollo del compilador C#, investigaba la posibilidad de hacer el compilador de C# ampliable, para que admitiera específicamente complementos que pudieran comprobar la sintaxis de lenguajes específicos de dominio como SQL. Anders, por otro lado, concebía un nivel de integración más profundo y más específico. Pensaba en un conjunto de “operadores de secuencia” que funcionarían sobre cualquier colección que implementara IEnumerable, así como consultas remotas para los tipos que implementaran IQueryable. Finalmente, la idea del operador de secuencias ganó mayor aceptación y, a principios de 2004, Anders sometió un documento acerca de la idea a Thinkweek de Bill Gates. La respuesta recibida fue muy positiva. En las primeras fases del diseño, una consulta sencilla tenía la sintaxis siguiente:

sequence locals = customers.where(ZipCode == 98112);

La secuencia, en este caso, era un alias para IEnumerable y la palabra “where” tenía un operador especial que el compilador podía comprender. La implementación del operador where era un método estático normal de C# que aceptaba un delegado del predicado (es decir, un delegado con la forma bool Pred(elemento T)). La idea era que el compilador tuviera un conocimiento especial acerca del operador. Esto permitiría que el compilador llamara correctamente al método estático y creara el código para conectar el delegado a la expresión.

Supongamos que en el ejemplo anterior sería la sintaxis ideal para una consulta en C#. ¿Cuál sería el aspecto de esta consulta en C# 2.0, sin extensiones de lenguaje?

IEnumerable locals = EnumerableExtensions.Where(customers,
delegate(Customer c)
{
return c.ZipCode == 98112;
});

Este código es tremendamente detallado y, lo que es peor, requiere profundizar bastante para encontrar el filtro pertinente (ZipCode == 98112). Y este ejemplo es sencillo; imaginemos lo mucho menos legible que sería con varios filtros, proyecciones, etcétera. La raíz del contenido es la sintaxis necesaria para los métodos anónimos. En la consulta ideal, la expresión sólo requeriría la expresión que se debe evaluar. El compilador entonces intentaría inferir el contexto; por ejemplo, que ZipCode se refería realmente al ZipCode definido en Customer. ¿Cómo se corrige este problema? Codificar el conocimiento de operadores específicos en el lenguaje no tuvo buena aceptación en el equipo de diseño del lenguaje, por lo que empezaron a buscar una sintaxis alternativa para métodos anónimos. Querían que fuera muy conciso y que para métodos anónimos no fueran necesarios más conocimientos que los actuales para el compilador. Finalmente, idearon las expresiones lambda.
Expresiones lambda
Las expresiones lambda son una característica del lenguaje parecida en muchos aspectos a los métodos anónimos. De hecho, si las expresiones lambda se hubieran incluido en el lenguaje desde el principio, no habría habido necesidad para los métodos anónimos. La idea básica es que el código se puede tratar como si fueran datos. En C# 1.0, es habitual pasar cadenas, enteros, tipos de referencias, etcétera a métodos, para que éstos puedan actuar sobre esos valores. Los métodos anónimos y las expresiones lambda amplían el intervalo de los valores para incluir bloques de código. Este concepto es común en la programación funcional.

Tomemos el ejemplo anterior y reemplacemos el método anónimo por una expresión lambda:

IEnumerable locals =
EnumerableExtensions.Where(customers, c => c.ZipCode == 91822);

Hay varias cosas que se deben tener en cuenta. Para empezar, la brevedad de la expresión lambda se puede atribuir a varios factores. En primer lugar, la palabra clave delegate se usa para presentar el constructor. En cambio, hay un operador nuevo, =>, que indica al compilador que no se trata de una expresión normal. En segundo lugar, el tipo Customer se infiere a partir de su uso. En este caso, la firma del método Where es parecida a ésta:

public static IEnumerable Where(
IEnumerable items, Func predicate)

El compilador puede inferir que “c” se refiere a un cliente porque el primer parámetro del método Where es IEnumerable, por lo que T debe, de hecho ser Customer. Con este conocimiento, el compilador también comprueba que Customer tiene un miembro ZipCode. Por último, no se ha especificado ninguna palabra clave de retorno. En el formulario sintáctico, el miembro de retorno se omite; pero esto se hace por pura conveniencia sintáctica. El resultado de la expresión se sigue considerando el valor de retorno.

Las expresiones lambda, como los métodos anónimos, también admiten la captura variable. Por ejemplo, es posible referirse a los parámetros o locales del método que contiene la expresión lambda dentro del cuerpo de la expresión lambda:

public IEnumerable LocalCusts(
IEnumerable customers, int zipCode)
{
return EnumerableExtensions.Where(customers,
c => c.ZipCode == zipCode);
}

Por último, las expresiones lambda admiten una sintaxis más detallada que permiten especificar los tipos explícitamente, así como ejecutar varias declaraciones. Por ejemplo:

return EnumerableExtensions.Where(customers,
(Customer c) => { int zip = zipCode; return c.ZipCode == zip; });

Las buenas noticias son que estamos mucho más cerca de la sintaxis ideal propuesta en el documento original y pudimos llegar allí con una característica del lenguaje que normalmente resulta útil fuera de los operadores de consulta. Echemos un vistazo otra vez a donde estamos:

IEnumerable locals =
EnumerableExtensions.Where(customers, c => c.ZipCode == 91822);

Hay un problema obvio aquí. En vez de pensar acerca de las operaciones que se pueden realizar en Customer, el consumidor actualmente tiene que conocer esta clase EnumerableExtensions. Además, en el caso de varios operadores, el consumidor tiene que invertir tiempo en escribir la sintaxis correcta. Por ejemplo:

IEnumerable locals =
EnumerableExtensions.Select(
EnumerableExtensions.Where(customers, c => c.ZipCode == 91822),
c => c.Name);

Observe que Select es el método exterior, aunque funciona sobre los resultados del método Where. La sintaxis ideal sería más parecida a la siguiente:

sequence locals =
customers.where(ZipCode == 98112).select(Name);

Así, ¿sería posible acercarse más a la sintaxis ideal con otra característica del lenguaje?

Back to top

Métodos de extensión
Una sintaxis mucho mejor, según se averiguó, iba a resultar en la forma de una característica del lenguaje conocida como métodos de extensión. Los métodos de extensión son básicamente métodos estáticos a los que se puede llamar a través de una sintaxis de instancia. La raíz del problema para la consulta anterior es que queremos agregar métodos a IEnumerable. Sin embargo, si tuviéramos que agregar operadores, tales como Where, Select, etc., sería obligatorio que cada implementador existente y futuro implementaran esos métodos. Aunque en realidad la gran mayoría de esas implementaciones sería igual. La única manera de compartir la “implementación de interfaz” en C# es usar métodos estáticos, que es lo que hicimos con la clase EnumerableExtensions anterior.

Supongamos que en lugar de eso tuviéramos que escribir el método Where como método de extensión. Entonces, la consulta se podría volver a escribir así:

IEnumerable locals =
customers.Where(c => c.ZipCode == 91822);

Para esta consulta sencilla, esta sintaxis es muy cercana al ideal. Pero ¿qué significa exactamente escribir el método Where como método de extensión? En realidad se trata de algo bastante sencillo. Básicamente la firma del método estático cambia de modo que se agrega un modificador “this” al primer parámetro:

public static IEnumerable Where(
this IEnumerable items, Func predicate)

Además, el método debe declararse dentro de una clase estática. Una clase estática es una que sólo puede contener miembros estáticos y que se indica con el modificador static en la declaración de la clase. Así de simple. Esta declaración indica al compilador que permita llamar a Where con la misma sintaxis que un método de instancia en cualquier tipo que implemente IEnumerable. Sin embargo, el método Where debe ser accesible desde el ámbito actual. Un método está en el ámbito cuando el tipo que contiene está en el ámbito. Por lo tanto, es posible traer métodos de extensión al ámbito usando la directiva Using. (Para obtener más información, consulte la barra lateral “Métodos de extensión”).

Ahora tenemos una sintaxis que es muy parecida a la ideal para la cláusula del filtro, pero ¿es eso todo lo que aporta la versión “Orcas” de C#? No exactamente; ampliemos el ejemplo un poco proyectando sólo el nombre del cliente, en comparación con todo el objeto del cliente. Como mencioné anteriormente, la sintaxis ideal tendría la forma siguiente:

sequence locals =
customers.where(ZipCode == 98112).select(Name);

Con sólo las extensiones del lenguaje que hemos tratado, las expresiones lambda y los métodos de extensión, esto podría volver a escribirse así:

IEnumerable locals =
customers.Where(c => c.ZipCode == 91822).Select(c => c.Name);

Observe que el de tipo de retorno es diferente para esta consulta, IEnumerable en vez de IEnumerable. Esto sucede porque sólo devolvemos el nombre del cliente desde la declaración select.

Esto funciona realmente bien cuando la proyección sólo es un único campo. Sin embargo, supongamos que en lugar de sólo el nombre del cliente, también queremos devolver su dirección. La sintaxis ideal podría ser parecida a esta:

locals = customers.where(ZipCode == 98112).select(Name, Address);
Back to top

Tipos anónimos
Si tuviéramos que continuar usando nuestra sintaxis existente para devolver el nombre y la dirección, rápidamente nos encontraríamos con el problema de que no hay tipo que contenga sólo un nombre y una dirección. Sin embargo, podríamos seguir escribiendo esta consulta especificando ese tipo:

class CustomerTuple
{
public string Name;
public string Address;

public CustomerTuple(string name, string address)
{
this.Name = name;
this.Address = address;
}
}

A continuación, podríamos usar ese tipo, en este caso CustomerTuple, para generar el resultado de nuestra consulta:

IEnumerable locals =
customers.Where(c => c.ZipCode == 91822)
.Select(c => new CustomerTuple(c.Name, c.Address));

Esto seguro que parece mucho código modelo para proyectar un subconjunto de los campos. Además, con frecuencia no está muy claro cómo denominar a un tipo como ese. ¿Es CustomerTuple realmente un nombre adecuado? ¿Qué ocurriría si tuviéramos que proyectar el nombre y la edad? Eso también podría ser un CustomerTuple. Así, los problemas son que tenemos código modelo y no parece que haya ningún nombre adecuado para los tipos que creamos. Además, también podrían haber muchos tipos diferentes necesarios y su administración podría convertirse rápidamente en un dolor de cabeza.

Esto es exactamente la razón por la cual existen los tipos anónimos. Básicamente, esta característica permite la creación de tipos estructurales sin especificar el nombre. Si volvemos a escribir la consulta anterior usando tipos anónimos, tendrá el aspecto siguiente:

locals = customers.Where(c => c.ZipCode == 91822)
.Select(c => new { c.Name, c.Address });

Este código crea implícitamente un tipo que tiene los campos Name y Address:

class
{
public string Name;
public string Address;
}

A este tipo no se puede hacer referencia por el nombre, ya que no tiene ninguno. Los nombres de los campos pueden declararse explícitamente en la creación del tipo anónimo. Por ejemplo, si el campo que se está creando se deriva de una expresión complicada o el nombre simplemente no es el deseable, es posible cambiarlo:

locals = customers.Where(c => c.ZipCode == 91822)
.Select(c => new { FullName = c.FirstName + “ “ + c.LastName,
HomeAddress = c.Address });

En este caso, el tipo que se genera tiene campos llamados FullName y HomeAddress.

Esto nos acerca más al ideal, pero existe un problema. Observará que omití estratégicamente el tipo de locales en cualquier lugar donde usé un tipo anónimo. Evidentemente, no podemos indicar el nombre de tipos anónimos, así que, ¿cómo los usamos?

Back to top

Variables locales con establecimiento implícito de tipos
Hay otra característica del lenguaje conocida como variables (o var para abreviar) locales con establecimiento implícito de tipos que indica al compilador que infiera el tipo de una variable local. Por ejemplo:

var integer = 1;

En este caso, el valor entero tiene el tipo int. Es importante comprender que el tipo sigue establecido estrictamente. En un lenguaje dinámico, el tipo del entero podría cambiar más adelante. Para ilustrar esto, el código siguiente no se puede compilar:

var integer = 1;
integer = “hello”;

El compilador de C# informará de un error en la segunda línea, indicando que no puede convertir implícitamente una cadena a un int.

En el caso de la consulta anterior, ahora podemos escribir la asignación completa tal como se muestra aquí:

var locals =
customers
.Where(c => c.ZipCode == 91822)
.Select(c => new { FullName = c.FirstName + “ “ + c.LastName,
HomeAddress = c.Address });

El tipo de locales termina siendo IEnumerable, donde “?” es el nombre de un tipo que no puede escribirse (ya que es anónimo).

Los locales con establecimiento implícito de tipos son justamente eso: un local dentro de un método. No les es posible escapar de los límites de un método, propiedad, indizador, ni de otros bloques porque el tipo no se puede indicar explícitamente y “var” no es legal en campos o tipos de parámetro.

Los locales con establecimiento implícito de tipos resultan ser útiles fuera del contexto de una consulta. Por ejemplo, ayudan a simplificar instancias genéricas complicadas:

var customerListLookup = new Dictionary>();

Ahora estamos en una buena situación con nuestra consulta; estamos cerca de la sintaxis ideal y llegamos aquí con características del lenguaje de uso general.

Curiosamente, observamos que a medida que un mayor número de personas trabajaba con esta sintaxis, a menudo había necesidad de permitir que una proyección escapara de los límites de un método. Como vimos anteriormente, esto es posible si se construye un objeto llamando a su constructor desde dentro de Select. Sin embargo, ¿qué sucede si no hay constructor que tome exactamente los valores que se necesitan establecer?

Back to top

Inicializadores de objetos
Para este caso, existe una característica del lenguaje C# en la versión próxima de “Orcas” conocida como inicializadores de objetos. Básicamente, los inicializadores de objetos permiten la asignación de múltiples propiedades o campos en una única expresión. Por ejemplo, un patrón común para la creación de objetos es:

Customer customer = new Customer();
customer.Name = “Roger”;
customer.Address = “1 Wilco Way”;

En este caso, no hay constructor de Customer que tome un nombre y una dirección; sin embargo, hay dos propiedades, Name y Address, que se pueden establecer cuando se cree una instancia. Los inicializadores de objetos permiten la misma creación con la sintaxis siguiente:

Customer customer = new Customer()
{ Name = “Roger”, Address = “1 Wilco Way” };

En nuestro ejemplo anterior de CustomerTuple, creamos la clase CustomerTuple llamando a su constructor. Podemos lograr el mismo resultado con los inicializadores de objetos:

var locals =
customers
.Where(c => c.ZipCode == 91822)
.Select(c =>
new CustomerTuple { Name = c.Name, Address = c.Address });

Observe que los inicializadores de objetos permiten omitir los paréntesis del constructor. Además, tanto los campos como las propiedades que se pueden definir se pueden asignar dentro del cuerpo del inicializador de objetos.

Ahora tenemos una sintaxis sucinta para crear consultas en C#. Sin embargo, también tenemos una manera ampliable de agregar operadores nuevos (Distinct, OrderBy, Sum, etcétera) a través de métodos de extensión y de un conjunto claro de características del lenguaje que son útiles en sí mismas.

El equipo de diseño del lenguaje ahora tenía varios prototipos de los que obtener comentarios y opiniones. De modo que organizamos un estudio de facilidad de uso con muchos participantes que tenían experiencia con C# y con SQL. Los comentarios recibidos fueron casi totalmente positivos, pero estaba claro que todavía faltaba algo. En particular, resultaba difícil para los desarrolladores aplicar sus conocimientos de SQL porque la sintaxis que considerábamos como ideal no se correspondía muy bien con su experiencia del dominio.

Back to top

Expresiones de consulta
Entonces, el equipo de diseño del lenguaje diseñó una sintaxis que era más cercana al SQL, conocida como expresiones de consulta. Por ejemplo, una expresión de consulta para nuestro ejemplo tendría el aspecto siguiente:

var locals = from c in customers
where c.ZipCode == 91822
select new { FullName = c.FirstName + “ “ +
c.LastName, HomeAddress = c.Address };

Las expresiones de consulta se generan usando las características del lenguaje descritas anteriormente. Se traducen sintácticamente de forma literal a la sintaxis subyacente que ya hemos visto. Por ejemplo, la consulta anterior se traduce directamente por:

var locals =
customers
.Where(c => c.ZipCode == 91822)
.Select(c => new { FullName = c.FirstName + “ “ + c.LastName,
HomeAddress = c.Address });

Las expresiones de consulta admiten varias “cláusulas” diferentes, tales como from, where, select, orderby, group by, let y join. Estas cláusulas se traducen en las llamadas de operador equivalentes que, a su vez, se implementan mediante métodos de extensión. La estrecha relación de las cláusulas de consulta y los métodos de extensión que implementan los operadores facilitan su combinación si la sintaxis de consulta no admite una cláusula para un operador necesario. Por ejemplo:

var locals = (from c in customers
where c.ZipCode == 91822
select new { FullName = c.FirstName + “ “ +
c.LastName, HomeAddress = c.Address})
.Count();

En este caso, la consulta ahora devuelve el número de clientes que residen en el área del código postal 91822.

Y con eso, hemos logrado terminar casi en el punto donde comenzamos (lo que yo siempre considero bastante satisfactorio). La sintaxis de la siguiente versión de C# evolucionó durante los últimos años con varias características nuevas del lenguaje para finalmente llegar muy cerca de la sintaxis original propuesta en el invierno de 2004. La incorporación de expresiones de consulta amplía los fundamentos que ofrecen las otras características del lenguaje en la versión próxima de C# y hace que muchas situaciones de consulta sean más fáciles de leer y comprender para los desarrolladores con conocimientos de SQL.

Por Anson Horton – MSDN Magazine