martes, 16 de diciembre de 2014

Claúsula Where





La cláusula where, representa el elemento de filtrado dentro del mundo de LinQ, al igual que en el lenguaje SQL. Esta es una de las más usadas y de las más útiles de todas las que componen su librería de métodos extensores (System.Linq).


La parte más importante dentro de la cláusula Where, es el llamado ‘Predicate’, este es un Parámetro de tipo Func<TSource, bool> que viene a indicar la condición del filtrado. Os facilito la entrada del blog donde se explican en detalle los delegados anónimos Func y donde podéis ampliar información sobre estos tipos.






Recuerda que aquí tienes el indice de todos los posts del Curso de LinQ.


Predicate (predicado), es simplemente una expresión que toma un elemento del mismo tipo de la colección que estamos filtrando y devuelve un true o un false, según el criterio del mismo.


Para entenderlo mejor, vamos a ver cómo sería el método extensor Where. Sobra decir que esto no tiene nada que ver con el código utilizado por el equipo de Microsoft para crear el original.


public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    /// Recorremos la colección
    foreach (TSource s in source)
    {
        if (predicate(s)) /// Preguntamos si comple nuestro criterio de filtrado
            yield return s; /// Construimos el iterador
    }
 
} 


Como podemos observar, el código es realmente sencillo, simplemente recorremos la colección (leída implícitamente mediante la inferencia de tipos desde IEnumerable<T> sobre el que se aplica el método. Más info aquí sobre los métodos extensores y aquí de la inferencia de tipos) y evaluamos cada uno de sus elementos para construir un iterador con cada una de las piezas que cumplan el criterio solicitado.

 Añadimos un ejemplo de uso muy sencillito:


static void Main(string[] args)
{
    string[] nombres = { "Luis", "Pablo", "Susana", "María" };
 
    var nombresFiltrados = from n in nombres
                            where n.EndsWith("a")
                            select n;
 
    var nombresFiltradosLambda = nombres.Where(n => n.EndsWith("a"));
 
    nombresFiltrados.ToConsole("PINTANDO EL RESULTADO DE LA EXPRESIÓN DE CONSULTA : ");
 
    nombresFiltrados.ToConsole("PINTANDO EL RESULTADO DE LA EXPRESIÓN LAMBDA : ");
 
    Console.Read();
}


Como veis, estoy haciendo uso de un nuevo método extensor para no repetir código, ToConsole:


public static void ToConsole<TSource>(this IEnumerable<TSource> source, string titulo = null)
{
    if ( ! string.IsNullOrEmpty(titulo))
        Console.WriteLine(titulo);
 
    foreach (TSource s in source)
    {
        string datoAPintar = s == null ? string.Empty : s.ToString();
        Console.WriteLine(datoAPintar);
    }
 
    Console.WriteLine();
}


Resultado del ejemplo:























Usando un método externo de evaluación

Una de las ventajas que nos ofrece el uso de las clases de LinQ en general, es que podemos utilizar el 100% de nuestro código y del código disponible dentro del CLR. Cuando indicamos un delegado Func<TSource, bool> no debemos ceñirnos únicamente a indicar condiciones ‘en línea’, podemos llamar a funciones (tanto nuestras como del Framework) o a la combinación de ambas. Esto quiere decir que podemos utilizar cualquier tipo de función que reciba como parámetro un tipo T, no tiene por qué ser un método genérico, puede estar limitado para un solo tipo de datos, significando que solo nos serviría para utilizar como condición del método Where con colecciones (IEnumerable) de ese tipo y no para la totalidad. Por supuesto el método tendrá que devolver un bool.


Un método genérico válido sería el siguiente:


public static bool IsNumeric<T>(T objeto)
{
    string datoToString = objeto == null ? string.Empty : objeto.ToString();
 
    double obj;
 
    return double.TryParse(datoToString, out obj);
}


Y así podríamos consumirlo, considerando el ejemplo anterior:


var nombresFiltrados = from n in nombres
                       where IsNumeric(n)
                       select n;


Este ejemplo lo podríamos utilizar con cualquier tipo de colección, pero si nos limitamos al ejemplo anterior también podríamos utilizar un método que recibiera un objeto de tipo string, ya que nuestra colección es de este tipo de datos, aunque evidentemente solo nos valdría para IEnumerables<string> o tipos derivados:


public static bool TieneLas5Vocales(string cadena)
{
    return   ( cadena.Contains("a") 
            && cadena.Contains("e")
            && cadena.Contains("i")
            && cadena.Contains("o")
            && cadena.Contains("u") );
}

Y así lo consumiríamos:


var nombresFiltrados = from n in nombres
                       where TieneLas5Vocales(n)
                       select n;


Podríamos utilizar un mix de todas sin problemas:

var nombresFiltrados = from n in nombres
                        where TieneLas5Vocales(n)
                        &&    IsNumeric(n)
                        &&    n.EndsWith("a")
                        select n;




Filtrando por índice de posición

Normalmente no hay ninguna diferencia en cuanto a posibilidades si optamos por realizar una consulta de LinQ mediante el uso de los operadores de consulta o las expresiones lambda, pero en esta ocasión, tenemos un caso que no cubre la opción del azúcar sintáctico y que está reservada solo para el uso de la las lambdas y del índice de posición.

El índice de posición consiste en el añadido de un parámetro int adicional para nuestro delegado genérico o Predicate, quedando así Func<T, int, bool>. Traducido, quiere decir que almacenará un método con 2 parámetros, uno de tipo T referente al objeto que está recorriendo y otro de tipo int referente al índice o posición de este objeto, siendo obligatorio que la función devuelva un true o un false.

Esta sería la firma del método extensor Where con índice de posición:


public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, int, bool> predicate)
{
    /// ... ...
}


Esto sería útil, para por ejemplo según nuestro primer ejemplo pintar los nombres que se encuentran en una posición par:


static void Main(string[] args)
{
    string[] nombres = { "Luis", "Pablo", "Susana", "María" };
 
    var nombresFiltrados = nombres.Where( (n, i) => i % 2 == 0).ToList();
 
    nombresFiltrados.ToConsole("PINTANDO LOS NOMBRES DE LA POSICIÓN 'PAR' : ");
 
    Console.Read();
}

Resultado:





















En este caso para formar la lambda ( n, i ) => , al tener más de un parámetro hemos tenido que añadir los paréntesis, ya que en este caso son obligatorios. Par más información os dejo la entrada donde explico las lambdas.




A tener en cuenta

Es importante recalcar, que cuando utilizamos el método extensor Where, el resultado siempre va a ser un conjunto de datos (IEnumerable<T>), por mucho que nosotros como programadores sepamos que el resultado de la consulta va a ser un único elemento, nuestro resultado será un (IEnumerable<T>, List<T>, T[] … ) compuesto de un objeto.


Estas consultas serían erróneas:


string[] nombres = { "Luis", "Pablo", "Susana", "María" };
 
string unNombre     = nombres.Where(n => n == "Susana");
string primerNombre = nombres.Where((n, i) => i == 0);


Esto lo solucionaremos más adelante con los métodos extensores: First, FirstOrDefault, Single y SingleOrDefault.




Frikada


Como frikada os dejo el código real del método Where:

/// <summary>
/// Filters a sequence of values based on a predicate.
/// </summary>
/// 
/// <returns>
/// An <see cref="T:System.Collections.Generic.IEnumerable`1"/> that contains elements from the input sequence that satisfy the condition.
/// </returns>
/// <param name="source">An <see cref="T:System.Collections.Generic.IEnumerable`1"/> to filter.</param><param name="predicate">A function to test each element for a condition.</param><typeparam name="TSource">The type of the elements of <paramref name="source"/>.</typeparam><exception cref="T:System.ArgumentNullException"><paramref name="source"/> or <paramref name="predicate"/> is null.</exception>
[__DynamicallyInvokable]
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    if (source == null)
    throw Error.ArgumentNull("source");
    if (predicate == null)
    throw Error.ArgumentNull("predicate");
    if (source is Enumerable.Iterator<TSource>)
    return ((Enumerable.Iterator<TSource>) source).Where(predicate);
    if (source is TSource[])
    return (IEnumerable<TSource>) new EnumerableWhereArrayIterator<TSource>((TSource[]) source, predicate);
    if (source is List<TSource>)
    return (IEnumerable<TSource>) new EnumerableWhereListIterator<TSource>((List<TSource>) source, predicate);
    else
    return (IEnumerable<TSource>) new EnumerableWhereEnumerableIterator<TSource>(source, predicate);
}

Como podemos apreciar, ellos ya tienen en cuenta el tipo de datos para mantenerlo y no realizar conversiones innecesarias. El código de las clases internas WhereArayIterator, etc, ya no lo pongo porque si no, no acabaríamos nunca.







Pues hasta aquí el método extensor Where, con el que ya hemos empezado la chicha de verdad, ahora a continuar con ello.



No hay comentarios :

Publicar un comentario