Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Présentation
Ce tutoriel vous apprend les fonctionnalités de .NET Core et du langage C#. Vous découvrirez comment :
- Générez des séquences avec LINQ.
- Écrire des méthodes qui peuvent être facilement utilisées dans les requêtes LINQ.
- Faire la distinction entre l’évaluation stricte et l’évaluation paresseuse.
Vous allez apprendre ces techniques en créant une application qui illustre l’une des compétences de base d'un magicien : le faro shuffle. En quelques mots, le mélange faro est une technique qui consiste à diviser un paquet de cartes en deux moitiés exactes, puis à intercaler une carte sur deux de chacune des deux moitiés de façon à reconstruire le jeu d’origine.
Les magiciens utilisent cette technique, car chaque carte se trouve dans un emplacement connu après chaque shuffle, et l’ordre est un modèle répétitif.
Pour vos besoins, c'est un aperçu léger sur la manipulation de séquences de données. L’application que vous allez générer construit un jeu de cartes, puis effectue une séquence de shuffles, en écrivant la séquence à chaque fois. Vous allez également comparer l’ordre mis à jour à l’ordre d’origine.
Ce didacticiel comporte plusieurs étapes. Après chaque étape, vous pouvez exécuter l’application et voir la progression. Vous pouvez également voir l’exemple terminé dans le dépôt GitHub dotnet/samples. Pour obtenir des instructions de téléchargement, consultez Exemples et didacticiels.
Conditions préalables
- La dernière version du SDK .NET
- Éditeur de code Visual Studio
- Le DevKit C#
Créer l’application
La première étape consiste à créer une application. Ouvrez une invite de commandes et créez un répertoire pour votre application. Réglez-le comme répertoire actuel. Saisissez la commande dotnet new console
à l’invite. Cela crée les fichiers de démarrage pour une application « Hello World » de base.
Si vous n’avez jamais utilisé C# auparavant, ce didacticiel explique la structure d’un programme C#. Vous pouvez lire cela, puis revenir ici pour en savoir plus sur LINQ.
Créer le jeu de données
Avant de commencer, vérifiez que les lignes suivantes se trouvent en haut du fichier Program.cs
généré par dotnet new console
:
// Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
Si ces trois lignes (using
directives) ne sont pas en haut du fichier, votre programme peut ne pas être compilé.
Maintenant que vous avez toutes les références dont vous aurez besoin, considérez ce qui constitue un jeu de cartes. Généralement, un jeu de cartes de jeu a quatre costumes, et chaque costume a treize valeurs. Normalement, vous pourriez envisager de créer une classe Card
dès le départ et de remplir une collection d’objets Card
à la main. Avec LINQ, vous pouvez être plus concis que le moyen habituel de gérer la création d’un jeu de cartes. Au lieu de créer une classe Card
, vous pouvez créer deux séquences pour représenter des combinaisons et des rangs, respectivement. Créez deux méthodes d’itération simples qui génèreront les rangs et les couleurs sous forme de IEnumerable<T> de chaînes :
// 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";
}
Placez-les sous la méthode Main
dans votre fichier Program.cs
. Ces deux méthodes utilisent la syntaxe yield return
pour produire une séquence à mesure qu’elles s’exécutent. Le compilateur génère un objet qui implémente IEnumerable<T> et génère la séquence de chaînes à mesure qu’elles sont demandées.
Utilisez maintenant ces méthodes d’itérateur pour créer le jeu de cartes. Vous allez placer la requête LINQ dans notre méthode Main
. Voici un aperçu :
// 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);
}
}
Les clauses from
multiples produisent un SelectMany, ce qui crée une séquence unique à partir de la combinaison de chaque élément de la première séquence avec chaque élément de la deuxième séquence. L’ordre est important à nos fins. Le premier élément de la première séquence source (Suits) est combiné à chaque élément de la deuxième séquence (Rangs). Cette opération génère les treize cartes de la première couleur. Ce processus est répété avec chaque élément de la première séquence (Suits). Le résultat final est un jeu de cartes triées par des combinaisons, suivies de valeurs.
Il est important de garder à l’esprit que si vous choisissez d’écrire votre LINQ dans la syntaxe de requête utilisée ci-dessus ou d’utiliser la syntaxe de méthode à la place, il est toujours possible d’aller d’une forme de syntaxe à l’autre. La requête ci-dessus écrite dans la syntaxe de requête peut être écrite dans la syntaxe de méthode comme suit :
var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => new { Suit = suit, Rank = rank }));
Le compilateur traduit les instructions LINQ écrites avec la syntaxe de requête en syntaxe d’appel de méthode équivalente. Par conséquent, quel que soit votre choix de syntaxe, les deux versions de la requête produisent le même résultat. Choisissez la syntaxe qui convient le mieux à votre situation : par exemple, si vous travaillez dans une équipe où certains membres ont des difficultés à utiliser la syntaxe de méthode, essayez d’utiliser la syntaxe de requête.
Passez en avant et exécutez l’exemple que vous avez créé à ce stade. Il affichera les 52 cartes du paquet. Vous pouvez constater qu’il est très utile d’exécuter cet exemple sous un débogueur pour observer comment les méthodes Suits()
et Ranks()
s’exécutent. Vous pouvez clairement voir que chaque chaîne de chaque séquence est générée uniquement en fonction des besoins.
Manipuler l’ordre
Ensuite, concentrez-vous sur comment vous allez mélanger les cartes du jeu. Pour bien faire, la première étape consiste à couper le jeu en deux. Les méthodes Take et Skip qui font partie des API LINQ fournissent cette fonctionnalité pour vous. Placez-les sous la boucle foreach
:
// 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);
}
Cependant, il n’y a pas de méthode de battage dans la bibliothèque standard ; vous devez donc écrire la vôtre. La méthode de mélange que vous allez créer illustre plusieurs techniques que vous utiliserez avec des programmes LINQ, c'est pourquoi chaque partie de ce processus sera expliquée en étapes.
Pour ajouter certaines fonctionnalités à la façon dont vous interagissez avec les IEnumerable<T> que vous obtiendrez à partir des requêtes LINQ, vous devez écrire des types particuliers de méthodes appelées méthodes d’extension. Brièvement, une méthode d’extension est une méthode statique spéciale qui ajoute de nouvelles fonctionnalités à un type déjà existant sans avoir à modifier le type d’origine auquel vous souhaitez ajouter des fonctionnalités.
Donnez à vos méthodes d’extension une nouvelle maison en ajoutant un nouveau fichier de classe statique à votre programme appelé Extensions.cs
, puis commencez à générer la première méthode d’extension :
// 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
}
}
}
Examinez la signature de méthode pour un instant, en particulier les paramètres :
public static IEnumerable<T> InterleaveSequenceWith<T> (this IEnumerable<T> first, IEnumerable<T> second)
Vous pouvez voir l’ajout du modificateur this
sur le premier argument à la méthode. Cela signifie que vous appelez la méthode comme s’il s’agissait d’une méthode membre du type du premier argument. Cette déclaration de méthode suit également un idiome standard où les types d’entrée et de sortie sont IEnumerable<T>
. Cette pratique permet aux méthodes LINQ d’être chaînées pour effectuer des requêtes plus complexes.
Bien entendu, le jeu ayant été coupé en deux, il faut réunir les deux moitiés. Dans le code, cela signifie que vous allez énumérer simultanément les deux séquences que vous avez acquises via Take et Skip, et les éléments interleaving
, afin de créer une seule séquence : votre jeu de cartes désormais mélangé. L’écriture d’une méthode LINQ qui fonctionne avec deux séquences nécessite que vous compreniez le fonctionnement de IEnumerable<T>.
L’interface IEnumerable<T> a une méthode : GetEnumerator. L’objet retourné par GetEnumerator a une méthode pour passer à l’élément suivant et une propriété qui récupère l’élément actuel dans la séquence. Vous allez utiliser ces deux membres pour énumérer la collection et retourner les éléments. Cette méthode Interleave est une méthode itérateur. Par conséquent, au lieu de créer une collection et de retourner la collection, vous utiliserez la syntaxe yield return
indiquée ci-dessus.
Voici l’implémentation de cette méthode :
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;
}
}
Maintenant que vous avez écrit cette méthode, revenez à la méthode Main
et mélangez le jeu une fois :
// 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);
}
}
Comparaisons
Combien de battages faut-il compter pour remettre le jeu dans l’ordre d’origine ? Pour en savoir plus, vous devez écrire une méthode qui détermine si deux séquences sont égales. Ensuite, il vous faudra placer le code qui mélange le jeu dans une boucle et vérifier si le jeu est de nouveau dans l’ordre.
L’écriture d’une méthode pour déterminer si les deux séquences sont égales doit être simple. Il s'agit d'une structure similaire à la méthode que vous avez utilisée pour mélanger le jeu de cartes. Seulement, cette fois, au lieu d’utiliser l’instruction yield return
sur chaque élément, vous allez comparer les éléments correspondants de chaque séquence. Lorsque la séquence entière a été énumérée, si chaque élément correspond, les séquences sont identiques :
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;
}
Cela montre une deuxième expression LINQ : les méthodes terminales. Ils prennent une séquence comme entrée (ou dans ce cas, deux séquences) et retournent une valeur scalaire unique. Lorsque vous utilisez des méthodes de terminal, elles sont toujours la méthode finale dans une chaîne de méthodes pour une requête LINQ, d’où le nom « terminal ».
Vous pouvez voir cela en action lorsque vous l’utilisez pour déterminer quand le jeu est de retour dans son ordre d’origine. Placez le code de mélange à l’intérieur d’une boucle, et arrêtez-la lorsque la séquence est à nouveau dans l’ordre d’origine en appliquant la méthode SequenceEquals()
. Vous pouvez voir qu’il s’agirait toujours de la méthode finale dans n’importe quelle requête, car elle retourne une valeur unique au lieu d’une séquence :
// 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);
}
Exécutez le code ainsi obtenu et étudiez la façon dont le jeu se réorganise à chaque battage. Après 8 shuffles (itérations de la boucle do-while), le jeu revient à la configuration d’origine dans laquelle il se trouvait lorsque vous l’avez créé à partir de la requête LINQ de départ.
Optimisations
L’exemple que vous avez produit jusque-là exécute un mélange extérieur, où les cartes du haut et du bas restent les mêmes à chaque exécution. Faisons une modification en effectuant plutôt un mélange intérieur, où les 52 cartes changent toutes de position. Dans le cas d’un mélange intérieur, on intercale les deux moitiés du jeu de sorte que la première carte de la moitié du dessous devienne la première carte du jeu. Cela signifie que la dernière carte dans la moitié supérieure devient la carte inférieure. Il s’agit d’une simple modification d’une ligne de code unique. Mettez à jour la requête de battage actuelle en inversant les positions de Take et de Skip, Cela modifie l’ordre des moitiés supérieure et inférieure du pont :
shuffle = shuffle.Skip(26).InterleaveSequenceWith(shuffle.Take(26));
Réexécutez le programme et vous verrez qu’il faut 52 itérations pour que le jeu se réorganise. Vous commencerez également à remarquer des dégradations graves des performances à mesure que le programme s’exécute.
Il y a un certain nombre de raisons pour cela. L’une des causes principales de cette baisse de performances est une utilisation inefficace de l’évaluation paresseuse.
En bref, l’évaluation d’une instruction n’est effectuée que lorsque sa valeur devient nécessaire. Les requêtes LINQ sont évaluées de cette manière. Les séquences sont générées uniquement à mesure que les éléments sont demandés. En règle générale, c’est un avantage majeur de LINQ. Toutefois, dans un usage tel que ce programme, cela provoque une croissance exponentielle dans le temps d’exécution.
N’oubliez pas que nous avons généré le jeu d’origine à l’aide d’une requête LINQ. Chaque mélange est généré en effectuant trois requêtes LINQ sur le jeu précédent. Toutes ces opérations sont exécutées de façon paresseuse. Cela signifie également qu’elles sont effectuées à nouveau chaque fois que la séquence est demandée. À la 52e itération, vous aurez régénéré le jeu d’origine de nombreuses fois. Nous allons écrire un journal pour illustrer ce comportement. Ensuite, vous résoudrez le problème.
Dans votre fichier Extensions.cs
, tapez ou copiez la méthode ci-dessous. Cette méthode d’extension crée un fichier appelé debug.log
dans votre répertoire de projet et enregistre la requête en cours d’exécution dans le fichier journal. Cette méthode d’extension peut être ajoutée à n’importe quelle requête pour marquer que la requête a été exécutée.
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;
}
Vous verrez une ligne ondulée rouge sous File
, ce qui signifie qu’elle n’existe pas. Elle ne sera pas compilée, car le compilateur ne sait pas ce que File
est. Pour résoudre ce problème, veillez à ajouter la ligne de code suivante sous la première ligne de Extensions.cs
:
using System.IO;
Cela doit résoudre le problème et l’erreur rouge disparaît.
Ensuite, instrumentez la définition de chaque requête avec un message de journal :
// 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);
}
Remarquez que vous n’écrivez pas dans le journal à chaque fois que vous accédez à une requête, Vous vous connectez uniquement lorsque vous créez la requête d’origine. Le programme prend encore beaucoup de temps à s’exécuter, mais maintenant vous pouvez voir pourquoi. Si vous perdez patience au cours de l’exécution du mélange intérieur avec journalisation, revenez au mélange extérieur. Vous verrez quand même les effets de l’évaluation paresseuse. Sur une itération, elle exécute 2 592 requêtes, génération de toutes les valeurs et couleurs comprise.
Vous pouvez améliorer les performances du code ici pour réduire le nombre d’exécutions que vous effectuez. L’un des correctifs les plus simples consiste à mettre en cache les résultats de la requête LINQ d’origine qui construit le jeu de cartes. Actuellement, les requêtes sont réexécutées chaque fois que la boucle do-while effectue une itération, construisant et battant de nouveau le jeu de cartes à chaque fois. Pour mettre en cache le jeu de cartes, vous pouvez tirer parti des méthodes LINQ ToArray et ToList; Lorsque vous les ajoutez aux requêtes, ils effectuent les mêmes actions que celles auxquelles vous leur avez dit, mais maintenant ils stockent les résultats dans un tableau ou une liste, selon la méthode à appeler. Ajoutez la méthode LINQ ToArray aux deux requêtes et réexécutez le programme :
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);
}
Le mélange extérieur est descendu à 30 requêtes. Réexécutez en mode shuffle et vous verrez des améliorations similaires : on exécute désormais 162 requêtes.
Sachez que cet exemple est conçu pour mettre en évidence les cas d’usage où l’évaluation paresseuse cause des problèmes de performances. S’il est important de voir son impact sur les performances du code, il faut également comprendre que toutes les requêtes ne doivent pas s’exécuter de manière stricte. L'impact sur les performances que vous subissez sans utiliser ToArray vient du fait que chaque nouvelle configuration du jeu de cartes est construite à partir de la configuration précédente. Avec l’évaluation paresseuse, chaque nouvelle configuration du jeu est générée à partir du jeu d’origine, y compris l’exécution du code qui a construit startingDeck
, Cela provoque une grande quantité de travail supplémentaire.
Dans la pratique, certains algorithmes fonctionnent bien avec l’évaluation stricte, d’autres avec l’évaluation paresseuse. Pour une utilisation quotidienne, l’évaluation différée est généralement un meilleur choix lorsque la source de données est un processus distinct, comme un moteur de base de données. Pour les bases de données, l’évaluation différée permet aux requêtes plus complexes d’exécuter un seul aller-retour vers le processus de base de données et de revenir au reste de votre code. LINQ est flexible, que vous choisissiez d'utiliser une évaluation différée ou immédiate. Mesurez vos processus et choisissez le type d'évaluation qui vous offre les meilleures performances.
Conclusion
Dans ce projet, vous avez abordé les éléments suivants :
- utilisation de requêtes LINQ pour agréger des données dans une séquence significative
- écriture de méthodes d’extension pour ajouter nos propres fonctionnalités personnalisées aux requêtes LINQ
- localisation de zones dans notre code où nos requêtes LINQ peuvent rencontrer des problèmes de performances tels que la vitesse détériorée
- utiliser l’évaluation paresseuse et l’évaluation stricte dans des requêtes LINQ, avec les implications que cela comporte sur les performances de requête.
En plus de LINQ, vous avez appris un peu sur une technique utilisée par les magiciens pour les tours de cartes. Les magiciens utilisent le shuffle de Féro parce qu’ils peuvent contrôler où chaque carte se déplace dans le jeu. Maintenant que vous le savez, gardez le secret !
Pour plus d’informations sur LINQ, consultez :
- L'introduction à LINQ
- opérations de requête LINQ de base (C#)
- transformations de données avec LINQ (C#)
- syntaxe de requête et syntaxe de méthode dans linQ (C#)
- Fonctionnalités de C# qui prennent en charge LINQ