Remarque
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 dans .NET et le langage C#. Vous apprenez à :
- Générez des séquences avec LINQ.
- Écrivez des méthodes que vous pouvez facilement utiliser dans les requêtes LINQ.
- Faire la distinction entre l’évaluation stricte et l’évaluation paresseuse.
Vous apprenez ces techniques en créant une application qui illustre l’une des compétences de base de n’importe quel magicien : le shuffle de féro. Un mélange faro est une technique où vous divisez un jeu de cartes en deux moitiés exactes, puis le mélange entrelace chaque carte de chaque moitié pour reconstituer 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.
Ce tutoriel offre un aperçu clair de la manipulation de séquences de données. L’application construit un jeu de cartes, effectue une séquence de shuffles et écrit la séquence à chaque fois. Il compare également 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
Créez une nouvelle 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 -o LinqFaroShuffle à l’invite. Cette commande 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
Conseil / Astuce
Pour ce tutoriel, vous pouvez organiser votre code dans un espace de noms appelé LinqFaroShuffle pour correspondre à l’exemple de code, ou utiliser l’espace de noms global par défaut. Si vous choisissez d’utiliser un espace de noms, vérifiez que toutes vos classes et méthodes sont cohérentes dans le même espace de noms, ou ajoutez les instructions appropriées using si nécessaire.
Considérez ce qui constitue un jeu de cartes. Un jeu de cartes de jeu a quatre costumes, et chaque costume a 13 valeurs. Normalement, vous pouvez envisager de créer une Card classe immédiatement et de remplir manuellement une collection d’objets Card . Avec LINQ, vous pouvez être plus concis que la façon habituelle de créer un jeu de cartes. Au lieu de créer une Card classe, créez deux séquences pour représenter des combinaisons et des rangs. Créez une paire de méthodes d’itérateur qui génèrent les rangs et correspondent à IEnumerable<T>des chaînes :
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 ces méthodes sous l’instruction Console.WriteLine dans votre Program.cs fichier. Ces deux méthodes utilisent la yield return syntaxe 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’ils sont demandés.
Utilisez maintenant ces méthodes d’itérateur pour créer le jeu de cartes. Placez la requête LINQ en haut du Program.cs fichier. Voici ce à quoi il ressemble :
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);
}
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 pour cet exemple. Le premier élément de la première séquence source (Suits) est combiné à chaque élément de la deuxième séquence (Rangs). Ce processus produit toutes les 13 cartes de première combinaison. 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.
Gardez à l’esprit que si vous écrivez votre LINQ dans la syntaxe de requête utilisée dans l’exemple précédent ou utilisez plutôt la syntaxe de méthode, il est toujours possible d’aller d’une forme de syntaxe à l’autre. La requête précédente é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 => (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 avec la syntaxe de méthode, essayez d’utiliser la syntaxe de requête.
Exécutez l’exemple que vous avez généré à ce stade. Il affiche toutes les 52 cartes dans le jeu. ** Vous pourriez trouver 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 si nécessaire.
Manipuler l’ordre
Ensuite, concentrez-vous sur la façon dont vous mélangez les cartes dans le paquet. Pour bien faire, la première étape consiste à couper le jeu en deux. Les méthodes Take et Skip faisant partie des API LINQ fournissent cette fonctionnalité. Placez-les en suivant la foreach boucle :
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
Toutefois, la bibliothèque standard ne propose pas de méthode de mélange, vous devez donc écrire la vôtre. La méthode de mélange que vous créez illustre plusieurs techniques que vous utilisez avec des programmes LINQ, donc chaque partie de ce processus est expliquée en étapes.
Pour ajouter des fonctionnalités à la façon dont vous interagissez avec les IEnumerable<T> résultats des requêtes LINQ, vous écrivez des types spéciaux de méthodes appelées méthodes d’extension. Une méthode d’extension est une méthode statique à usage spécial 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 :
public static class CardExtensions
{
extension<T>(IEnumerable<T> sequence)
{
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
{
// Your implementation goes here
return default;
}
}
}
Remarque
Si vous utilisez un éditeur autre que Visual Studio (par exemple, Visual Studio Code), vous devrez peut-être ajouter using LinqFaroShuffle; en haut de votre fichier Program.cs pour que les méthodes d’extension soient accessibles. Visual Studio ajoute automatiquement cette instruction using, mais d’autres éditeurs peuvent ne pas le faire.
Le extension conteneur spécifie le type en cours d’extension. Le extension nœud déclare le type et le nom du paramètre de récepteur pour tous les membres à l’intérieur du extension conteneur. Dans cet exemple, vous étendez IEnumerable<T>et le paramètre est nommé sequence.
Les déclarations de membre d’extension apparaissent comme si elles étaient membres du type récepteur :
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
Vous appelez la méthode comme s’il s’agissait d’une méthode membre du type étendu. 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.
Étant donné que vous avez divisé le jeu de cartes en deux, vous devez réunir ces moitiés. Dans le code, cela signifie que vous parcourez simultanément les deux séquences que vous avez acquises via Take et Skip, entrelacez les éléments pour 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 utilisez ces deux membres pour énumérer la collection et retourner les éléments. Cette méthode Interleave est une méthode d’itérateur. Par conséquent, au lieu de générer une collection et de renvoyer la collection, vous utilisez la yield return syntaxe indiquée dans le code précédent.
Voici l’implémentation de cette méthode :
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;
}
}
Maintenant que vous avez écrit cette méthode, revenez à la méthode Main et mélangez le jeu une fois :
var shuffledDeck = top.InterleaveSequenceWith(bottom);
foreach (var c in shuffledDeck)
{
Console.WriteLine(c);
}
Comparaisons
Déterminez le nombre de shuffles qu’il faut pour remettre le jeu dans son ordre d’origine. Pour en savoir plus, écrivez une méthode qui détermine si deux séquences sont égales. Après avoir cette méthode, placez le code qui déchique le jeu dans une boucle et vérifiez quand le jeu est de retour 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. Toutefois, cette fois, au lieu d’utiliser yield return pour chaque élément, vous comparez les éléments correspondants de chaque séquence. Lorsque la séquence entière est énumérée, si chaque élément correspond, les séquences sont identiques :
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;
}
Cette méthode montre un deuxième idiome 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.
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 :
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);
Exécutez le code que vous avez construit jusqu’à présent et observez comment le jeu de cartes se réorganise à chaque mélange. 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 généré jusqu’à présent exécute une séquence aléatoire, où les cartes supérieures et inférieures restent identiques à chaque exécution. Faisons un changement : utilisons un in shuffle à la place, 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. Cette modification nécessite une ligne de code. Mettez à jour la requête de battage actuelle en inversant les positions de Take et de Skip, Cette modification change l’ordre des moitiés supérieure et inférieure du pont :
shuffledDeck = shuffledDeck.Skip(26).InterleaveSequenceWith(shuffledDeck.Take(26));
Réexécutez le programme et vous voyez qu’il faut 52 itérations pour que le jeu se réorganise. Vous remarquez également une grave dégradation des performances à mesure que le programme continue à s’exécuter.
Il existe plusieurs raisons pour cette baisse de performances. Vous pouvez vous attaquer à l’une des principales causes : utilisation inefficace de l’évaluation différée.
L’évaluation différée indique que l’évaluation d’une instruction n’est pas effectuée tant que sa valeur n’est pas 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 programme comme celui-ci, l’évaluation différée provoque une croissance exponentielle dans le temps d’exécution.
N’oubliez pas que vous avez 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 requêtes sont effectuées de façon paresseuse. Cela signifie également qu’ils sont réexécrits chaque fois que la séquence est demandée. Au moment où vous atteignez la 52ème itération, vous régénérez le jeu d’origine plusieurs fois. Écrivez un journal pour illustrer ce comportement. Une fois que vous avez recueilli des données, vous pouvez améliorer les performances.
Dans votre Extensions.cs fichier, tapez ou copiez la méthode dans l’exemple de code suivant. 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. Ajoutez cette méthode d’extension à n’importe quelle requête pour marquer que la requête a été exécutée.
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;
}
Ensuite, instrumentez la définition de chaque requête avec un message de journal :
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);
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 voyez toujours les effets d'évaluation paresseuse. En une seule exécution, elle exécute 2 592 requêtes, notamment la valeur et la génération d’adéquation.
Vous pouvez améliorer les performances du code pour réduire le nombre d’exécutions que vous effectuez. Un correctif simple consiste à mettre en cache les résultats de la requête LINQ d’origine qui construit le jeu de cartes. Actuellement, vous réexécutez les requêtes chaque fois que la boucle do-while passe par une itération, reconstruisez le jeu de cartes et réexécutez-les à chaque fois. Pour mettre en cache le jeu de cartes, appliquez les méthodes ToArray LINQ 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 que vous choisissez d’appeler. Ajoutez la méthode LINQ ToArray aux deux requêtes et réexécutez le programme :
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 avec l'option shuffle et vous constaterez des améliorations similaires : il exécute maintenant 162 requêtes.
Cet exemple est conçu pour mettre en évidence les cas d’usage où l’évaluation différée peut entraîner des difficultés 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 l'évaluation différée ou immédiate. Évaluez les performances de vos processus et choisissez l'é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 des fonctionnalités personnalisées aux requêtes LINQ.
- Localisation de zones dans le code où les requêtes LINQ peuvent rencontrer des problèmes de performances tels que la vitesse détériorée.
- Évaluation différée et hâtive dans les requêtes LINQ ainsi que leurs implications sur les performances des requêtes.
En plus de LINQ, vous avez appris une technique utilisée par les magiciens pour les tours de cartes. Les magiciens utilisent le mélange faro parce qu'ils peuvent contrôler le déplacement de chaque carte dans le jeu. Maintenant que vous le savez, gardez le secret !
Pour plus d’informations sur LINQ, consultez :