9. Collections and LINQ
Sections
Reading settings
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
The List<T> of lesson 4 keeps its items in order and finds them by position. Two other collections answer other questions. A dictionary answers “what is the value for this name?”, and a set answers “is this value in the group?”. The second half of the lesson introduces LINQ, a set of methods that count, filter and sort a collection in one line each. With them, the loops that lesson 4 wrote by hand for Guitar Alchemist’s projects become single lines.
All the programs of this lesson are in code/csharp-beginner; run one with dotnet run followed by its path, for example examples/l09_dictionary.cs. check.sh compares their output, and the compiler errors of the rejected snippets, with the files in expected/.
Dictionary: a value for each key
Section titled “Dictionary: a value for each key”A Dictionary<TKey, TValue> stores pairs: a key, and the value that goes with it. Each key appears only once. Its two types go between the angle brackets: Dictionary<string, int> has string keys and int values. Here the key is a note’s name and the value is its number of semitones above C, the table that lesson 3 wrote as a switch:
// A Dictionary<string, int>: each note's name (the key) gives its number of semitones above C (the value)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; // a key that isn't there yet: addedsemitones.Add("Bb", 10); // Add also adds...try{ semitones.Add("C", 0); // ...but refuses a key that is already there}catch (ArgumentException ex){ Console.WriteLine(ex.Message);}Console.WriteLine($"{semitones.Count} notes");
Console.WriteLine(semitones.ContainsKey("Bb"));Console.WriteLine(semitones.ContainsKey("H")); // H is B in German notation, not a key here
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)); // each item is a 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"] = 0between the braces puts the value 0 under the key"C"when the dictionary is created.semitones["G"]reads the value of a key, with the same brackets as an array, but with a key instead of a position.semitones["F#"] = 6adds the key if it isn’t there, and replaces its value if it is.Addadds a key too, but throws anArgumentExceptionif the key is already there: thecatchof lesson 8 prints its message.ContainsKeysays whether a key is there.TryGetValueis a Try method, like those of lesson 8: it returnsfalsewhen the key is missing, and puts the value in itsoutvariable when it is there.- Each item of a dictionary is a
KeyValuePair<string, int>, with aKeyand aValue;string.Joinprints it as[key, value].
A dictionary holds the table as data, not as code: a program can fill it while it runs, and lesson 10 will fill one from a file.
A dictionary is read by key, not by position. Asking for item 0 of a dictionary whose keys are strings is a type error:
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'A missing key throws an exception
Section titled “A missing key throws an exception”Reading a key that isn’t in the dictionary doesn’t return 0 or null, it throws:
// Reading a key that isn't in the dictionary throws an 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.The rule of lesson 8 applies. When a missing key is a bug in the program, the indexer semitones[note] and its KeyNotFoundException are right: the program stops where the bug is. When a missing key is ordinary, like a note typed by a user, use TryGetValue.
Counting with a dictionary
Section titled “Counting with a dictionary”A dictionary can count: the key is what you count, the value is how many times you have seen it.
// How many times each note appears in the first phrase of Beethoven's Ode to Joystring[] 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 the first time}
foreach (var (note, count) in counts){ Console.WriteLine($"{note}: {count}");}E: 5F: 2G: 2D: 4C: 2GetValueOrDefault returns the value of the key, or 0, the default value of an int, when the key isn’t there yet. The first E therefore stores 0 + 1, and each later E replaces the count with one more. foreach (var (note, count) in counts) takes each KeyValuePair apart into two variables, its key and its value.
Don’t count on the order of a dictionary
Section titled “Don’t count on the order of a dictionary”The notes came out in the order of their first appearance: E, F, G, D, C. The documentation of Dictionary doesn’t promise it: “The order in which the items are returned is undefined.” Here is what .NET 10 does when a key is removed, then another added:
// The order of a foreach over a dictionary: remove a key, add anothervar 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 AF# was added last but comes second, in the place D left. As long as a program only adds keys, the order looks like the order of addition; one Remove is enough to break that. When the order matters, sort: OrderBy, below, does it, and exercise 2 sorts the counts of the melody. The journal records this experiment, with the hypothesis written before running it.
HashSet: each value once
Section titled “HashSet: each value once”A HashSet<T> is a set: it holds each value at most once, and it answers “is this value in it?” quickly. The notes of a chord are a set: C major is C, E and G, whatever their order, and adding a second E doesn’t change the chord.
// A HashSet<string> holds each value once: the notes of a chordHashSet<string> cMajor = ["C", "E", "G"];HashSet<string> aMinor = ["A", "C", "E"];
Console.WriteLine(cMajor.Add("E")); // False: E is already thereConsole.WriteLine(cMajor.Add("B")); // True: C E G B is Cmaj7Console.WriteLine($"{cMajor.Count} notes, contains G: {cMajor.Contains("G")}");cMajor.Remove("B");
// IntersectWith, UnionWith and ExceptWith change the set they are called on: work on a copyvar 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: same notes, written in another order
HashSet<string> cSharp = ["C#", "F", "G#"];HashSet<string> dFlat = ["Db", "F", "Ab"];Console.WriteLine(cSharp.SetEquals(dFlat)); // False: the strings differ, the sounds don'tFalseTrue4 notes, contains G: Truein both chords: C Ein either chord: C E G Aonly in C major: GTrueFalseAddreturnsfalsewhen the value is already in the set, and changes nothing.IntersectWithkeeps the values that are also in the other set: C and E, the two notes that C major and A minor share.UnionWithadds the values of the other set, andExceptWithremoves them. These three change the set itself, so the program works on copies made withnew HashSet<string>(cMajor).SetEqualscompares the contents and ignores the order.
The last line is a limit of this program, not of HashSet. C♯ and D♭ are the same key of the piano, but "C#" and "Db" are different strings, so the two sets differ. A program that must treat them as the same note compares numbers, the semitones of the dictionary above, rather than names.
A set has no positions. Its documentation says it “is not sorted and cannot contain duplicate elements”, and it has no indexer:
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> |
|
|---|---|---|---|
| Holds | items in order | one value per key | each value once |
| Find an item by | position, list[2] |
key, dict["G"] |
value, set.Contains("G") |
| A duplicate | is kept | key: Add throws, [key] = replaces |
Add returns false |
| Use it when | the order matters | you look things up by name | you ask “is it in?” |
LINQ: count, filter and sort in one line
Section titled “LINQ: count, filter and sort in one line”Lesson 4 wrote a method with a loop for each question about GA’s projects: how many start with GA.Business., which are in F#, which name is the longest. LINQ (Language-Integrated Query) is a set of methods that every collection has: arrays, lists, dictionaries and sets. Each method takes a small piece of code that says what to look for.
Lambdas
Section titled “Lambdas”That piece of code is a lambda expression, a method without a name, written where it is used. Project is a record of lesson 6, with a name and a language:
// A lambda and a named method that do the same workList<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);22In p => p.Language == "F#", left of => is the lambda’s parameter, p; right of it, the value it returns, here a bool. Count calls it once for each project and counts the true answers. The method IsFSharp, written with => as in lesson 8, does the same work, and Count accepts it too; the lambda saves giving a name to code used in one place. The compiler works out the type of p from the collection: in a List<Project>, p is a Project.
Where, Select, OrderBy and the others
Section titled “Where, Select, OrderBy and the others”Lesson 4 kept the projects in two arrays that had to stay in step. With the records of lesson 6, each project is one object with a name and a language, and LINQ answers each question in one line:
// Lesson 4's twelve projects of GuitarAlchemist/ga, as one list of records, queried with 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.CoreThe answers are those of lesson 4, without its three methods and their loops. A chain reads from left to right, each method working on what the previous one returned:
flowchart LR A["projects<br/>12 Project"] -->|"Where<br/>Language is F#"| B["4 Project"] B -->|"Select<br/>Name"| C["4 string"] C -->|"string.Join"| D["one line of text"]| Method | Returns | Here |
|---|---|---|
Count |
how many items match | 7 projects start with GA.Business. |
Where |
the items that match | the F# projects |
Select |
one new value per item | the name of each project |
OrderBy, OrderByDescending, ThenBy |
the items, sorted by a key; ThenBy breaks ties |
by language, then by name |
First, FirstOrDefault |
the first item (that matches) | the longest name; no Rust project |
Any, All |
whether one item, or every item, matches | True, True |
Take |
the first n items | four lines only |
ToList |
a new List<T> with the items |
the C# projects |
First throws an InvalidOperationException when nothing matches; FirstOrDefault returns null instead, so its result is a Project?, and ?. and ??, from lesson 8, handle the missing project. OrderBy performs a stable sort, its documentation says: two projects with the same key keep their order, and ThenBy decides between them.
Where and Select return an IEnumerable<T>, a sequence that can be read with foreach, not a List<T>. Putting the result in a list variable is an error, and ToList is the fix:
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?)The lambda given to Where must return a bool, “keep it or not”. A lambda that returns a number gets two errors on the same spot:
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 returns a bool and compiles.
A query runs when it is read
Section titled “A query runs when it is read”Where doesn’t compute its result when the line runs. It returns a query that computes it each time something reads it, with a foreach, string.Join or ToList:
// A query runs when it is read, not when it is written; ToList keeps the result of one runList<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 7The fret 12 was added after both lines, and the query still finds it, because string.Join ran it after the Add. ToList ran it once, before the Add, and kept that result. This is called deferred execution. Call ToList when you want the result as it is now, or when you will read it several times.
Don’t change a collection inside its own foreach
Section titled “Don’t change a collection inside its own foreach”A foreach over a list stops with an exception if the list changes during the loop:
// Removing items from a list inside a foreach over that same listList<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.The foreach can’t know which item comes next once the list has moved its items, so it refuses to go on. Exercise 3 removes the E strings in two ways that work.
A dictionary is an exception to the rule. Since .NET Core 3.0, its documentation says that Remove “may be safely called without invalidating active enumerators”. Adding a key still stops the loop:
// A dictionary allows Remove inside its own foreach, but not adding a keyvar 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.In Guitar Alchemist
Section titled “In Guitar Alchemist”At commit 5c3a52a, Guitar Alchemist uses the three collections of this lesson, and LINQ, in code a beginner can read.
A dictionary that solves the C♯ and D♭ problem. ChordVocabulary.PitchClasses maps each spelling of a note to its pitch class, its number of semitones above C, from 0 to 11, like this lesson’s first dictionary. "C#" and "Db" both give 1: a program that turns names into numbers before comparing them treats the two as the same note. The dictionary is created with StringComparer.OrdinalIgnoreCase, which makes "c#" find the key "C#", and TryGetPitchClass reads it with TryGetValue, because a user may type a note that doesn’t exist. The class’s remarks say why it exists: two copies of the table “had drifted in a load-bearing way”. At that commit, 18 files of GA map "Db" to 1 in a table of their own, this one included.
A set that keeps its notes in order. PitchClassSet, GA’s set of notes, implements IReadOnlySet<PitchClass>, the interface of a set that can’t be changed, and keeps its notes in an ImmutableSortedSet (line 37). Unlike a HashSet, it always lists its notes in ascending order, from 0 to 11, and two sets with the same notes are equal (line 780).
A sort that a dictionary keeps by chance. MusicalKnowledgeService counts the entries of each artist in GA’s music data. Line 226 reads foreach (var artist in GetAllArtists().Take(20)) // Top 20 artists, and line 235 returns breakdown.OrderByDescending(kvp => kvp.Value).ToDictionary(kvp => kvp.Key, kvp => kvp.Value). Two points of this lesson meet there:
GetAllArtistsends withOrderBy(a => a)(line 125): the artists come in alphabetical order, soTake(20)keeps the first 20 of the alphabet, not the 20 with the most entries. The sort by count comes after the cut.ToDictionaryputs the sorted result in a dictionary, whose order isn’t promised. It comes out sorted because nothing is removed from it, as this lesson’s experiment shows; a list would keep the order for sure.
A probe ran this code at that commit and found something else: 16 artists only, so Take(20) leaves none out yet. Three of the four YAML files that the service reads don’t load:
- in
ChordProgressions.yaml,Functionis one string where the C# class expects aList<string>(line 16); - in
GuitarTechniques.yaml,Applicationsholds strings where the class expects objects (line 20); - line 95 of
SpecializedTunings.yamlisn’t valid YAML.
Each loader catches the exception, prints one line, and carries on with a single example item. That is the opposite of lesson 8’s rule: this catch hides a bug instead of handling an ordinary failure. The QA table of the journal records the measurements; lesson 10 reads files.
Exercises
Section titled “Exercises”Exercise 1 — predict the output
Section titled “Exercise 1 — predict the output”Write down what each line prints, then run exercises/l09_ex_predict.cs:
// Exercise 1: write down what each line prints, then run the programHashSet<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")returnsFalse: E is already in the set, which keeps 3 notes.capos["Here Comes the Sun"] = 2replaces the value of a key that is there; it adds nothing, so the dictionary has 2 keys, and the value is now 2.OrderByreturns a query, which runs whenstring.Joinreads it, after theAdd: the 1 is in the result.
Exercise 2 — the most frequent notes first
Section titled “Exercise 2 — the most frequent notes first”Start from examples/l09_count_notes.cs and print the counts from the most frequent note to the least frequent; notes with the same count go in alphabetical order. Use OrderByDescending and ThenBy on the dictionary: each item is a KeyValuePair, with Key and Value.
Solution
// Exercise 2: the notes of the melody, the most frequent first; equal counts in alphabetical orderstring[] 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: 2Without ThenBy, C, F and G would keep the dictionary’s order, because the sort is stable, and that order isn’t promised. ThenBy(pair => pair.Key) makes the output the same whatever the dictionary does.
Exercise 3 — remove the E strings
Section titled “Exercise 3 — remove the E strings”Fix examples/l09_modify_while_looping.cs so that it prints A2 D3 G3 B3 without changing the list inside its own foreach. Find two ways: one that changes the list, with List<T>.RemoveAll, and one that builds a new list with LINQ and keeps the original.
Solution
// Exercise 3: remove the E strings without changing the list inside its own foreachList<string> strings = ["E2", "A2", "D3", "G3", "B3", "E4"];
// RemoveAll takes a lambda and removes every item for which it returns trueList<string> first = new List<string>(strings);first.RemoveAll(s => s.StartsWith("E"));Console.WriteLine(string.Join(" ", first));
// Or build a new list with Where, and keep the originalList<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 loops over the list itself and moves the items it keeps, so no foreach is running when the list changes. Where reads the list without changing it, and ToList copies what it keeps into a new list.
Exercise 4 — the chords of C major
Section titled “Exercise 4 — the chords of C major”The six chords built on the notes of C major are in a dictionary whose values are sets:
// Exercise 4: the six chords of C major, each a set of 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"],};- Print the chords that contain both E and G.
- Print the other chords, from the one that shares the most notes with C to the one that shares the fewest, then by name, with the number of notes they share.
Solution
// 1. The chords that contain both E and 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. The other chords, by the number of notes they share with C, then by namevar 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 CA chain of LINQ methods can go on several lines, each starting with its .. CommonWithC is a method of lesson 4’s kind, written with => as in lesson 8; LINQ’s Count works on a set like on a list. Am and Em share two notes with C, which is why a song can often use one in place of the other.
Check your understanding
Section titled “Check your understanding”- A
Dictionary<TKey, TValue>finds a value by its key.dict[key]throwsKeyNotFoundExceptionwhen the key is missing;TryGetValuereturnsfalse.dict[key] = valueadds or replaces;Addthrows on a key that is already there. - The order of a
foreachover a dictionary isn’t promised: after aRemove, a new key can take the place of the removed one. Sort when the order matters. - A
HashSet<T>holds each value once, has no positions, and compares contents withSetEquals.IntersectWith,UnionWithandExceptWithchange the set itself. - A lambda
x => ...is a method without a name. LINQ’sWhere,Select,OrderBy,Count,AnyandFirsttake one and work on every collection. WhereandSelectreturn anIEnumerable<T>that runs when it is read;ToListmakes a list of the result as it is now.- Don’t add or remove items of a list inside a
foreachover it: useRemoveAll, or build a new list.
Next: files and text, to read Guitar Alchemist’s projects from a CSV file instead of copying them by hand.
Sources
Section titled “Sources”- Collections:
Dictionary<TKey, TValue>,Add,ContainsKey,TryGetValue,Remove,KeyValuePair<TKey, TValue>,GetValueOrDefault,HashSet<T>,List<T>.RemoveAll,IEnumerable<T> - Exceptions:
ArgumentException,KeyNotFoundException,InvalidOperationException - LINQ: overview, lambda expressions, deferred execution,
Enumerable’s methods - Compiler errors CS1503, CS0021, CS0266, CS0029, CS1662