Inicio > .NET Framework, C#, Tutorial > Pininos con LINQ – 2. Métodos de extensión

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.

Anuncios
Categorías:.NET Framework, C#, Tutorial Etiquetas: ,
  1. gabaldrina
    abril 25, 2010 en 8:35 pm

    Hola! me encanta tu blog!””” soy tu fan, besitos!

  2. Luis
    enero 29, 2012 en 10:19 am

    Muy interesante tu blog. Gracias entendi que eran metodos de extension.

  1. mayo 3, 2010 en 11:13 pm

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s