Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Introducción
En este tutorial se enseñan las características en .NET y el lenguaje C#. Aprenderá a:
- Genere secuencias con LINQ.
- Escriba métodos que puede usar fácilmente en consultas LINQ.
- Distinguir entre evaluación diligente y diferida.
Se aprenden estas técnicas desarrollando una aplicación que muestra una de las habilidades básicas de cualquier mago: el faro shuffle. Una mezcla faro es una técnica en la que se divide una baraja exactamente a la mitad, y luego se entremezcla cada carta de cada mitad para reconstruir la baraja 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.
En este tutorial se ofrece un vistazo claro a la manipulación de secuencias de datos. La aplicación construye una baraja de cartas, realiza una secuencia de orden aleatorio y escribe la secuencia cada vez. También compara el orden 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
- La versión más reciente del SDK de .NET
- Editor de Visual Studio Code
- El DevKit de C#
Creación de la aplicación
Cree una 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 -o LinqFaroShuffle en el indicador de comandos. Este comando 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
Sugerencia
En este tutorial, puede organizar el código en un espacio de nombres llamado LinqFaroShuffle para que coincida con el código de ejemplo o puede usar el espacio de nombres global predeterminado. Si decide usar un espacio de nombres, asegúrese de que todas las clases y métodos se encuentran de forma coherente en el mismo espacio de nombres o agregue instrucciones adecuadas using según sea necesario.
Considere lo que constituye una baraja de cartas. Una baraja de cartas tiene cuatro trajes, y cada traje tiene 13 valores. Normalmente, podría considerar la posibilidad de crear una Card clase inmediatamente y rellenar una colección de Card objetos a mano. Con LINQ, puede ser más conciso que la forma habitual de crear una baraja de cartas. En lugar de crear una Card clase, cree dos secuencias para representar trajes y clasificaciones. Cree un par de métodos de iterador que generen las clasificaciones y se adapten a las IEnumerable<T>cadenas:
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 métodos bajo la declaración Console.WriteLine 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. Coloque la consulta LINQ en la parte superior del Program.cs archivo. Así es como se ve:
var startingDeck = from s in Suits()
from r in Ranks()
select (Suit: s, Rank: r);
// Display each card that's generated and placed in startingDeck
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 este ejemplo. El primer elemento de la primera secuencia de origen (palos) se combina con todos los elementos de la segunda secuencia (clasificaciones). Este proceso produce las 13 tarjetas del primer traje. 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.
Tenga en cuenta que si escribe su LINQ en la sintaxis de consulta usada en el ejemplo anterior o usa 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 se puede escribir en la sintaxis del método como:
var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => (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 la sintaxis que mejor funcione para su situación. Por ejemplo, si trabaja en un equipo en el que algunos miembros tienen dificultades con la sintaxis del método, intente usar la sintaxis de consulta.
Ejecute el ejemplo que ha creado en este momento. Muestra las 52 cartas en la baraja. Es posible que le resulte ú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 de cada secuencia solo se genera según sea necesario.
Manipular el orden
A continuación, céntrese en cómo ordena las cartas en la baraja. El primer paso en cualquier orden aleatorio consiste en dividir la baraja en dos. Los Take métodos y Skip que forman parte de las API de LINQ proporcionan esa característica. Colóquelos siguiendo el foreach bucle:
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
Sin embargo, no hay ningún método shuffle del que aprovecharse en la biblioteca estándar, por lo que debe escribir su propio método. El método de mezclado que creó ilustra varias técnicas que se usan con programas basados en LINQ, por lo que cada parte de este proceso se explica paso a paso.
Para agregar funcionalidad a la forma de interactuar con los resultados de las IEnumerable<T> consultas LINQ, escriba algunos tipos especiales de métodos denominados métodos de extensión. 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:
public static class CardExtensions
{
extension<T>(IEnumerable<T> sequence)
{
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
{
// Your implementation goes here
return default;
}
}
}
Nota:
Si usa un editor distinto de Visual Studio (por ejemplo, Visual Studio Code), es posible que tenga que agregar using LinqFaroShuffle; al principio del archivo de Program.cs para que los métodos de extensión sean accesibles. Visual Studio agrega automáticamente esta instrucción 'using', pero otros editores pueden no hacerlo.
El extension contenedor especifica el tipo que se va a extender. El extension nodo declara el tipo y el nombre del parámetro receiver para todos los miembros del extension contenedor. En este ejemplo, estás extendiendo IEnumerable<T> y el parámetro se denomina sequence.
Las declaraciones de miembros de extensión aparecen como si formaran parte del tipo de receptor.
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
Llame al método como si fuera un método miembro del tipo extendido. 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.
Puesto que divides la baraja en mitades, necesitas unir esas mitades. En el código, esto significa que enumera las dos secuencias que adquirió a través de Take y Skip a la vez, intercalando los elementos y creando una secuencia: tu baraja de cartas ahora mezclada. 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. Usas esos dos miembros para enumerar la colección y devolver los elementos. Este método Interleave es un método de iterador, por lo que, en lugar de compilar una colección y devolver la colección, se usa la yield return sintaxis que se muestra en el código anterior.
Esta es la implementación de ese método:
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
{
var firstIter = sequence.GetEnumerator();
var secondIter = second.GetEnumerator();
while (firstIter.MoveNext() && secondIter.MoveNext())
{
yield return firstIter.Current;
yield return secondIter.Current;
}
}
Ahora que escribió este método, vuelva al Main método y ordene la baraja una vez:
var shuffledDeck = top.InterleaveSequenceWith(bottom);
foreach (var c in shuffledDeck)
{
Console.WriteLine(c);
}
Comparaciones
Determina cuántas mezclas se necesitan para volver a establecer la baraja en su orden original. Para averiguarlo, escriba un método que determine si dos secuencias son iguales. Después de tener ese método, coloque el código que ordena la baraja en un bucle y compruebe cuándo la baraja está en orden.
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. Sin embargo, esta vez, en lugar de usar yield return para cada elemento, se comparan los elementos coincidentes de cada secuencia. Cuando se enumera toda la secuencia, si cada elemento coincide, las secuencias son las mismas:
public bool SequenceEquals(IEnumerable<T> second)
{
var firstIter = sequence.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;
}
Este método 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 en una cadena de métodos para una consulta LINQ.
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 porque devuelve un único valor en lugar de una secuencia:
var startingDeck = from s in Suits()
from r in Ranks()
select (Suit: s, Rank: r);
// Display each card generated and placed in startingDeck in the console
foreach (var card in startingDeck)
{
Console.WriteLine(card);
}
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
var shuffledDeck = top.InterleaveSequenceWith(bottom);
var times = 0;
// Re-use the shuffle variable from earlier, or you can make a new one
shuffledDeck = startingDeck;
do
{
shuffledDeck = shuffledDeck.Take(26).InterleaveSequenceWith(shuffledDeck.Skip(26));
foreach (var card in shuffledDeck)
{
Console.WriteLine(card);
}
Console.WriteLine();
times++;
} while (!startingDeck.SequenceEquals(shuffledDeck));
Console.WriteLine(times);
Ejecute el código que has creado hasta ahora y observe cómo la baraja se reorganiza en cada barajado. 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 que ha creado hasta ahora ejecuta un orden aleatorio de salida, donde las tarjetas superior e inferior permanecen iguales en cada ejecución. Hagamos un cambio: en su lugar, use un barajado en vivo, 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 cambio requiere una línea de código. Actualice la consulta de orden aleatorio actual cambiando las posiciones de Take y Skip. Este cambio cambia el orden de las mitades superior e inferior de la cubierta:
shuffledDeck = shuffledDeck.Skip(26).InterleaveSequenceWith(shuffledDeck.Take(26));
Vuelva a ejecutar el programa y verá que se tardan 52 iteraciones para que la baraja se reordene. También observará una degradación grave del rendimiento a medida que el programa continúa ejecutándose.
Hay varias razones para esta caída del rendimiento. Puede abordar una de las principales causas: el uso ineficaz de la evaluación diferida.
La evaluación diferida indica que la evaluación de una instrucción no se realiza hasta que se necesita su valor. 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 programa como este, la evaluación diferida provoca un crecimiento exponencial en el tiempo de ejecución.
Recuerde que ha generado la presentación original mediante una consulta LINQ. Cada orden aleatorio se genera mediante la realización de tres consultas LINQ sobre la baraja anterior. Todas estas consultas se realizan de forma perezosa. Esto también significa que se realizan nuevamente cada vez que se solicita la secuencia. Cuando llegues a la 52ª iteración, estás regenerando la baraja original muchas veces. Escriba un registro para demostrar este comportamiento. Una vez recopilados los datos, puede mejorar el rendimiento.
En el Extensions.cs archivo, escriba o copie el método en el ejemplo de código 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. Anexe este método de extensión a cualquier consulta para marcar que se ejecutó la consulta.
public IEnumerable<T> LogQuery(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;
}
A continuación, instrumente la definición de cada consulta con un mensaje de registro:
var startingDeck = (from s in Suits().LogQuery("Suit Generation")
from r in Ranks().LogQuery("Rank Generation")
select (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. Todavía puede ver los efectos de la evaluación diferida. En una ejecución, ejecuta 2592 consultas, incluido el valor y la generación de trajes.
Puede mejorar el rendimiento del código para reducir el número de ejecuciones que realice. Una corrección sencilla consiste en almacenar en caché los resultados de la consulta LINQ original que construye la baraja de cartas. Actualmente, las consultas se ejecutan una y otra vez cada vez que el bucle do-while pasa por una iteración, se reconstruye la baraja de cartas y se vuelve a reorganizar cada vez. Para almacenar en caché la baraja de cartas, aplique los métodos ToArray LINQ y ToList. Al anexarlos a las consultas, realizan las mismas acciones a las que se les dijo, pero ahora almacenan 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:
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 ejecutar con el modo de barajado y verá mejoras similares: ahora ejecuta 162 consultas.
Este ejemplo está diseñado para resaltar los casos de uso en los que la evaluación diferida puede provocar 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 si decide usar la evaluación diferida o diligente, por lo que mida los procesos y elija la 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 funcionalidad personalizada a las consultas LINQ.
- Buscar áreas en el código donde las consultas LINQ podrían encontrarse con problemas de rendimiento, como la velocidad degradada.
- Evaluación perezosa y ansiosa en las consultas LINQ y las implicaciones que podrían tener sobre el rendimiento de las consultas.
Además de LINQ, aprendiste sobre una técnica que usan los magos para trucos de cartas. Los magos usan el faro shuffle porque pueden controlar dónde se mueven todas las cartas en la baraja. Ahora que ya sabes, ¡no lo estropees para todos los demás!
Para obtener más información sobre LINQ, consulte: