Leer en inglés

Compartir a través de


Trabajar con Language-Integrated Query (LINQ)

Introducción

En este tutorial se enseñan las características de .NET Core y el lenguaje C#. Aprenderá a:

  • Genere secuencias con LINQ.
  • Escribir métodos que se pueden usar fácilmente en consultas LINQ.
  • Distinguir entre evaluación diligente y diferida.

Aprenderá estas técnicas mediante la creación de una aplicación que muestra uno de los conocimientos básicos de cualquier mago: el orden aleatorio faro. En resumen, el orden aleatorio faro es una técnica basada en dividir la baraja exactamente por la mitad; a continuación, el orden aleatorio intercala cada carta de cada mitad de la baraja hasta volver a crear la original.

Los magos usan esta técnica porque cada carta está en una ubicación conocida después de cada barajada y el orden es un patrón que se repite.

Para el propósito sobre el que trata este artículo, resulta divertido ocuparnos de la manipulación de secuencias de datos. La aplicación que se va a crear compilará una baraja de cartas y después realizará una secuencia de órdenes aleatorios, que escribirá cada vez la secuencia completa. También comparará el pedido actualizado con el pedido original.

Este tutorial tiene varios pasos. Después de cada paso, puede ejecutar la aplicación y ver el progreso. También puede ver el ejemplo completado en el repositorio dotnet/samples de GitHub. Para obtener instrucciones de descarga, consulte Ejemplos y tutoriales.

Prerrequisitos

Creación de la aplicación

El primer paso es crear una nueva aplicación. Abra un símbolo del sistema y cree un nuevo directorio para la aplicación. Conviértalo en el directorio actual. Escriba el comando dotnet new console en el indicador de comandos. Esto crea los archivos de inicio para una aplicación básica "Hola mundo".

Si nunca ha usado C# antes, en este tutorial se explica la estructura de un programa de C#. Puede leerlo y volver aquí para obtener más información sobre LINQ.

Creación del conjunto de datos

Antes de comenzar, asegúrese de que las siguientes líneas están en la parte superior del Program.cs archivo generado por dotnet new console:

// Program.cs
using System;
using System.Collections.Generic;
using System.Linq;

Si estas tres líneas (using directivas) no están en la parte superior del archivo, es posible que el programa no se compile.

Ahora que tienes todas las referencias que necesitarás, ten en cuenta lo que constituye una baraja de cartas. Normalmente, una baraja de cartas de juego tiene cuatro trajes, y cada traje tiene trece valores. Normalmente, podrías considerar crear una Card clase desde el principio y rellenar una colección de objetos Card a mano. Con LINQ, puedes ser más conciso que la forma tradicional de crear una baraja de cartas. En lugar de crear una Card clase, puede crear dos secuencias para representar trajes y clasificaciones, respectivamente. Podrá crear un par sencillo de métodos iterator que generará las clasificaciones y palos como objetos IEnumerable<T> de cadenas:

// Program.cs
// The Main() method

static IEnumerable<string> Suits()
{
    yield return "clubs";
    yield return "diamonds";
    yield return "hearts";
    yield return "spades";
}

static IEnumerable<string> Ranks()
{
    yield return "two";
    yield return "three";
    yield return "four";
    yield return "five";
    yield return "six";
    yield return "seven";
    yield return "eight";
    yield return "nine";
    yield return "ten";
    yield return "jack";
    yield return "queen";
    yield return "king";
    yield return "ace";
}

Coloque estos debajo del método Main en su archivo Program.cs. Estos dos métodos usan la yield return sintaxis para generar una secuencia a medida que se ejecutan. El compilador compila un objeto que implementa IEnumerable<T> y genera la secuencia de cadenas a medida que se solicitan.

Ahora, puede usar estos métodos iterator para crear la baraja de cartas. Colocarás la consulta LINQ en el método Main. A continuación se muestra un vistazo a él:

// Program.cs
static void Main(string[] args)
{
    var startingDeck = from s in Suits()
                       from r in Ranks()
                       select new { Suit = s, Rank = r };

    // Display each card that we've generated and placed in startingDeck in the console
    foreach (var card in startingDeck)
    {
        Console.WriteLine(card);
    }
}

Las cláusulas múltiples from generan un SelectMany, que crea una sola secuencia a partir de la combinación de cada elemento de la primera secuencia con cada elemento de la segunda secuencia. El orden es importante para nuestros propósitos. El primer elemento de la primera secuencia de origen (palos) se combina con todos los elementos de la segunda secuencia (clasificaciones). Esto genera las trece cartas del primer palo. Ese proceso se repite con cada elemento de la primera secuencia (Suits). El resultado final es una baraja de cartas ordenadas por palos, seguidos de valores.

Es importante tener en cuenta que si decide escribir su LINQ en la sintaxis de consulta usada anteriormente o usar la sintaxis del método en su lugar, siempre es posible pasar de una forma de sintaxis a la otra. La consulta anterior escrita en la sintaxis de consulta puede escribirse en la sintaxis de método como:

var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => new { Suit = suit, Rank = rank }));

El compilador traduce instrucciones LINQ escritas con sintaxis de consulta en la sintaxis de llamada al método equivalente. Por lo tanto, independientemente de la elección de la sintaxis, las dos versiones de la consulta generan el mismo resultado. Elija qué sintaxis funciona mejor para su situación: por ejemplo, si trabaja en un equipo en el que algunos de los miembros tienen dificultades con la sintaxis del método, intente preferir usar la sintaxis de consulta.

Continúe y ejecute el ejemplo que ha compilado en este momento. Mostrará las 52 cartas en la baraja. Es posible que le resulte muy útil ejecutar este ejemplo en un depurador para observar cómo se ejecutan los Suits() métodos y Ranks() . Puede ver claramente que cada cadena en cada secuencia se genera solo cuando es necesaria.

Una ventana de la consola que muestra la aplicación escribiendo 52 tarjetas.

Manipular el orden

A continuación, céntrese en cómo vas a ordenar las cartas en la baraja. El primer paso en cualquier orden aleatorio consiste en dividir la baraja en dos. Los métodos Take y Skip que forman parte de las API de LINQ proporcionan esa característica para ti. Colóquelos debajo del foreach bucle:

// Program.cs
public static void Main(string[] args)
{
    var startingDeck = from s in Suits()
                       from r in Ranks()
                       select new { Suit = s, Rank = r };

    foreach (var c in startingDeck)
    {
        Console.WriteLine(c);
    }

    // 52 cards in a deck, so 52 / 2 = 26
    var top = startingDeck.Take(26);
    var bottom = startingDeck.Skip(26);
}

Pero no existe ningún método de orden aleatorio en la biblioteca estándar que pueda aprovechar, por lo que tendrá que escribir el suyo propio. El método de mezcla que vas a crear ilustra varias técnicas que usarás con programas basados en LINQ, por lo que cada parte de este proceso se explicará en pasos.

Para poder agregar cierta funcionalidad a la manera en que interactúas con el IEnumerable<T> que obtendrás de las consultas LINQ, deberás escribir algunos tipos especiales de métodos denominados métodos de extensión. Brevemente, un método de extensión es un método estático de propósito especial que agrega nueva funcionalidad a un tipo ya existente sin tener que modificar el tipo original al que desea agregar funcionalidad.

Asigne a los métodos de extensión un nuevo hogar agregando un nuevo archivo de clase estática al programa denominado Extensions.csy, a continuación, empiece a compilar el primer método de extensión:

// Extensions.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqFaroShuffle
{
    public static class Extensions
    {
        public static IEnumerable<T> InterleaveSequenceWith<T>(this IEnumerable<T> first, IEnumerable<T> second)
        {
            // Your implementation will go here soon enough
        }
    }
}

Examine la firma del método durante un momento, específicamente los parámetros:

public static IEnumerable<T> InterleaveSequenceWith<T> (this IEnumerable<T> first, IEnumerable<T> second)

Puede ver la adición del modificador this en el primer argumento del método. Esto significa que se llama al método como si fuera un método miembro del tipo del primer argumento. Esta declaración de método también sigue un lenguaje estándar donde los tipos de entrada y salida son IEnumerable<T>. Esta práctica permite encadenar métodos LINQ para realizar consultas más complejas.

Naturalmente, dado que divides la baraja en mitades, tendrás que unir esas mitades juntas. En el código, esto significa que enumerará las dos secuencias adquiridas a través de Take y Skip a la vez, interleaving los elementos y crear una sola secuencia: su baraja de cartas recién ordenada aleatoriamente. Escribir un método LINQ que funcione con dos secuencias requiere que comprenda cómo IEnumerable<T> funciona.

La IEnumerable<T> interfaz tiene un método: GetEnumerator. El objeto devuelto por GetEnumerator tiene un método para desplazarse al elemento siguiente y una propiedad que recupera el elemento actual de la secuencia. Utilizará estos dos miembros para enumerar la colección y devolver los elementos. Este método de intercalación será un método iterador, por lo que en lugar de crear una colección y devolverla, usará la sintaxis yield return anterior.

Esta es la implementación de ese método:

public static IEnumerable<T> InterleaveSequenceWith<T>
    (this IEnumerable<T> first, IEnumerable<T> second)
{
    var firstIter = first.GetEnumerator();
    var secondIter = second.GetEnumerator();

    while (firstIter.MoveNext() && secondIter.MoveNext())
    {
        yield return firstIter.Current;
        yield return secondIter.Current;
    }
}

Ahora que ha escrito este método, vuelva al Main método y ordene la baraja una vez:

// Program.cs
public static void Main(string[] args)
{
    var startingDeck = from s in Suits()
                       from r in Ranks()
                       select new { Suit = s, Rank = r };

    foreach (var c in startingDeck)
    {
        Console.WriteLine(c);
    }

    var top = startingDeck.Take(26);
    var bottom = startingDeck.Skip(26);
    var shuffle = top.InterleaveSequenceWith(bottom);

    foreach (var c in shuffle)
    {
        Console.WriteLine(c);
    }
}

Comparaciones

¿Cuántos órdenes aleatorios se necesitan para devolver la baraja a su orden original? Para averiguarlo, deberá escribir un método que determine si dos secuencias son iguales. Cuando ya disponga del método, debe colocar el código que ordena la baraja aleatoriamente en un bucle y comprobarlo para ver cuándo la baraja vuelve a tener su orden original.

Escribir un método para determinar si las dos secuencias son iguales debe ser sencilla. Presenta una estructura similar al método que se escribió para ordenar la baraja aleatoriamente. Solo que esta vez, en lugar de aplicar yield return a cada elemento, se compararán los elementos coincidentes de cada secuencia. Cuando se ha enumerado toda la secuencia, si cada elemento coincide, las secuencias son las mismas:

public static bool SequenceEquals<T>
    (this IEnumerable<T> first, IEnumerable<T> second)
{
    var firstIter = first.GetEnumerator();
    var secondIter = second.GetEnumerator();

    while ((firstIter?.MoveNext() == true) && secondIter.MoveNext())
    {
        if ((firstIter.Current is not null) && !firstIter.Current.Equals(secondIter.Current))
        {
            return false;
        }
    }

    return true;
}

Esto muestra un segundo lenguaje LINQ: métodos terminales. Toman una secuencia como entrada (o en este caso, dos secuencias) y devuelven un único valor escalar. Cuando se usan métodos terminales, siempre son el método final de una cadena de métodos para una consulta LINQ, por lo que el nombre "terminal".

Puede verlo en acción cuando lo use para determinar cuándo vuelve la baraja en su orden original. Coloque el código de mezcla dentro de un bucle y detenga este cuando la secuencia vuelva a su orden original aplicando el método SequenceEquals(). Puede ver que siempre sería el método final en cualquier consulta, ya que devuelve un valor único en lugar de una secuencia:

// Program.cs
static void Main(string[] args)
{
    // Query for building the deck

    // Shuffling using InterleaveSequenceWith<T>();

    var times = 0;
    // We can re-use the shuffle variable from earlier, or you can make a new one
    shuffle = startingDeck;
    do
    {
        shuffle = shuffle.Take(26).InterleaveSequenceWith(shuffle.Skip(26));

        foreach (var card in shuffle)
        {
            Console.WriteLine(card);
        }
        Console.WriteLine();
        times++;

    } while (!startingDeck.SequenceEquals(shuffle));

    Console.WriteLine(times);
}

Ejecuta el código que tienes hasta ahora y toma nota de cómo se reorganiza la baraja en cada mezcla. Después de ocho órdenes aleatorios (iteraciones del bucle do-while), la baraja vuelve a la configuración original en que se encontraba cuando la creó a partir la consulta LINQ inicial.

Optimizaciones

El ejemplo creado hasta el momento se ejecuta en orden no aleatorio, donde las cartas superiores e inferiores son las mismas en cada ejecución. Vamos a realizar un cambio: utilizaremos una ejecución en orden aleatorio en su lugar, donde las 52 cartas cambian de posición. Si se trata de un orden aleatorio, intercale la baraja de tal forma que la primera carta de la mitad inferior sea la primera carta de la baraja. Esto significa que la última tarjeta de la mitad superior se convierte en la tarjeta inferior. Este es un cambio sencillo a una línea de código singular. Actualice la consulta de orden aleatorio actual cambiando las posiciones de Take y Skip. Esto cambiará el orden de las mitades superior e inferior de la baraja:

shuffle = shuffle.Skip(26).InterleaveSequenceWith(shuffle.Take(26));

Vuelva a ejecutar el programa y verá que se tardan 52 iteraciones para que la baraja se reordene. También comenzará a observar algunas degradaciones graves del rendimiento a medida que el programa continúa ejecutándose.

Hay varias razones para esto. Puede que se trate de una de las principales causas de este descenso de rendimiento: un uso ineficaz de la evaluación diferida.

En pocas palabras, la evaluación diferida indica que no se realiza la evaluación de una instrucción hasta que su valor es necesario. Las consultas LINQ son instrucciones se evalúan de forma diferida. Las secuencias solo se generan a medida que se solicitan los elementos. Normalmente, es una ventaja importante de LINQ. Sin embargo, en un uso como este programa, esto provoca un crecimiento exponencial en el tiempo de ejecución.

Recuerde que la baraja original se generó con una consulta LINQ. Cada orden aleatorio se genera mediante la realización de tres consultas LINQ sobre la baraja anterior. Todas se realizan de forma diferida. Esto también significa que se llevan a cabo nuevamente cada vez que se solicita la secuencia. Cuando llegues a la 52ª iteración, estás regenerando la baraja original muchas veces. Vamos a escribir un registro para demostrar este comportamiento. Entonces, lo corregirás.

En el Extensions.cs archivo, escriba o copie el método siguiente. Este método de extensión crea un nuevo archivo llamado debug.log en el directorio del proyecto y registra la consulta que se está ejecutando actualmente en el archivo de registro. Este método de extensión se puede anexar a cualquier consulta para marcar que se ejecutó la consulta.

public static IEnumerable<T> LogQuery<T>
    (this IEnumerable<T> sequence, string tag)
{
    // File.AppendText creates a new file if the file doesn't exist.
    using (var writer = File.AppendText("debug.log"))
    {
        writer.WriteLine($"Executing Query {tag}");
    }

    return sequence;
}

Verá un subrayado en zigzag rojo bajo File, lo que indica que no existe. No se compilará, ya que el compilador no sabe lo que File es. Para solucionar este problema, asegúrese de agregar la siguiente línea de código en la primera línea de Extensions.cs:

using System.IO;

Esto debe resolver el problema y el error rojo desaparece.

A continuación, instrumente la definición de cada consulta con un mensaje de registro:

// Program.cs
public static void Main(string[] args)
{
    var startingDeck = (from s in Suits().LogQuery("Suit Generation")
                        from r in Ranks().LogQuery("Rank Generation")
                        select new { Suit = s, Rank = r }).LogQuery("Starting Deck");

    foreach (var c in startingDeck)
    {
        Console.WriteLine(c);
    }

    Console.WriteLine();
    var times = 0;
    var shuffle = startingDeck;

    do
    {
        // Out shuffle
        /*
        shuffle = shuffle.Take(26)
            .LogQuery("Top Half")
            .InterleaveSequenceWith(shuffle.Skip(26)
            .LogQuery("Bottom Half"))
            .LogQuery("Shuffle");
        */

        // In shuffle
        shuffle = shuffle.Skip(26).LogQuery("Bottom Half")
                .InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half"))
                .LogQuery("Shuffle");

        foreach (var c in shuffle)
        {
            Console.WriteLine(c);
        }

        times++;
        Console.WriteLine(times);
    } while (!startingDeck.SequenceEquals(shuffle));

    Console.WriteLine(times);
}

Observe que no se genera un registro cada vez que accede a una consulta. Solo se registra cuando se crea la consulta original. El programa todavía tarda mucho tiempo en ejecutarse, pero ahora puede ver por qué. Si se le agota la paciencia al ejecutar el orden aleatorio interno con los registros activados, vuelva al orden aleatorio externo. Aún puede ver los efectos de la evaluación diferida. En una ejecución, ejecuta 2592 consultas, incluida toda la generación de palos y valores.

Puede mejorar el rendimiento del código aquí para reducir el número de ejecuciones que realice. Una corrección sencilla que puede realizar es almacenar en caché los resultados de la consulta LINQ original que construye la baraja de cartas. Actualmente, ejecuta las consultas una y otra vez siempre que el bucle do-while pasa por una iteración, lo que vuelve a construir la baraja de cartas y cambia continuamente el orden aleatorio. Para almacenar en caché la baraja de cartas, puede aprovechar los métodos ToArray LINQ y ToList; al anexarlos a las consultas, realizarán las mismas acciones que les ha dicho, pero ahora almacenarán los resultados en una matriz o una lista, en función del método al que elija llamar. Anexe el método ToArray LINQ a ambas consultas y vuelva a ejecutar el programa:

public static void Main(string[] args)
{
    IEnumerable<Suit>? suits = Suits();
    IEnumerable<Rank>? ranks = Ranks();

    if ((suits is null) || (ranks is null))
        return;

    var startingDeck = (from s in suits.LogQuery("Suit Generation")
                        from r in ranks.LogQuery("Value Generation")
                        select new { Suit = s, Rank = r })
                        .LogQuery("Starting Deck")
                        .ToArray();

    foreach (var c in startingDeck)
    {
        Console.WriteLine(c);
    }

    Console.WriteLine();

    var times = 0;
    var shuffle = startingDeck;

    do
    {
        /*
        shuffle = shuffle.Take(26)
            .LogQuery("Top Half")
            .InterleaveSequenceWith(shuffle.Skip(26).LogQuery("Bottom Half"))
            .LogQuery("Shuffle")
            .ToArray();
        */

        shuffle = shuffle.Skip(26)
            .LogQuery("Bottom Half")
            .InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half"))
            .LogQuery("Shuffle")
            .ToArray();

        foreach (var c in shuffle)
        {
            Console.WriteLine(c);
        }

        times++;
        Console.WriteLine(times);
    } while (!startingDeck.SequenceEquals(shuffle));

    Console.WriteLine(times);
}

Ahora el orden aleatorio externo se reduce a 30 consultas. Vuelva a ejecutarlo con el orden aleatorio interno y verá mejoras similares: ahora ejecuta 162 consultas.

Tenga en cuenta que este ejemplo está diseñado para resaltar los casos de uso en los que la evaluación diferida puede causar dificultades de rendimiento. Si bien es importante ver dónde la evaluación diferida puede afectar al rendimiento del código, es igualmente importante entender que no todas las consultas deben ejecutarse de manera diligente. El impacto de rendimiento que incurre sin usar ToArray es porque cada nueva disposición de la baraja de cartas se construye a partir de la disposición anterior. La evaluación diferida supone que cada nueva configuración de la baraja se realiza a partir de la baraja original, incluso con la ejecución del código que crea el elemento startingDeck. Esto provoca una gran cantidad de trabajo adicional.

En la práctica, algunos algoritmos se ejecutan bien con la evaluación diligente y otros, con la evaluación diferida. Para el uso diario, la evaluación diferida suele ser una mejor opción cuando el origen de datos es un proceso independiente, como un motor de base de datos. En el caso de las bases de datos, la evaluación diferida permite que las consultas más complejas ejecuten solo un recorrido de ida y vuelta al proceso de base de datos y vuelvan al resto del código. LINQ es flexible tanto si decide utilizar la evaluación diferida como la evaluación anticipada, por lo que mida sus procesos y elija el tipo de evaluación que le proporcione el mejor rendimiento.

Conclusión

En este proyecto, ha tratado lo siguiente:

  • uso de consultas LINQ para agregar datos en una secuencia significativa
  • escribir métodos de extensión para agregar nuestra propia funcionalidad personalizada a las consultas LINQ
  • Buscar áreas en nuestro código en las que las consultas LINQ podrían encontrarse con problemas de rendimiento, como la velocidad degradada
  • Evaluación diligente y diferida en lo que respecta a las consultas LINQ y las implicaciones que podrían tener en el rendimiento de la consulta

Aparte de LINQ, ha aprendido algo sobre una técnica que los magos utilizan para hacer trucos de cartas. Los magos usan el orden aleatorio Faro porque les permite controlar dónde está cada carta en la baraja. Ahora que ya sabes, ¡no lo estropees para todos los demás!

Para obtener más información sobre LINQ, consulte:


Recursos adicionales