Archivo

Posts Tagged ‘LINQ’

101 Ejemplos de LINQ en MSDN


Pues el día de hoy me topé… no no no, mejor dicho… el día de hoy JTorrecilla, en los foros de MSDN, respondió una pregunta sobre ejemplos en LINQ y puso un enlace muy bueno a MSDN con muchos ejemplos sobre LINQ. Me parece justo reproducirlo aquí, por si a alguien le sirve.

MSDN – 101 LINQ Samples

Das ist alles für den Moment.

Categorías: .NET Framework, C#, Noticias Etiquetas:

Pininos con LINQ – 3. Agrupaciones


En la primera entrega de este tutorial vimos cómo podemos realizar consultas básicas con LINQ. En la segunda entrega vimos lo que hay tras bambalinas y los métodos que son llamados en realidad. En ambas entregas vimos cómo realizar consultas, filtrarlas, ordenarlas y transformar los resultados.

Sin embargo, hasta el momento he dejado de lado dos temas importantes: las agrupaciones y las uniones. Hice esto porque ambos temas son lo suficientemente extensos como para tener su propia entrega, además de que ninguna de las dos son necesarias para comenzar a utilizar LINQ. Pero creo que ya es tiempo de tratarlas, así que esta entrega está dedicada a las agrupaciones, mientras que la siguiente entrega lo estará a las uniones. Comencemos pues.

Cuando realizamos alguna consulta en LINQ, es posible que queramos agrupar aquellos datos que compartan alguna característica en común. Por ejemplo, supongamos que tenemos un array de cadenas de texto en las cuales guardamos nombres. Por ejemplo, quizás queramos agruparlas por la letra con la que comienzan. Es decir, generar una salida con algo así:

C
- Catalinga
- Claudia
F
- Fernando
- Francisco
J
- Joan
L
- Lorena
M
- Miguel
- Moisés
- Minerva

¿Por qué haríamos algo así? Usualmente, por comodidad. Aunque a un sistema no le importa cómo se agrupen los datos, nuestros usuarios sí que pueden querer visualizarlos de esta forma para identificar algún elemento de forma rápida. Por otra parte, suele ocurrir que queramos operar sobre solo cierto grupo de datos en una colección.

Pues bien, LINQ soporta agrupaciones de forma similar a como las soporta SQL: con una cláusula “group by”.

Lo primero que debemos saber es que cuando hacemos una agrupación, en lugar de recibir un IEnumerable<T>, donde T es el tipo  de dato que regresamos en nuestra cláusula “select”, recibimos un objeto IEnumerable<IGrouping<K, T>>, donde K es el tipo de dato sobre el cual queremos agrupar nuestros datos. En nuestro ejemplo anterior, la consulta regresaría un IEnumerable<IGrouping<char, string>> porque operamos sobre nombres (cadenas de texto) y agrupamos por el primer caracter del nombre, es decir, un char.

La interfaz IGrouping<K, T> hereda de IEnumerable<T> y añade una propiedad: Key, de tipo K. Así pues, Key define el elemento por el cual agrupamos (en nuestro ejemplo, sería el primer caracter de un nombre) mientras que al iterar sobre este objeto (recordemos que hereda de IEnumerable<T>) obtenemos los elementos que coincidieron con el criterio de agrupación (en nuestro ejemplo, los nombres que comienzan con el mismo caracter que Key). Así que ahora tendríamos que hacer dos bucles “foreach”: uno para cada agrupación y otro anidado para los elementos que contenga dicha agrupación.

Pero basta de teoría y veamos el código de nuestro ejemplo.

string str = 
  "Fernando,Catalina,Miguel,Joan,Moisés,Claudia,Francisco,Minerva,Lorena";
string[] names = str.Split(',');

IEnumerable<IGrouping<char, string>> q = from name in names
    orderby name[0] ascending
    group name by name[0];

foreach (IGrouping<char, string> nameGroup in q)
{
  Console.WriteLine(nameGroup.Key);
  foreach (string name in nameGroup)
    Console.WriteLine("- {0}", name);
}

Puedes haber notado algunas diferencias. En primer lugar, la cláusula “select” ha sido reemplazada por “group by”. Esta se forma de la siguiente manera: “group [valor] by [criterio]”. Aquí, [valor] es lo mismo que pondrías en el “select”, siendo nuestro ejemplo la cadena de texto con el nombre; mientras que [criterio] es un valor sobre el cual se realizará la agrupación: en nuestro ejemplo, sería el primer caracter que tenga el nombre. Dado que de alguna manera la cláusula “group by” reemplaza a “select”, ésta tiene que escribirse al final de la consulta (es decir, las cláusulas orderby y where van antes que group by).

Por supuesto, las agrupaciones pueden ser de cualquier tipo de dato y no necesariamente estar relacionados con el tipo de dato original. El siguiente ejemplo agrupa aquellos nombres que tienen más de ocho caracteres (es decir, el valor retornado es IEnumerable<IGrouping<bool, string>>).

string str = 
  "Fernando,Catalina,Miguel,Joan,Moisés,Claudia,Francisco,Minerva,Lorena";
string[] names = str.Split(',');

IEnumerable<IGrouping<bool, string>> q = from name in names
                     orderby name[0] ascending
                     group name by name.Length >= 8;

foreach (IGrouping<bool, string> nameGroup in q)
{
  Console.WriteLine(nameGroup.Key);
  foreach (string name in nameGroup)
    Console.WriteLine("- {0}", name);
}
/* Resultado: 
True
- Catalina
- Fernando
- Francisco
False
- Claudia
- Joan
- Lorena
- Miguel
- Moisés
- Minerva
*/

Dado que escribir IEnumerable<IGrouping<bool, string>> es un tanto engorroso, en adelante haremos uso de “var” para ahorrarnos un poco de escritura.

Podemos también agrupar por valores numéricos:

...
var q = from name in names
  orderby name.Length ascending, name ascending
  group name by name.Length;
...
/* Resultado: 
4
- Joan
6
- Lorena
- Miguel
- Moisés
7
- Claudia
- Minerva
8
- Catalina
- Fernando
9
- Francisco
*/

Nota que además hicimos un doble criterio de ordenamiento: primer ordenar por la longitud de la cadena, y luego ordenamos por la cadena misma.

De igual forma, podemos agrupar por tipos de datos que hayamos creado y también podemos valernos de la oportunidad de crear tipos de datos anónimos. En el siguiente ejemplo regresamos a nuestra querida clase Employee que desarrollamos en las pasadas entregas.

using System;
using System.Linq;

namespace Fermasmas.Wordpress.Com
{
  enum Gender { Male, Female, Unknown }

  class Employee : IComparable
  {
    public string Name;
    public int Age;
    public float Income;
    public Gender Gender;

    public Employee()
      : this(string.Empty, 0, 0F, Gender.Unknown)
    {
    }

    public Employee(string name, int age, float income, Gender gender)
    {
      Name = name;
      Age = age;
      Income = income;
      Gender = gender;
    }

    public int CompareTo(object obj)
    {
      if (obj == null)
        throw new ArgumentNullException("obj");
      if (!(obj is Employee))
        throw new ArgumentException("obj debe ser de tipo Employee", "obj");

      Employee emp = obj as Employee;
      return Name.CompareTo(emp.Name);
    }

    public override string ToString()
    {
      return Name;
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      Employee[] employees = new Employee[]
      {
        new Employee("Fernando Gómez", 27, 20000F, Gender.Male),
        new Employee("Katia De Montanaro", 29, 30000F, Gender.Female),
        new Employee("Moisés Pedraza", 30, 25000F, Gender.Male),
        new Employee("Joan Hernández", 25, 20000F, Gender.Male),
        new Employee("Claudia Estrella", 27, 15000F, Gender.Female)
      };

      var q = from employee in employees
          orderby employee.Name ascending
          group employee by employee.Gender;

      foreach (var nameGroup in q)
      {
        Console.WriteLine(nameGroup.Key);
        foreach (var name in nameGroup)
          Console.WriteLine("- {0}", name);
      }

      Console.ReadKey(true);
    }
  }
}

Al correr este ejemplo, tenemos el siguiente resultado.

Female
- Claudia Estrella
- Katia De Montanaro
Male
- Fernando Gómez
- Joan Hernández
- Moisés Pedraza

Como puedes ver, las agrupaciones son una forma útil de presentar nuestros datos. Un ejemplo práctico (que por cierto, desarrollaremos al final de este tutorial) puede ser una vista en Windows Forms donde tengamos un ListView donde podamos fácilmente agrupar y ordenar lo que se presenta, sin necesidad de realizar nuevas consultas a la base de datos (digo, si ya tenemos nuestro DataSet).

Bien, ya para finalizar… en la entrega anterior mostramos que los operadores estándares de consulta en realidad el compilador los transforma en llamadas a métodos de extensión. Pues bien, la cláusula group…by no es excepción. Ésta se tranforma en una llamada al método GroupBy que toma como parámetro una expresión lambda que nos devuelve el valor sobre el cual queremos agrupar. Así, el primer ejemplo que mostramos podría reescribirse como:

string str =
  "Fernando,Catalina,Miguel,Joan,Moisés,Claudia,Francisco,Minerva,Lorena";
string[] names = str.Split(',');

IEnumerable<IGrouping<char, string>> q = names.OrderBy(x => x[0])
                        .GroupBy(x => x[0]);
  
foreach (IGrouping<char, string> nameGroup in q)
{
  Console.WriteLine(nameGroup.Key);
  foreach (string name in nameGroup)
    Console.WriteLine("- {0}", name);
}

y obtendríamos el mismo resultado:

C
- Catalinga
- Claudia
F
- Fernando
- Francisco
J
- Joan
L
- Lorena
M
- Miguel
- Moisés
- Minerva

mientras que el ejemplo con nuestra clase Employee lo podemos traducir como:

static void Main(string[] args)
{
  Employee[] employees = new Employee[]
  {
	new Employee("Fernando Gómez", 27, 20000F, Gender.Male),
	new Employee("Katia De Montanaro", 29, 30000F, Gender.Female),
	new Employee("Moisés Pedraza", 30, 25000F, Gender.Male),
	new Employee("Joan Hernández", 25, 20000F, Gender.Male),
	new Employee("Claudia Estrella", 27, 15000F, Gender.Female)
  };

  var q = employees.OrderBy(x => x.Name)
		   .GroupBy(x => x.Gender);

  foreach (var nameGroup in q)
  {
	Console.WriteLine(nameGroup.Key);
	foreach (var name in nameGroup)
	  Console.WriteLine("- {0}", name);
  }

  Console.ReadKey(true);
}

y evidentemente, obtendríamos el mismo resultado que se mostró anteriormente.

Bueno, eso es todo por el momento. En la siguiente entrega veremos como hacer uniones, un tema bastante amplio por sí mismo. Pero mientras, espero que te diviertas con las agrupaciones.

Categorías: .NET Framework, C#, Tutorial Etiquetas: ,

Pininos con LINQ – 2. Métodos de extensión


En la primera entrega de este tutorial: Pininos con LINQ, explicamos los conceptos básicos que hay tras esta tecnología. Para este momento ya sabemos cómo hacer consultas utilizando los operadores estándares de consulta.

En dicha entrada también mencioné que LINQ es azúcar sintáctica, y que en realidad los operadores de consulta eran interpretados por el compilador para producir llamadas a métodos de objetos que el CLR pueda entender. Pues bien, en esta entrada retomaremos lo expuesto en la primera entrega y lo explicaré a la luz de dichos objetos y métodos. Sigue leyendo.

En primera instancia, dijimos (y probamos) que LINQ actúa sobre cualquier colección de datos. Más preciso: actúa sobre cualquier enumeración. Es decir, sobre cualquier objeto que implemente System.Collections.Generic.IEnumerable<T>. Pero ¿cómo es esto posible? Gracias a una característica de C# 3.0 llamada métodos de extensión (y que casi puedo asegurar que la incorporaron para poder meter LINQ). En fin, en resumen: los operadores estándares se traducen a llamadas a métodos de extensión (ubicados dentro del espacio de nombres System.Linq) que operan sobre IEnumerable<T>. Veamos.

El primer método que llama nuestra atención es el IEnumerable<T>.Where. Éste método toma como parámetro un delegado de tipo Func<T, bool>, donde T es el tipo de dato de nuestra enumeracion. El delegado pide un método que tome un parámetro de tipo T y regrese un valor verdadero (si T debe seleccionarse) o falso (si T no debe hacerlo). Como vemos, es similar a la expresión “where”.

Para ilustrar lo anterior, recordemos un ejemplo sencillo de la primera entrega, que reproduzco a continuación.

int[] nums = new int[] { 1, 2, 3, 4, 5 };

IEnumerable<int> query =
    from num in nums
    where num % 2 == 0
    select num;
foreach (int num in nums)
    Console.Write("{0} ", num);

El ejemplo anterior devuelve solo los números pares, como lo especifica la expresión where. Por ende, lo podemos traducir con el método Where de la siguiente forma.

bool FilterFunc(int num)
{
    return num % 2 == 0;
}

void Test()
{
    int[] nums = new int[] { 1, 2, 3, 4, 5 };
    IEnumerable<int> query =
        nums.Where(new Func<int, bool>(FilterFunc));
    foreach (int num in nums)
        Console.Write("{0} ", num);
}

Mucho más largo, ¿verdad? Bueno, por eso los operadores de consulta son azúcar sintáctica… El punto es que utilizamos un delegado que sigue la firma adecuada que pide el método Where (en este caso, Func<int, bool> que se traduce en una función que tome un parámetro int y regrese un valor verdadero/falso).

Por supuesto, podemos hacer uso de las expresiones lambdas, que también son nuevas a C# 3.0. Así, lo anterior quedaría reducido a:

int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<int> query =
    nums.Where( num => num % 2 == 0);
foreach (int num in nums)
    Console.Write("{0} ", num);

Esta forma es más corta y cómoda, y es la que usaremos en el resto del apunte. Pero quise mostrar la anterior para que veas de dónde sale la expresión lambda (no he tenido tiempo de preparar algún artículo sobre expresiones lambda todavía, pero éstas son sencillas de entender; para mayor información consulta este artículo de MSDN).

Naturalmente, el siguiente paso es ver cómo podemos transformar el resultado. Sin mucha sorpresa, el método equivalente a la expresión “select” se llama… Select. Bueno, pues era obvio, ¿no? En fin, que Select toma como parámetro un delegado de tipo Func, pero ahora de la forma Func<T, TResult>, donde T es el tipo de dato de la colección original, mientras que TResult es el tipo de dato que va a resultar de la transformación. Por ejemplo, si queremos transformar los resultados a una cadena de texto, nuestro delegado sería de la forma Func<int, string>, y devolvería un IEnumerable<string>. Ejemplo:

string SelectFunc(int num)
{
    return "texto: " + num;
}

void Test()
{
    int[] nums = new int[] { 1, 2, 3, 4, 5 };
    IEnumerable<string> query =
        nums.Select(new Func<int, string>(SelectFunc));
    foreach (string num in nums)
        Console.WriteLine(num);
}

 

O bien, traducido a expresión lambda:

int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<string> query =
    nums.Select( num => "texto: " + num);
foreach (string num in nums)
    Console.Write("{0} ", num);

Ahora bien, ¿qué pasa si quisiéramos juntar el Where con el Select? Por ejemplo, para seleccionar todos los números pares y regresar su texto correspondiente, tendríamos que ejecutar el Where y sobre el resultado de éste llamar al Select –a final de cuentas Where regresa un IEnumerable.

bool FilterFunc(int num)
{
  return num % 2 == 0;
}

string SelectFunc(int num)
{
  return "texto: " + num;
}

void Test()
{
  int[] nums = new int[] { 1, 2, 3, 4, 5 };

  IEnumerable<int> q1 = nums.Where(num => num % 2 == 0);
  IEnumerable<string> q2 = q1.Select(num => "texto: " + num);
  foreach (string num in q2)
    Console.WriteLine(num);
}

O bien, para hacer la expresión más corta, simplemente los encadenamos en una sola sentencia:

IEnumerable<string> query = nums
                .Where(num => num % 2 == 0)
                .Select(num => "texto: " + num);
foreach (string num in query)
  Console.WriteLine(num);

Por supuesto, todo lo que hablamos en la entrega anterior aplica en este caso (al final, el compilador traduce a este mismo código). Por ejemplo, ¿recuerdas cuando dijimos que la consulta sobre los datos se ejecuta hasta que se itera sobre la enumeración? Pues obvio esto también funciona aquí. Este es el ejemplo de la excepción dividida entre cero, utilizando métodos de extensión de LINQ.

int zero = 0;
int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<int> query = nums
    .Select(num => num / zero);
foreach (int num in query)
    Console.WriteLine(num);

La excepción DivideByZeroException se lanzará hasta que se ejecute el primer foreach.

Adicionalmente, también aplica todo lo dicho sobre los tipos de dato anónimos. El siguiente código muestra una consulta que devuelve un tipo anónimo. ¿Recuerdas la clase Employee que utilizamos? Pues está de vuelta. De hecho vamos a traducir el mismo ejemplo que usé la vez pasada. No muestro el código de Employee por razones de espacio (y flojerita). Por cierto, nota el uso del identificador var.

List<Employee> emps = new List<Employee>()
{
  new Employee("Fernando Gómez", 27, 20000F, Gender.Male),
  new Employee("Katia De Montanaro", 29, 30000F, Gender.Female),
  new Employee("Moisés Pedraza", 30, 25000F, Gender.Male),
  new Employee("Joan Hernández", 25, 20000F, Gender.Male),
  new Employee("Claudia Estrella", 27, 15000F, Gender.Female)
};

var query = emps
  .Where(emp => emp.Gender == Gender.Female)
  .Select(emp => new { Data = emp.Name + " - " + emp.Gender });
foreach (var result in query)
  Console.WriteLine(result.Data);

Analiza detenidamente el código anterior, de nueva cuenta. ¿Ya lo notaste? El código hace lo que promete, pero resalta un hecho de utilizar tipos anónimos: éstos solo pueden ser empleados en una función lambda. En efecto, dada su naturaleza es imposible saber cuál es el nombre que el compilador le asigna. Luego, no podemos referenciarlos y por ende no podríamos crear nuestro delegado ya que no sabríamos especificar el parámetro de retorno del método invocado por éste. En otras palabras, ¿cómo escribirías el delegado: Func<T, *>, ¿qué pondrías en lugar del asterisco? No can do.  Sugerencia: utiliza siempre lambdas.

Hasta el momento hemos visto como el trasladar las expresiones select y where a sus métodos equivalentes Select y Where ha sido muy directo. Es decir, solo cambia un poquito la forma de escribir, pero al final creo que no hay mucha diferencia entre operadores y métodos de consultas. Sin embargo esto es un poco diferente con el ordenamiento.

De entrada, para ordenar de alguna forma en particular se utilizan dos métodos: OrderBy y OrderByDescending, respectivamente. Éstos reciben un delegado, el cual regresa la variable sobre la que se va a ordenar (y el tipo de dato de ésta debe implementar IComparable). Por ejemplo:

var query = emps
  .Where(emp => emp.Gender == Gender.Female)
  .OrderBy(emp => emp.Income)
  .Select(emp => new { Data = emp.Name + " - " + emp.Gender });
foreach (var result in query)
  Console.WriteLine(result.Data);

ordenaría el resultado de forma ascendente (de menor ingreso al mayor), y:

var query = emps
  .Where(emp => emp.Gender == Gender.Female)
  .OrderByDescending(emp => emp.Income)
  .Select(emp => new { Data = emp.Name + " - " + emp.Gender });
foreach (var result in query)
  Console.WriteLine(result.Data);

lo haría en forma descendente (mayor ingreso al menor). Nota que ambos métodos regresan un IOrderedEnumerable en lugar de un simple IEnumerable.

Bueno, ¿y qué pasa si queremos aplicar dos o más criterios de ordenamiento? Es decir, consideremos que queremos ordenar nuestros empleados por género ascendente y luego por nombre descendente. Usando los operadores de consulta, haríamos:

var query = from emp in emps
            orderby emp.Gender ascending, emp.Name descending
            select new { Data = emp.Name + " - " + emp.Gender };

Para hacerlo usando los métodos de extensión, usamos OrderBy u OrderByDescending con el primer criterio de ordenamiento, pero con el segundo (y subsiguientes) tenemos que emplear los métodos ThenBy y ThenByDescending, respectivamente. De esta forma:

var query = emps
        .OrderBy(emp => emp.Gender)
        .ThenByDescending(emp => emp.Name)
        .Select(emp => new { Data = emp.Name + " - " + emp.Gender });

Esto es así porque tanto OrderBy como OrderByDescending toman un IEnumerable, que en principio está desordenado. Pero para añadir un segundo criterio de ordenamiento se tiene que asumir que la enumeración ya ha sido previamente ordenada: de no hacerlo corremos el riesgo de “desordenar” nuestra enumeración. Por eso es que se necesitan los métodos ThenBy y ThenByDescending, que toman como parámetro un IOrderedEnumerable (el devuelto por OrderBy y OrderByDescending). Por supuesto, para añadir un tercer criterio de ordenamiento utilizamos ThenBy y ThenByDescending respectivamente, y así sucesivamente.

Y así llegamos al final de esta entrada. Al igual que en la anterior, dejamos en suspenso lo relativo a uniones de datos y agrupaciones (groupby y join, respectivamente), pero esto es así porque en una entrada posterior explicaremos cómo se utilizan.

Así pues, ahora sí queda claro cómo es que LINQ no es nada del otro mundo: puros métodos de extensión; y se reafirma lo que comentaba acerca de que LINQ es azúcar sintáctica. De qué forma realizar la consulta: utilizando los operadores estándares de consulta, o bien utilizando los métodos de extensión, eso es algo que depende del gusto de cada quién. Utilizar los operadores de consulta es una forma elegante que mejora la legibilidad del código. Sin embargo, los métodos de extensión son más claros con respecto a qué es lo que se está realizando, desde la perspectiva del CLR. Ahora sí que cada quién. En lo personal, me quedo con los operadores de consulta, siempre que sea posible y no tenga que realizar acciones demasiado complicadas, en cuyo caso un lambda sería menos engorroso.

En fin, lo importante es que ya sabes qué ocurre tras bambalinas. No te pierdas la siguiente entrega de este tutorial: Pininos con LINQ.

Categorías: .NET Framework, C#, Tutorial Etiquetas: ,

Pininos con LINQ – 1. Conceptos básicos


LINQ, acrónimo para Language Integrated Query, es el nombre que se le da a ciertas palabras reservadas, métodos y expresiones que contiene el lenguaje C# (versión 3 en adelante), y que permiten realizar consultas, muy similares a SQL, sobre diversas fuentes de datos: bases de datos, archivos XML, e incluso simples colecciones de objetos.

Por ejemplo, en SQL, uno hace una sentencia donde pide qué campos se traen de una o varias tablas, se especifican criterios de búsqueda y finalmente se determina la forma en que los resultados son ordenados. LINQ intenta emular este comportamiento, pero sin limitarse a una fuente de datos en particular.

Evidentemente, LINQ no es otra cosa que azúcar sintáctica. Esto es, al compilar traduce las expresiones LINQ en llamadas a ciertos métodos y regresan un resultado en particular (una colección IEnumerable<T>). De lo anterior se desprende que hay dos formas de usar LINQ: con las palabras reservadas de C#, o bien con llamadas a los métodos (de extensión, por cierto) que se definen en el espacio de nombres System.Linq.

Así pues comencemos con lo básico. En primer lugar, debes saber que LINQ actúa sobre cualquier objeto que implemente la interfaz IEnumerable<T>, donde T es un objeto cualquiera sobre el que queramos operar. Esto lo consigue a través de los métodos de extensión que se encuentran en el espacio de nombres System.Linq. Así, lo primero que hacemos al crear nuestra consulta es indicarle cual es la fuente de datos. Esto lo hacemos a través de la cláusula “from”. Consideremos el siguiente código.

int[] nums = new int[] { 1, 2, 3, 4, 5 };
from num in nums
...etc...

Sabemos que un array de enteros implementa IEnumerable<int>. De hecho, cualquier array de tipo T implementa IEnumerable<T>. Por lo que podemos utilizar LINQ. En la segunda línea vemos cómo decimos que se tomará el valor de la colección “nums”. El valor “num” es como una variable con la que podemos hacer referencia a cada objeto que pertenece a la colección (análogo a si hiciéramos “foreach (int num in nums)”).

En segundo lugar, para que la consulta esté completa, se necesita indicar qué valores interesa que se regresen (similar a cuando se hace un “select campo1, campo2 from tabla” en SQL). Si queremos que regrese el número tal cual, simplemente hacemos:

int[] nums = new int[] { 1, 2, 3, 4, 5 };
from num in nums
select num;

Y ahí termina nuestra cláusula, por lo que la terminamos con un punto y coma. Como en el select solo regresamos “num”, que es de tipo entero, nuestro valor de retorno de la consulta será un IEnumerable<int>. Por lo que para que la cláusula quede completa, tenemos que asignarla a alguna variable. Ahora sí:

int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<int> query = 
    from num in nums
    select num;

Por supuesto, en el select podemos hacer cualquier transformación que nos venga en gana. Por ejemplo, el siguiente código crea una consulta que regresa la representación en cadena de texto de cada número de la colección.

int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<string> query = 
    from num in nums
    select "texto: " + num.ToString();

Ahora bien, consideremos el siguiente ejemplo:

int zero = 0;
int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<int> query = 
    from num in nums
    select num / zero;

Éste código evidentemente divide cada número de la colección entre cero, lo cual –uno pensaría- nos debería lanzar un DivideByZeroException. Si ejecutamos este código así tal cual, y lo depuramos, vemos que cuando se asigna el valor a query no se lanza ninguna excepción, como cabría esperar. Esto es así debido a que cuando creamos la consulta LINQ, ésta no se ejecuta hasta que iteramos sobre los resultados, sobre query (por ejemplo, hasta que hacemos un foreach). En el siguiente código, obtenemos un DivideByZeroException en la línea 6, no antes.

int zero = 0;
int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<int> query = 
    from num in nums
    select num / zero;
foreach (int i in query)
    Console.WriteLine(i);

Hasta ahorita hemos visto como transformar datos dada una consulta. Un aspecto importante de ésta es el poder filtrar los datos que se deben obtener de la colección. Para ello, utilizamos una cláusula “where”, la cual debe ser seguida por una expresión que se evalúe a verdadero o falso. Para cada elemento de la colección, se evalúa dicha expresión, y si ésta es verdadera entonces se ejecuta el select para ese elemento. Si no, se descarta y se pasa al siguiente.

La cláusula where debe estar entre el from y el select. El siguiente ejemplo obtiene todos los números pares de la colección.

int[] nums = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<int> query = 
    from num in nums
    where num % 2 == 0
    select num;

Cuando se ejecuta la consulta, recorremos la colección. Para el primer elemento, se evalúa “1 % 2 == 0”, lo cual es falso, por lo que “1” no se incluye en la colección de regreso. Algo similar ocurre para 3 y 5. Pero 2 y 4 sí cumplen, por lo que “query” contendrá solo dos números: 2 y 4. En otras palabras, al ser compilada, la anterior consulta generaría un pseudo-código similar a este:

int[] nums = new in[] { 1, 2, 3, 4, 5 };
List<int> query = new List<int>();
foreach (int num in nums)
{
  if (num % 2 == 0)
    query.Add(num);
}

Más o menos esa es una aproximación al código que generaría el compilador.

También podemos incluir criterios de ordenamiento utilizando la cláusula orderby … ascending y orderby … descending para ordenar de forma ascendente o descendente, respectivamente. Para poder ordenar de esta forma, el tipo de dato del objeto seleccionado en el orderby debe implementar la interfaz IComparable (o IComparer, en su defecto). Si no se implementa IComparable, se utilizará el comparador por defecto que tenga asociado el tipo de dato en cuestión. Por supuesto, int sí implementa esta interfaz, por lo que podemos ahora ordenar nuestros números pares de forma descendente, por ejemplo.

int[] nums = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
IEnumerable<int> query = 
    from num in nums
    where num % 2 == 0
    orderby num descending
    select num;

Las consultas pueden ser más complejas incluso, realizando joins y groups, pero eso es más avanzado y será otro tema de la serie “Pininos en LINQ”.

Por supuesto, podemos hacer consultas sobre tipos de dato que nosotros hayamos definido, como clases y estructuras. El siguiente ejemplo es un programa que utiliza una clase, Employee, y genera una consulta que obtiene todos los objetos que representen a mujeres.

using System;
using System.Collections.Generic;
using System.Linq;

namespace Fermasmas.Wordpress.Com
{
  enum Gender { Male, Female, Unknown }

  class Employee : IComparable
  {
    public string Name;
    public int Age;
    public float Income;
    public Gender Gender;

    public Employee()
      : this(string.Empty, 0, 0F, Gender.Unknown)
    {
    }

    public Employee(string name, int age, float income, Gender gender)
    {
      Name = name;
      Age = age;
      Income = income;
      Gender = gender;
    }

    public int CompareTo(object obj)
    {
      if (obj == null)
        throw new ArgumentNullException("obj");
      if (!(obj is Employee))
        throw new ArgumentException("obj debe ser de tipo Employee", "obj");

      Employee emp = obj as Employee;
      return Name.CompareTo(emp.Name);
    }

    public override string ToString()
    {
      return Name;
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
        List<Employee> emps = new List<Employee>()
        {
          new Employee("Fernando Gómez", 27, 20000F, Gender.Male),
          new Employee("Katia De Montanaro", 29, 30000F, Gender.Female),
          new Employee("Moisés Pedraza", 30, 25000F, Gender.Male),
          new Employee("Joan Hernández", 25, 20000F, Gender.Male),
          new Employee("Claudia Estrella", 27, 15000F, Gender.Female)
        };

        IEnumerable<Employee> query =
        from emp in emps 
        where emp.Gender == Gender.Female
        orderby emp.Age ascending
        select emp;

        Print(query);

        Console.ReadKey(true);
    }

    static void Print<T>(IEnumerable<T> query)
    {
        foreach (T t in query)
            Console.WriteLine(t);
    }
  }
}

Al correr este ejemplo, obtenemos:

Claudia Estrella
Katia De Montanaro

Como podemos ver, LINQ es una forma muy buena y fácil no solo para realizar consultas sobre colecciones, sino que también para ordenar nuestros datos de forma rápida y elegante.

Pero aún hay más. Hasta el momento hemos utilizado la cláusula “select” para regresar los objetos completos (como un número entero o la clase Employee del ejemplo anterior). Sin embargo, ¿qué pasa si solo requiero un subconjunto del objeto? Por ejemplo, ¿qué pasa si solo me interesa obtener la propiedad Name del objeto Employee? Bueno, pues podemos combinar con LINQ un aspecto de C# conocido como tipos anónimos. Éstos no son muy útiles por sí solos, pero combinados con LINQ tenemos una forma elegante y potente no solo de realizar consultas, sino que además podemos hacerlas sobre propiedades específicas de un objeto.

Para llevar esto a cabo, basta con crear un tipo anónimo en la cláusula “select” e inicializarlo con el objeto parámetro (aquel que anteriormente simplemente devolvíamos). Sin embargo, dado que un tipo anónimo no puede ser referenciado por su nombre, no podemos crear una variable del tipo IEnumerable<T> que reciba la consulta, porque no sabemos el nombre de “T”. Pero podemos utilizar un tipo de dato variable para que el compilador se encargue de determinar el tipo de dato en base al tipo devuelto por la consulta, lo cual es perfectamente legal. Así pues, podemos modificar el ejemplo anterior para que nos devuelva solamente el nombre y el género. Esto lo haríamos de la siguiente forma.

var query =
  from emp in emps 
  where emp.Gender == Gender.Female
  orderby emp.Age ascending
  select new { Data = emp.Name + " - " + emp.Gender };
foreach (var result in query)
  Console.WriteLine(result.Data);

Como puedes observar, la línea 1 ha cambiado y ahora utilizamos “var” para que no haya problema con el tipo de dato que devuelve la consulta. Luego, en la línea 5 tenemos la cláusula “select” que regresa un tipo de dato anónimo con una sola propiedad: Data, que se forma de concatenar el nombre del empleado con su género. Y finalmente, en la línea 6 tenemos que usar “var” de nueva cuenta para iterar sobre la consulta, por lo mismo que en la línea 1.

Lo que hemos mostrado al momento es solo la superficie de LINQ, pero creo que es suficiente para mostrar su poder y sobre todo, la facilidad con la que ahora podemos buscar, filtrar y ordenar datos de una colección. Seguramente, si no conocías LINQ, te has dado cuenta del mundo de posibilidades que ésto abre: dado que LINQ opera sobre cualquier colección (i.e. cualquier objeto enumerable, es decir, que implemente IEnumerable<T>), es posible crear clases de lógica de negocio que obtengan datos de cualquier fuente (como bases de datos o un archivo XML) y que sean enumerables. En efecto, aplicaciones particulares de LINQ son LINQ to SQL, que permite utiliar LINQ para realizar consultas a bases de datos Microsoft SQL Server, LINQ to DataSet para realizar consultas sobre objetos System.Data.DataSet de ADO.NET, o LINQ to XML para realiar consultas sobre archivos XML. En apuntes subsiguientes iremos explicando esto y más, como parte de este tutorial.

Y ya para finalizar. En este apunte vimos los conceptos básicos de LINQ. Mencionaba yo al inicio que LINQ es “azúcar sintáctica”, y que el compilador traducía consultas a llamadas a métodos de extensión. En el siguiente apunte explicaré qué hay tras bambalinas, y qué métodos son los que llama LINQ. Más aún, veremos cómo utilizar las clases propias de LINQ (dentro de System.Linq) para realizar las mismas consultas, pero con llamadas a métodos de extensión en lugar de cláusulas from…select. No te lo pierdas.

Categorías: .NET Framework, C#, Tutorial Etiquetas: ,