9. Collections et LINQ
Sections
Réglages de lecture
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
La List<T> de la leçon 4 garde ses éléments dans l’ordre et les retrouve par leur position. Deux autres collections répondent à d’autres questions. Un dictionnaire répond à « quelle est la valeur pour ce nom ? », et un ensemble répond à « cette valeur fait-elle partie du groupe ? ». La seconde moitié de la leçon présente LINQ, une famille de méthodes qui comptent, filtrent et trient une collection, chacune en une ligne. Avec elles, chacune des boucles que la leçon 4 écrivait à la main pour les projets de Guitar Alchemist tient en une seule ligne.
Tous les programmes de cette leçon se trouvent dans code/csharp-beginner ; lance-en un avec dotnet run suivi de son chemin, par exemple examples/l09_dictionary.cs. check.sh compare leur sortie, et les erreurs du compilateur pour les extraits refusés, avec les fichiers de expected/.
Dictionary : une valeur pour chaque clé
Section intitulée « Dictionary : une valeur pour chaque clé »Un Dictionary<TKey, TValue> stocke des paires : une clé, et la valeur qui va avec. Chaque clé n’apparaît qu’une fois. Ses deux types s’écrivent entre les chevrons : Dictionary<string, int> a des clés string et des valeurs int. Ici, la clé est le nom d’une note et la valeur son nombre de demi-tons au-dessus de C, la table que la leçon 3 écrivait sous forme de switch :
// Un Dictionary<string, int> : le nom de chaque note (la clé) donne son nombre de demi-tons au-dessus de C (la valeur)var semitones = new Dictionary<string, int>{ ["C"] = 0, ["D"] = 2, ["E"] = 4, ["F"] = 5, ["G"] = 7, ["A"] = 9, ["B"] = 11,};Console.WriteLine($"{semitones.Count} notes; G is {semitones["G"]} semitones above C");
semitones["F#"] = 6; // une clé qui n'y est pas encore : ajoutéesemitones.Add("Bb", 10); // Add ajoute aussi...try{ semitones.Add("C", 0); // ...mais refuse une clé qui y est déjà}catch (ArgumentException ex){ Console.WriteLine(ex.Message);}Console.WriteLine($"{semitones.Count} notes");
Console.WriteLine(semitones.ContainsKey("Bb"));Console.WriteLine(semitones.ContainsKey("H")); // H est le B de la notation allemande, pas une clé ici
if (semitones.TryGetValue("H", out int h)){ Console.WriteLine($"H is {h} semitones above C");}else{ Console.WriteLine("no H in this dictionary");}
Console.WriteLine(string.Join(" ", semitones)); // chaque élément est une KeyValuePair<string, int>7 notes; G is 7 semitones above CAn item with the same key has already been added. Key: C9 notesTrueFalseno H in this dictionary[C, 0] [D, 2] [E, 4] [F, 5] [G, 7] [A, 9] [B, 11] [F#, 6] [Bb, 10]["C"] = 0entre les accolades range la valeur 0 sous la clé"C"à la création du dictionnaire.semitones["G"]lit la valeur d’une clé, avec les mêmes crochets qu’un tableau, mais avec une clé au lieu d’une position.semitones["F#"] = 6ajoute la clé si elle n’y est pas, et remplace sa valeur si elle y est.Addajoute aussi une clé, mais lève uneArgumentExceptionsi la clé y est déjà : lecatchde la leçon 8 affiche son message.ContainsKeydit si une clé est présente.TryGetValueest une méthode Try, comme celles de la leçon 8 : elle renvoiefalsequand la clé manque, et met la valeur dans sa variableoutquand elle est présente.- Chaque élément d’un dictionnaire est une
KeyValuePair<string, int>, avec uneKeyet uneValue;string.Joinl’affiche sous la forme[key, value].
Un dictionnaire contient la table sous forme de données, pas de code : un programme peut le remplir pendant qu’il s’exécute, et la leçon 10 en remplira un à partir d’un fichier.
Un dictionnaire se lit par clé, pas par position. Demander l’élément 0 d’un dictionnaire dont les clés sont des chaînes est une erreur de type :
var semitones = new Dictionary<string, int> { ["C"] = 0, ["D"] = 2, ["E"] = 4 };Console.WriteLine(semitones[0]);l09_dictionary_by_position.cs(2,29): error CS1503: Argument 1: cannot convert from 'int' to 'string'Une clé absente lève une exception
Section intitulée « Une clé absente lève une exception »Lire une clé absente du dictionnaire ne renvoie ni 0 ni null, mais lève une exception :
// Lire une clé qui n'est pas dans le dictionnaire lève une exceptionvar semitones = new Dictionary<string, int>{ ["C"] = 0, ["D"] = 2, ["E"] = 4, ["F"] = 5, ["G"] = 7, ["A"] = 9, ["B"] = 11,};
string note = "H";Console.WriteLine($"{note} is {semitones[note]} semitones above C");Unhandled exception. System.Collections.Generic.KeyNotFoundException: The given key 'H' was not present in the dictionary.La règle de la leçon 8 s’applique. Quand une clé absente est un bogue du programme, l’indexeur semitones[note] et sa KeyNotFoundException sont le bon choix : le programme s’arrête là où se trouve le bogue. Quand une clé absente est un cas ordinaire, comme une note tapée par un utilisateur, utilise TryGetValue.
Compter avec un dictionnaire
Section intitulée « Compter avec un dictionnaire »Un dictionnaire peut compter : la clé est ce que tu comptes, la valeur le nombre de fois que tu l’as vu.
// Combien de fois chaque note apparaît dans la première phrase de l'Hymne à la joie de Beethovenstring[] melody = ["E", "E", "F", "G", "G", "F", "E", "D", "C", "C", "D", "E", "E", "D", "D"];
var counts = new Dictionary<string, int>();foreach (string note in melody){ counts[note] = counts.GetValueOrDefault(note) + 1; // 0 + 1 la première fois}
foreach (var (note, count) in counts){ Console.WriteLine($"{note}: {count}");}E: 5F: 2G: 2D: 4C: 2GetValueOrDefault renvoie la valeur de la clé, ou 0, la valeur par défaut d’un int, quand la clé n’y est pas encore. Le premier E enregistre donc 0 + 1, et chaque E suivant remplace le compte par le compte plus un. foreach (var (note, count) in counts) décompose chaque KeyValuePair en deux variables, sa clé et sa valeur.
Ne compte pas sur l’ordre d’un dictionnaire
Section intitulée « Ne compte pas sur l’ordre d’un dictionnaire »Les notes sont sorties dans l’ordre de leur première apparition : E, F, G, D, C. La documentation de Dictionary ne le promet pas : « The order in which the items are returned is undefined. » (l’ordre dans lequel les éléments sont renvoyés n’est pas défini). Voici ce que fait .NET 10 quand on retire une clé, puis qu’on en ajoute une autre :
// L'ordre d'un foreach sur un dictionnaire : retirer une clé, en ajouter une autrevar semitones = new Dictionary<string, int> { ["C"] = 0, ["D"] = 2, ["E"] = 4, ["G"] = 7, ["A"] = 9 };Console.WriteLine(string.Join(" ", semitones.Keys));
semitones.Remove("D");semitones.Add("F#", 6);Console.WriteLine(string.Join(" ", semitones.Keys));C D E G AC F# E G ALa clé F# a été ajoutée en dernier mais arrive en deuxième position, à la place laissée par D. Tant qu’un programme ne fait qu’ajouter des clés, l’ordre ressemble à l’ordre d’ajout ; un seul Remove suffit à le casser. Quand l’ordre compte, trie : OrderBy, plus bas, le fait, et l’exercice 2 trie les comptes de la mélodie. Le journal consigne cette expérience, avec l’hypothèse écrite avant de la lancer.
HashSet : chaque valeur une seule fois
Section intitulée « HashSet : chaque valeur une seule fois »Un HashSet<T> est un ensemble : il contient chaque valeur au plus une fois, et il répond vite à « cette valeur en fait-elle partie ? ». Les notes d’un accord forment un ensemble : do majeur, c’est C, E et G, dans n’importe quel ordre, et ajouter un second E ne change pas l’accord.
// Un HashSet<string> contient chaque valeur une seule fois : les notes d'un accordHashSet<string> cMajor = ["C", "E", "G"];HashSet<string> aMinor = ["A", "C", "E"];
Console.WriteLine(cMajor.Add("E")); // False : E y est déjàConsole.WriteLine(cMajor.Add("B")); // True : C E G B, c'est Cmaj7Console.WriteLine($"{cMajor.Count} notes, contains G: {cMajor.Contains("G")}");cMajor.Remove("B");
// IntersectWith, UnionWith et ExceptWith modifient l'ensemble sur lequel on les appelle : travaille sur une copievar common = new HashSet<string>(cMajor);common.IntersectWith(aMinor);Console.WriteLine($"in both chords: {string.Join(" ", common)}");
var all = new HashSet<string>(cMajor);all.UnionWith(aMinor);Console.WriteLine($"in either chord: {string.Join(" ", all)}");
var onlyC = new HashSet<string>(cMajor);onlyC.ExceptWith(aMinor);Console.WriteLine($"only in C major: {string.Join(" ", onlyC)}");
HashSet<string> sameNotes = ["G", "C", "E"];Console.WriteLine(cMajor.SetEquals(sameNotes)); // True : mêmes notes, écrites dans un autre ordre
HashSet<string> cSharp = ["C#", "F", "G#"];HashSet<string> dFlat = ["Db", "F", "Ab"];Console.WriteLine(cSharp.SetEquals(dFlat)); // False : les chaînes diffèrent, pas les sonsFalseTrue4 notes, contains G: Truein both chords: C Ein either chord: C E G Aonly in C major: GTrueFalseAddrenvoiefalsequand la valeur est déjà dans l’ensemble, et ne change rien.IntersectWithgarde les valeurs qui sont aussi dans l’autre ensemble : C et E, les deux notes que do majeur et la mineur ont en commun.UnionWithajoute les valeurs de l’autre ensemble, etExceptWithles retire. Ces trois méthodes modifient l’ensemble lui-même, donc le programme travaille sur des copies faites avecnew HashSet<string>(cMajor).SetEqualscompare les contenus et ignore l’ordre.
La dernière ligne est une limite de ce programme, pas de HashSet. Do dièse et ré bémol sont la même touche du piano, mais "C#" et "Db" sont des chaînes différentes, donc les deux ensembles diffèrent. Un programme qui doit les traiter comme la même note compare des nombres, les demi-tons du dictionnaire plus haut, plutôt que des noms.
Un ensemble n’a pas de positions. Sa documentation dit qu’il « is not sorted and cannot contain duplicate elements » (n’est pas trié et ne peut pas contenir de doublons), et il n’a pas d’indexeur :
HashSet<string> chord = ["C", "E", "G"];Console.WriteLine(chord[0]);l09_hashset_index.cs(2,19): error CS0021: Cannot apply indexing with [] to an expression of type 'HashSet<string>'List<T> |
Dictionary<TKey, TValue> |
HashSet<T> |
|
|---|---|---|---|
| Contient | des éléments dans l’ordre | une valeur par clé | chaque valeur une seule fois |
| Trouver un élément par | position, list[2] |
clé, dict["G"] |
valeur, set.Contains("G") |
| Un doublon | est gardé | clé : Add lève une exception, [key] = remplace |
Add renvoie false |
| À utiliser quand | l’ordre compte | tu cherches par nom | tu demandes « est-ce dedans ? » |
LINQ : compter, filtrer et trier en une ligne
Section intitulée « LINQ : compter, filtrer et trier en une ligne »La leçon 4 écrivait une méthode avec une boucle pour chaque question sur les projets de GA : combien commencent par GA.Business., lesquels sont en F#, quel nom est le plus long. LINQ (Language-Integrated Query, requête intégrée au langage) est une famille de méthodes que possède toute collection : tableaux, listes, dictionnaires et ensembles. Chaque méthode reçoit un petit morceau de code qui dit quoi chercher.
Les lambdas
Section intitulée « Les lambdas »Ce morceau de code est une expression lambda, une méthode sans nom, écrite là où on l’utilise. Project est un record de la leçon 6, avec un nom et un langage :
// Une lambda et une méthode nommée qui font le même travailList<Project> projects =[ new Project("GA.Core", "C#"), new Project("GA.Business.Config", "F#"), new Project("GA.Business.DSL", "F#"),];
Console.WriteLine(projects.Count(p => p.Language == "F#"));Console.WriteLine(projects.Count(IsFSharp));
bool IsFSharp(Project p) => p.Language == "F#";
record Project(string Name, string Language);22Dans p => p.Language == "F#", à gauche de => se trouve le paramètre de la lambda, p ; à droite, la valeur qu’elle renvoie, ici un bool. Count l’appelle une fois pour chaque projet et compte les réponses true. La méthode IsFSharp, écrite avec => comme à la leçon 8, fait le même travail, et Count l’accepte aussi ; la lambda évite de donner un nom à du code utilisé à un seul endroit. Le compilateur déduit le type de p de la collection : dans une List<Project>, p est un Project.
Where, Select, OrderBy et les autres
Section intitulée « Where, Select, OrderBy et les autres »La leçon 4 rangeait les projets dans deux tableaux qui devaient rester alignés. Avec les records de la leçon 6, chaque projet est un seul objet avec un nom et un langage, et LINQ répond à chaque question en une ligne :
// Les douze projets de GuitarAlchemist/ga de la leçon 4, en une seule liste de records, interrogée avec LINQList<Project> projects =[ new Project("GA.Core", "C#"), new Project("GA.Domain.Core", "C#"), new Project("GA.Business.Config", "F#"), new Project("GA.Business.Core", "C#"), new Project("GA.Business.DSL", "F#"), new Project("GA.Business.AI", "C#"), new Project("GA.Business.ML", "C#"), new Project("GA.Business.ProbabilisticGrammar", "F#"), new Project("GA.Business.Core.Generated", "F#"), new Project("GA.Infrastructure", "C#"), new Project("GA.Presentation", "C#"), new Project("GA.Testing.Semantic", "C#"),];
int business = projects.Count(p => p.Name.StartsWith("GA.Business."));Console.WriteLine($"{business} projects start with GA.Business.");
IEnumerable<string> fsharp = projects.Where(p => p.Language == "F#").Select(p => p.Name);Console.WriteLine($"F#: {string.Join(", ", fsharp)}");
Project longest = projects.OrderByDescending(p => p.Name.Length).First();Console.WriteLine($"Longest name: {longest.Name}");
Console.WriteLine("Sorted by language, then by name:");foreach (Project project in projects.OrderBy(p => p.Language).ThenBy(p => p.Name).Take(4)){ Console.WriteLine($" {project.Language} {project.Name}");}
Console.WriteLine($"Any F#? {projects.Any(p => p.Language == "F#")}");Console.WriteLine($"All start with GA.? {projects.All(p => p.Name.StartsWith("GA."))}");
Project? rust = projects.FirstOrDefault(p => p.Language == "Rust");Console.WriteLine(rust?.Name ?? "no Rust project");
List<string> csharp = projects.Where(p => p.Language == "C#").Select(p => p.Name).ToList();Console.WriteLine($"{csharp.Count} C# projects, the first is {csharp[0]}");
record Project(string Name, string Language);7 projects start with GA.Business.F#: GA.Business.Config, GA.Business.DSL, GA.Business.ProbabilisticGrammar, GA.Business.Core.GeneratedLongest name: GA.Business.ProbabilisticGrammarSorted by language, then by name: C# GA.Business.AI C# GA.Business.Core C# GA.Business.ML C# GA.CoreAny F#? TrueAll start with GA.? Trueno Rust project8 C# projects, the first is GA.CoreLes réponses sont celles de la leçon 4, sans ses trois méthodes ni leurs boucles. Une chaîne se lit de gauche à droite, chaque méthode travaillant sur ce que la précédente a renvoyé :
flowchart LR A["projects<br/>12 Project"] -->|"Where<br/>Language vaut F#"| B["4 Project"] B -->|"Select<br/>Name"| C["4 string"] C -->|"string.Join"| D["une ligne de texte"]| Méthode | Renvoie | Ici |
|---|---|---|
Count |
combien d’éléments correspondent | 7 projets commencent par GA.Business. |
Where |
les éléments qui correspondent | les projets F# |
Select |
une nouvelle valeur par élément | le nom de chaque projet |
OrderBy, OrderByDescending, ThenBy |
les éléments, triés selon une clé ; ThenBy départage les égalités |
par langage, puis par nom |
First, FirstOrDefault |
le premier élément (qui correspond) | le nom le plus long ; aucun projet Rust |
Any, All |
si un élément, ou tous les éléments, correspondent | True, True |
Take |
les n premiers éléments | quatre lignes seulement |
ToList |
une nouvelle List<T> avec les éléments |
les projets C# |
First lève une InvalidOperationException quand rien ne correspond ; FirstOrDefault renvoie null à la place, donc son résultat est un Project?, et ?. et ??, vus à la leçon 8, traitent le projet manquant. OrderBy effectue un tri stable, dit sa documentation : deux projets qui ont la même clé gardent leur ordre, et ThenBy les départage.
Where et Select renvoient un IEnumerable<T>, une séquence qu’on peut lire avec foreach, pas une List<T>. Mettre le résultat dans une variable de type liste est une erreur, et ToList la corrige :
List<string> names = ["GA.Core", "GA.Business.DSL", "GA.Business.AI"];List<string> business = names.Where(n => n.StartsWith("GA.Business."));l09_where_is_not_a_list.cs(2,25): error CS0266: Cannot implicitly convert type 'System.Collections.Generic.IEnumerable<string>' to 'System.Collections.Generic.List<string>'. An explicit conversion exists (are you missing a cast?)La lambda donnée à Where doit renvoyer un bool, « garder ou non ». Une lambda qui renvoie un nombre reçoit deux erreurs au même endroit :
List<string> names = ["GA.Core", "GA.Business.DSL", "GA.Business.AI"];var longNames = names.Where(n => n.Length);l09_lambda_not_bool.cs(2,34): error CS0029: Cannot implicitly convert type 'int' to 'bool'l09_lambda_not_bool.cs(2,34): error CS1662: Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return typen => n.Length > 20 renvoie un bool et compile.
Une requête s’exécute quand on la lit
Section intitulée « Une requête s’exécute quand on la lit »Where ne calcule pas son résultat quand la ligne s’exécute. Il renvoie une requête qui le calcule chaque fois que quelque chose la lit, avec un foreach, string.Join ou ToList :
// Une requête s'exécute quand on la lit, pas quand on l'écrit ; ToList garde le résultat d'une exécutionList<int> frets = [0, 3, 5, 7];
IEnumerable<int> high = frets.Where(f => f >= 5);List<int> highNow = frets.Where(f => f >= 5).ToList();
frets.Add(12);
Console.WriteLine($"query: {string.Join(" ", high)}");Console.WriteLine($"ToList: {string.Join(" ", highNow)}");query: 5 7 12ToList: 5 7La frette 12 a été ajoutée après les deux lignes, et la requête la trouve quand même, parce que string.Join l’a exécutée après le Add. ToList l’a exécutée une fois, avant le Add, et a gardé ce résultat. C’est ce qu’on appelle l’exécution différée. Appelle ToList quand tu veux le résultat tel qu’il est maintenant, ou quand tu vas le lire plusieurs fois.
Ne modifie pas une collection dans son propre foreach
Section intitulée « Ne modifie pas une collection dans son propre foreach »Un foreach sur une liste s’arrête avec une exception si la liste change pendant la boucle :
// Retirer des éléments d'une liste dans un foreach sur cette même listeList<string> strings = ["E2", "A2", "D3", "G3", "B3", "E4"];
foreach (string s in strings){ if (s.StartsWith("E")) { strings.Remove(s); }}
Console.WriteLine(string.Join(" ", strings));Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.Le foreach ne peut pas savoir quel élément vient ensuite une fois que la liste a déplacé ses éléments, donc il refuse de continuer. L’exercice 3 retire les cordes de mi de deux façons qui fonctionnent.
Un dictionnaire fait exception à la règle. Depuis .NET Core 3.0, sa documentation dit que Remove « may be safely called without invalidating active enumerators » (peut être appelée sans risque sans invalider les énumérateurs actifs). Ajouter une clé arrête quand même la boucle :
// Un dictionnaire permet Remove dans son propre foreach, mais pas l'ajout d'une clévar semitones = new Dictionary<string, int> { ["C"] = 0, ["D"] = 2, ["E"] = 4 };
foreach (var (name, value) in semitones){ if (name == "D") { semitones.Remove(name); }}Console.WriteLine($"after Remove: {string.Join(" ", semitones.Keys)}");
try{ foreach (var (name, value) in semitones) { if (name == "C") { semitones["B"] = 11; } }}catch (InvalidOperationException ex){ Console.WriteLine($"adding a key: {ex.Message}");}after Remove: C Eadding a key: Collection was modified; enumeration operation may not execute.Dans Guitar Alchemist
Section intitulée « Dans Guitar Alchemist »Au commit 5c3a52a, Guitar Alchemist utilise les trois collections de cette leçon, et LINQ, dans du code qu’un débutant peut lire.
Un dictionnaire qui règle le problème de do dièse et ré bémol. ChordVocabulary.PitchClasses associe chaque façon d’écrire une note à sa classe de hauteur, son nombre de demi-tons au-dessus de C, de 0 à 11, comme le premier dictionnaire de cette leçon. "C#" et "Db" donnent tous deux 1 : un programme qui transforme les noms en nombres avant de les comparer traite les deux comme la même note. Le dictionnaire est créé avec StringComparer.OrdinalIgnoreCase, qui permet à "c#" de trouver la clé "C#", et TryGetPitchClass le lit avec TryGetValue, parce qu’un utilisateur peut taper une note qui n’existe pas. Les remarques de la classe disent pourquoi elle existe : deux copies de la table « had drifted in a load-bearing way » (avaient divergé sur un point dont le code dépendait). À ce commit, 18 fichiers de GA associent "Db" à 1 dans une table à eux, celle-ci comprise.
Un ensemble qui garde ses notes dans l’ordre. PitchClassSet, l’ensemble de notes de GA, implémente IReadOnlySet<PitchClass>, l’interface d’un ensemble qu’on ne peut pas modifier, et range ses notes dans un ImmutableSortedSet (ligne 37). Contrairement à un HashSet, il donne toujours ses notes dans l’ordre croissant, de 0 à 11, et deux ensembles qui ont les mêmes notes sont égaux (ligne 780).
Un tri qu’un dictionnaire garde par hasard. MusicalKnowledgeService compte les entrées de chaque artiste dans les données musicales de GA. La ligne 226 contient foreach (var artist in GetAllArtists().Take(20)) // Top 20 artists, et la ligne 235 renvoie breakdown.OrderByDescending(kvp => kvp.Value).ToDictionary(kvp => kvp.Key, kvp => kvp.Value). Deux points de cette leçon s’y rencontrent :
GetAllArtistsse termine parOrderBy(a => a)(ligne 125) : les artistes arrivent par ordre alphabétique, doncTake(20)garde les 20 premiers de l’alphabet, pas les 20 qui ont le plus d’entrées. Le tri par nombre d’entrées vient après la coupe.ToDictionaryrange le résultat trié dans un dictionnaire, dont l’ordre n’est pas promis. Il ressort trié parce que rien n’en est retiré, comme le montre l’expérience de cette leçon ; une liste garderait l’ordre à coup sûr.
Une sonde a exécuté ce code à ce commit et a trouvé autre chose : 16 artistes seulement, donc Take(20) n’en écarte encore aucun. Trois des quatre fichiers YAML que lit le service ne se chargent pas :
- dans
ChordProgressions.yaml,Functionest une seule chaîne là où la classe C# attend uneList<string>(ligne 16) ; - dans
GuitarTechniques.yaml,Applicationscontient des chaînes là où la classe attend des objets (ligne 20) ; - la ligne 95 de
SpecializedTunings.yamln’est pas du YAML valide.
Chaque chargeur attrape l’exception, affiche une ligne, et continue avec un seul élément d’exemple. C’est l’inverse de la règle de la leçon 8 : ce catch cache un bogue au lieu de traiter un échec ordinaire. Le tableau QA du journal consigne les mesures ; la leçon 10 lit des fichiers.
Exercices
Section intitulée « Exercices »Exercice 1 — prédis la sortie
Section intitulée « Exercice 1 — prédis la sortie »Note ce qu’affiche chaque ligne, puis exécute exercises/l09_ex_predict.cs :
// Exercice 1 : note ce qu'affiche chaque ligne, puis exécute le programmeHashSet<string> notes = ["C", "E", "G"];Console.WriteLine(notes.Add("E"));Console.WriteLine(notes.Count);
var capos = new Dictionary<string, int> { ["Here Comes the Sun"] = 7 };capos["Blackbird"] = 0;capos["Here Comes the Sun"] = 2;Console.WriteLine(capos.Count);Console.WriteLine(capos["Here Comes the Sun"]);
List<int> frets = [3, 0, 12, 5];IEnumerable<int> sorted = frets.OrderBy(f => f);frets.Add(1);Console.WriteLine(string.Join(" ", sorted));Solution
False3220 1 3 5 12Add("E")renvoieFalse: E est déjà dans l’ensemble, qui garde 3 notes.capos["Here Comes the Sun"] = 2remplace la valeur d’une clé présente ; il n’ajoute rien, donc le dictionnaire a 2 clés, et la valeur vaut maintenant 2.OrderByrenvoie une requête, qui s’exécute quandstring.Joinla lit, après leAdd: le 1 est dans le résultat.
Exercice 2 — les notes les plus fréquentes d’abord
Section intitulée « Exercice 2 — les notes les plus fréquentes d’abord »Pars de examples/l09_count_notes.cs et affiche les comptes de la note la plus fréquente à la moins fréquente ; les notes qui ont le même compte vont dans l’ordre alphabétique. Utilise OrderByDescending et ThenBy sur le dictionnaire : chaque élément est une KeyValuePair, avec Key et Value.
Solution
// Exercice 2 : les notes de la mélodie, la plus fréquente d'abord ; à compte égal, dans l'ordre alphabétiquestring[] melody = ["E", "E", "F", "G", "G", "F", "E", "D", "C", "C", "D", "E", "E", "D", "D"];
var counts = new Dictionary<string, int>();foreach (string note in melody){ counts[note] = counts.GetValueOrDefault(note) + 1;}
foreach (var (note, count) in counts.OrderByDescending(pair => pair.Value).ThenBy(pair => pair.Key)){ Console.WriteLine($"{note}: {count}");}E: 5D: 4C: 2F: 2G: 2Sans ThenBy, C, F et G garderaient l’ordre du dictionnaire, parce que le tri est stable, et cet ordre n’est pas promis. ThenBy(pair => pair.Key) rend la sortie identique quoi que fasse le dictionnaire.
Exercice 3 — retirer les cordes de mi
Section intitulée « Exercice 3 — retirer les cordes de mi »Corrige examples/l09_modify_while_looping.cs pour qu’il affiche A2 D3 G3 B3 sans modifier la liste dans son propre foreach. Trouve deux façons : une qui modifie la liste, avec List<T>.RemoveAll, et une qui construit une nouvelle liste avec LINQ et garde l’originale.
Solution
// Exercice 3 : retirer les cordes de mi sans modifier la liste dans son propre foreachList<string> strings = ["E2", "A2", "D3", "G3", "B3", "E4"];
// RemoveAll reçoit une lambda et retire chaque élément pour lequel elle renvoie trueList<string> first = new List<string>(strings);first.RemoveAll(s => s.StartsWith("E"));Console.WriteLine(string.Join(" ", first));
// Ou construire une nouvelle liste avec Where, et garder l'originaleList<string> second = strings.Where(s => !s.StartsWith("E")).ToList();Console.WriteLine(string.Join(" ", second));Console.WriteLine(string.Join(" ", strings));A2 D3 G3 B3A2 D3 G3 B3E2 A2 D3 G3 B3 E4RemoveAll fait lui-même sa boucle sur la liste et déplace les éléments qu’il garde, donc aucun foreach n’est en cours quand la liste change. Where lit la liste sans la modifier, et ToList copie ce que Where garde dans une nouvelle liste.
Exercice 4 — les accords de do majeur
Section intitulée « Exercice 4 — les accords de do majeur »Les six accords construits sur les notes de do majeur sont dans un dictionnaire dont les valeurs sont des ensembles :
// Exercice 4 : les six accords de do majeur, chacun un ensemble de notesvar chords = new Dictionary<string, HashSet<string>>{ ["C"] = ["C", "E", "G"], ["Dm"] = ["D", "F", "A"], ["Em"] = ["E", "G", "B"], ["F"] = ["F", "A", "C"], ["G"] = ["G", "B", "D"], ["Am"] = ["A", "C", "E"],};- Affiche les accords qui contiennent à la fois E et G.
- Affiche les autres accords, de celui qui a le plus de notes en commun avec C à celui qui en a le moins, puis par nom, avec le nombre de notes qu’ils partagent.
Solution
// 1. Les accords qui contiennent à la fois E et GIEnumerable<string> withEAndG = chords .Where(pair => pair.Value.Contains("E") && pair.Value.Contains("G")) .Select(pair => pair.Key);Console.WriteLine($"E and G: {string.Join(" ", withEAndG)}");
// 2. Les autres accords, selon le nombre de notes qu'ils partagent avec C, puis par nomvar others = chords .Where(pair => pair.Key != "C") .OrderByDescending(pair => CommonWithC(pair.Value)) .ThenBy(pair => pair.Key);foreach (var (name, notes) in others){ Console.WriteLine($"{name}: {CommonWithC(notes)} in common with C");}
int CommonWithC(HashSet<string> notes) => notes.Count(note => chords["C"].Contains(note));E and G: C EmAm: 2 in common with CEm: 2 in common with CF: 1 in common with CG: 1 in common with CDm: 0 in common with CUne chaîne de méthodes LINQ peut s’étendre sur plusieurs lignes, chacune commençant par son .. CommonWithC est une méthode du même genre que celles de la leçon 4, écrite avec => comme à la leçon 8 ; le Count de LINQ fonctionne sur un ensemble comme sur une liste. La mineur (Am) et mi mineur (Em) ont deux notes en commun avec do majeur (C), c’est pourquoi une chanson peut souvent utiliser l’un à la place de l’autre.
Ce qu’il faut retenir
Section intitulée « Ce qu’il faut retenir »- Un
Dictionary<TKey, TValue>trouve une valeur par sa clé.dict[key]lèveKeyNotFoundExceptionquand la clé manque ;TryGetValuerenvoiefalse.dict[key] = valueajoute ou remplace ;Addlève une exception pour une clé déjà présente. - L’ordre d’un
foreachsur un dictionnaire n’est pas promis : après unRemove, une nouvelle clé peut prendre la place de celle qui a été retirée. Trie quand l’ordre compte. - Un
HashSet<T>contient chaque valeur une seule fois, n’a pas de positions, et compare des contenus avecSetEquals.IntersectWith,UnionWithetExceptWithmodifient l’ensemble lui-même. - Une lambda
x => ...est une méthode sans nom. Les méthodesWhere,Select,OrderBy,Count,AnyetFirstde LINQ en reçoivent une et fonctionnent sur toutes les collections. WhereetSelectrenvoient unIEnumerable<T>qui s’exécute quand on le lit ;ToListfait une liste du résultat tel qu’il est maintenant.- N’ajoute ni ne retire d’éléments d’une liste dans un
foreachsur cette liste : utiliseRemoveAll, ou construis une nouvelle liste.
La suite, fichiers et texte, lira les projets de Guitar Alchemist dans un fichier CSV au lieu de les recopier à la main.
- Collections :
Dictionary<TKey, TValue>,Add,ContainsKey,TryGetValue,Remove,KeyValuePair<TKey, TValue>,GetValueOrDefault,HashSet<T>,List<T>.RemoveAll,IEnumerable<T> - Exceptions :
ArgumentException,KeyNotFoundException,InvalidOperationException - LINQ : présentation, expressions lambda, exécution différée, les méthodes d’
Enumerable - Erreurs du compilateur CS1503, CS0021, CS0266, CS0029 et CS1662