4. Methods, arrays and lists
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.
Code: the examples examples/l04_*.cs, the rejected snippets compile_fail/l04_*.cs and the solutions exercises/l04_*.cs.
The programs of lesson 3 repeated the same twelve note names in several places. This lesson removes that repetition twice: a method gives a name to a piece of work, so you write it once and call it many times, and an array or a list holds many values under one name.
Methods
Section titled “Methods”You’ve been calling methods since lesson 1: Console.WriteLine, Math.Pow, int.TryParse. Now you write your own.
// Calling the methods declared belowPrintTitle("Methods");Console.WriteLine(Square(12));Console.WriteLine(FretFrequency(110.0, 7));Console.WriteLine(FretFrequency(82.41, 5));Console.WriteLine(Describe(0));Console.WriteLine(Describe(12));Console.WriteLine(Repeat("la")); // default value for timesConsole.WriteLine(Repeat("la", 3));Console.WriteLine(Repeat(times: 2, text: "do")); // named arguments, in any order
// A method with no result: its return type is voidvoid PrintTitle(string title){ Console.WriteLine($"== {title} ==");}
// A method that returns an intint Square(int x){ return x * x;}
// A method with two parameters, rounded to two decimalsdouble FretFrequency(double openString, int fret){ double frequency = openString * Math.Pow(2, fret / 12.0); return Math.Round(frequency, 2);}
// return leaves the method at oncestring Describe(int fret){ if (fret == 0) { return "open string"; } return $"fret {fret}";}
// An optional parameter, and a body written as one expression with =>string Repeat(string text, int times = 2) => string.Concat(Enumerable.Repeat(text, times));== Methods ==144164.81110open stringfret 12lalalalaladodoA method declaration has four parts:
double FretFrequency (double openString, int fret) { … return …; } │ │ │ │return name parameters body type- The return type is the type of the result:
int,double,string… orvoidwhen the method returns nothing and only does something, like printing. - The name starts with a capital letter, by convention, and usually with a verb:
PrintTitle,Describe. - The parameters are variables that receive the values given by the caller, the arguments.
FretFrequency(110.0, 7)puts110.0inopenStringand7infret, in that order. - The body does the work.
returngives the result back to the caller and leaves the method at once: inDescribe(0), the secondreturnis never reached.
Fret 5 of the low E string, at 82.41 Hz, is an A at 110 Hz: the note of the open A string. That’s how guitarists tune the fifth string from the sixth.
Three conveniences:
- A parameter with a default value,
int times = 2, can be left out:Repeat("la")uses 2. - Named arguments,
times: 2, text: "do", say which parameter gets which value, in any order. - When the body is a single expression,
=>replaces the braces and thereturn: that’s an expression-bodied member.Enumerable.Repeat(text, times)makes a sequence oftimescopies oftext, andstring.Concatjoins them.
In a file with top-level statements, the methods can be declared after the lines that call them, as here. The C# documentation calls them local functions: they belong to the program’s top-level code. Lesson 5 puts methods inside classes, the usual place in larger programs.
What the compiler checks
Section titled “What the compiler checks”The compiler checks every call against the declaration. A missing argument:
Console.WriteLine(FretFrequency(110.0));
double FretFrequency(double openString, int fret)l04_missing_argument.cs(1,19): error CS7036: There is no argument given that corresponds to the required parameter 'fret' of 'FretFrequency(double, int)'An argument of the wrong type:
Console.WriteLine(Square("12"));
int Square(int x)l04_wrong_argument_type.cs(1,26): error CS1503: Argument 1: cannot convert from 'string' to 'int'Using the result of a void method:
string title = PrintTitle("Methods");
void PrintTitle(string text)l04_void_result.cs(1,16): error CS0029: Cannot implicitly convert type 'void' to 'string'And a method that may end without returning a value. Here, a negative fret goes past both return statements:
string Describe(int fret){ if (fret == 0) { return "open string"; } else if (fret > 0) { return $"fret {fret}"; }}l04_not_all_paths.cs(3,8): error CS0161: 'Describe(int)': not all code paths return a valueThe compiler doesn’t try to guess that fret is never negative: every path through the method must end with a return or an exception.
Arguments are copies
Section titled “Arguments are copies”int fret = 5;AddOctave(fret);Console.WriteLine($"After AddOctave: {fret}"); // unchanged: the method got a copy
int[] frets = [0, 2, 2, 1, 0, 0]; // an E major chordAddOctaveToAll(frets);Console.WriteLine($"After AddOctaveToAll: {string.Join(" ", frets)}"); // changed: the method got the same array
void AddOctave(int value){ value += 12; Console.WriteLine($"Inside AddOctave: {value}");}
void AddOctaveToAll(int[] values){ for (int i = 0; i < values.Length; i++) { values[i] += 12; }}Inside AddOctave: 17After AddOctave: 5After AddOctaveToAll: 12 14 14 13 12 12A parameter receives a copy of the argument. Changing value inside AddOctave doesn’t change fret. But an array variable doesn’t hold the array itself: it holds a reference, the address where the array lives. The copy is a copy of the address, so values and frets point to the same array, and the method changes it. The E major chord moved up an octave, to the 12th fret. Types that behave like int are value types; types that behave like arrays are reference types. Lesson 6 comes back to the difference.
Arrays
Section titled “Arrays”An array holds a fixed number of values of the same type, one after the other, each at a numbered position, its index.
// An array: a fixed number of values of the same typestring[] strings = ["E2", "A2", "D3", "G3", "B3", "E4"];
Console.WriteLine(strings.Length);Console.WriteLine(strings[0]); // indexes start at 0Console.WriteLine(strings[5]); // the last index is Length - 1Console.WriteLine(strings[^1]); // ^1: the first from the endConsole.WriteLine(string.Join(", ", strings[1..3])); // a range: indexes 1 and 2
strings[0] = "D2"; // drop D tuning: the values can changeConsole.WriteLine(string.Join(" ", strings));
// new int[4]: four ints, all 0 to start withint[] minutes = new int[4];minutes[1] = 30;Console.WriteLine(string.Join(" ", minutes));
// Loop over the valuesint[] practice = [30, 45, 0, 60, 20];int total = 0;foreach (int m in practice){ total += m;}Console.WriteLine($"Total: {total} minutes over {practice.Length} days");
// Sort changes the array itselfArray.Sort(practice);Console.WriteLine(string.Join(" ", practice));6E2E4E4A2, D3D2 A2 D3 G3 B3 E40 30 0 0Total: 155 minutes over 5 days0 20 30 45 60string[], said string array, is the type “array of strings”.["E2", "A2", …]is a collection expression: the values between square brackets.new int[4]makes an array of fourint, all0.- Indexes start at 0: a six-item array goes from
strings[0]tostrings[5].strings[^1], with a caret, counts from the end, andstrings[1..3]is a new array with the items from index 1 up to, but not including, index 3. See indices and ranges. Lengthgives the number of items. It can’t change: an array has noAdd.string.Joinbuilds one string from all the items, with a separator between them, andArray.Sortsorts the array in place.
Going past the end is the most common mistake with arrays. The compiler can’t see it, because the index is only known while the program runs:
string[] strings = ["E2", "A2", "D3", "G3", "B3", "E4"];for (int i = 0; i <= strings.Length; i++) // <= goes one step too far{ Console.WriteLine($"{i}: {strings[i]}");}0: E21: A22: D33: G34: B35: E4Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array. at Program.<Main>$(String[] args) in C:\Users\spare\source\repos\learn\code\csharp-beginner\examples\l04_index_out_of_range.cs:line 4With less than or equal, <=, the loop also runs with i equal to 6, and there is no strings[6]. The loop condition of an array is almost always i less than the length of the array, i < array.Length, or better, a foreach, which can’t go too far.
And the size of an array is fixed:
string[] strings = ["E2", "A2", "D3", "G3", "B3", "E4"];strings.Add("B1");l04_array_fixed_size.cs(2,9): error CS1061: 'string[]' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'string[]' could be found (are you missing a using directive or an assembly reference?)For a collection that grows, use a list.
A List<T> is like an array that can grow and shrink. The T stands for the type of its items, written between angle brackets: List<string> is a list of strings, List<int> a list of integers.
// A List<string> grows and shrinks; the type between < > is the type of its itemsList<string> chord = ["C", "E", "G"];Console.WriteLine($"{chord.Count} notes: {string.Join(" ", chord)}");
chord.Add("B"); // Cmaj7Console.WriteLine(string.Join(" ", chord));
chord.Insert(1, "D"); // at index 1, the others move upConsole.WriteLine(string.Join(" ", chord));
chord.Remove("D"); // removes the first "D" it findsConsole.WriteLine(string.Join(" ", chord));
Console.WriteLine(chord.Contains("G"));Console.WriteLine(chord.IndexOf("B"));Console.WriteLine(chord.IndexOf("F#")); // -1: not in the list
chord[3] = "Bb"; // C7Console.WriteLine(string.Join(" ", chord));
chord.RemoveAt(chord.Count - 1);Console.WriteLine(string.Join(" ", chord));
// An empty list, filled in a loopList<int> octaves = [];for (int midi = 12; midi <= 60; midi += 12){ octaves.Add(midi);}Console.WriteLine($"The C notes in MIDI numbers: {string.Join(", ", octaves)}");3 notes: C E GC E G BC D E G BC E G BTrue3-1C E G BbC E GThe C notes in MIDI numbers: 12, 24, 36, 48, 60Array string[] |
List List<string> |
|
|---|---|---|
| Size | fixed when created | grows and shrinks |
| Number of items | Length |
Count |
| Read and change an item | a[i], a[i] = x |
list[i], list[i] = x |
| Add, insert, remove | no | Add, Insert, Remove, RemoveAt |
| Search | Array.IndexOf(a, x) |
list.IndexOf(x), list.Contains(x) |
| Use it when | the number of items is known and doesn’t change | items come and go |
A C major chord is C, E and G. Adding B makes a major seventh chord, Cmaj7; with B♭ (written Bb) it’s a dominant seventh, C7. IndexOf returns -1 when the item isn’t there.
The list checks the type of what you add:
List<int> frets = [0, 2, 2];frets.Add("1");l04_list_wrong_type.cs(2,11): error CS1503: Argument 1: cannot convert from 'string' to 'int'Methods, arrays and lists together: Guitar Alchemist’s projects
Section titled “Methods, arrays and lists together: Guitar Alchemist’s projects”Guitar Alchemist is split into more than a hundred projects. The LadybugDB course extracted their list to projects.csv, from GA’s commit a26a7893. Twelve of the projects of its Common folder, copied by hand into two arrays, are enough to practise:
// Twelve projects of the Common folder of GuitarAlchemist/ga, and their languages,// copied from code/ladybugdb/data/ga/projects.csvstring[] names =[ "GA.Core", "GA.Domain.Core", "GA.Business.Config", "GA.Business.Core", "GA.Business.DSL", "GA.Business.AI", "GA.Business.ML", "GA.Business.ProbabilisticGrammar", "GA.Business.Core.Generated", "GA.Infrastructure", "GA.Presentation", "GA.Testing.Semantic",];string[] languages = ["C#", "C#", "F#", "C#", "F#", "C#", "C#", "F#", "F#", "C#", "C#", "C#"];
Console.WriteLine($"{CountStartingWith(names, "GA.Business.")} projects start with GA.Business.");
List<string> fsharp = ProjectsIn(names, languages, "F#");Console.WriteLine($"{fsharp.Count} F# projects:");foreach (string name in fsharp){ Console.WriteLine($" {name}");}
Console.WriteLine($"Longest name: {Longest(names)}");
int CountStartingWith(string[] values, string prefix){ int count = 0; foreach (string value in values) { if (value.StartsWith(prefix)) { count++; } } return count;}
// The two arrays go together: names[i] is written in languages[i]List<string> ProjectsIn(string[] projectNames, string[] projectLanguages, string language){ List<string> result = []; for (int i = 0; i < projectNames.Length; i++) { if (projectLanguages[i] == language) { result.Add(projectNames[i]); } } return result;}
string Longest(string[] values){ string longest = values[0]; foreach (string value in values) { if (value.Length > longest.Length) { longest = value; } } return longest;}7 projects start with GA.Business.4 F# projects: GA.Business.Config GA.Business.DSL GA.Business.ProbabilisticGrammar GA.Business.Core.GeneratedLongest name: GA.Business.ProbabilisticGrammarEach method does one thing and has a name that says what: count, filter, find the longest. ProjectsIn uses a for instead of a foreach because it needs the index i to read the same position in both arrays. Two arrays that must stay in step are fragile: add a name and forget its language, and every language after it is wrong. Lesson 9 replaces them with one list of records, each with a name and a language, and answers the same questions with LINQ. Lesson 10 reads the whole CSV file instead of copying it by hand.
null: no value at all
Section titled “null: no value at all”Some variables need a way to say “there is nothing here”: no capo on the guitar, no more lines to read. C# uses null for that. A type followed by a question mark, such as string?, a string or null, accepts null; a plain string isn’t supposed to hold it.
// string? : a string, or null (no string at all)string? capo = null;Console.WriteLine(capo == null);Console.WriteLine(capo is null);
// ?. gives null instead of reading a member of null; ?? gives a value to use instead of nullConsole.WriteLine(capo?.Length);Console.WriteLine(capo ?? "no capo");Console.WriteLine(capo?.Length ?? 0);
capo ??= "fret 2"; // assigns only if capo is nullConsole.WriteLine(capo);Console.WriteLine(capo.Length); // the compiler knows capo isn't null here
// Console.ReadLine returns null when there is nothing left to readList<string> lines = [];string? line;while ((line = Console.ReadLine()) != null){ if (!string.IsNullOrWhiteSpace(line)) { lines.Add(line.Trim()); }}Console.WriteLine($"{lines.Count} non-empty lines: {string.Join(" | ", lines)}");With the lines Am, an empty line, F , C, a line of spaces, and G as input (input/l04_null.txt):
TrueTrue
no capo0fret 264 non-empty lines: Am | F | C | G| Write | Means |
|---|---|
x == null, x is null |
is x null? |
x?.Length |
null if x is null, else x.Length |
x ?? other |
x, or other if x is null |
x ??= value |
put value in x only if x is null |
The third line of the output is empty: capo?.Length is null, and WriteLine prints nothing for it. In the loop, (line = Console.ReadLine()) != null reads a line, stores it in line, then compares it with null: the loop stops at the end of the input. string.IsNullOrWhiteSpace is true for null, an empty string, or only spaces, and Trim removes the spaces around the text.
The compiler watches for null
Section titled “The compiler watches for null”Reading a member of null, such as its Length, is impossible: there is no string to measure. The compiler follows where null can come from and warns before it happens. This feature is called nullable reference types, turned on by the <Nullable>enable</Nullable> you saw in the project file of lesson 1, and by default in file-based apps.
string? FindTuning(string name){ if (name == "standard") { return "E A D G B E"; } return null;}
string? tuning = FindTuning("drop D");Console.WriteLine(tuning.Length); // warning CS8602, then a NullReferenceException at run timel04_null_warning.cs(11,19): warning CS8602: Dereference of a possibly null reference.Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. at Program.<Main>$(String[] args) in C:\Users\spare\source\repos\learn\code\csharp-beginner\examples\l04_null_warning.cs:line 11It’s only a warning, so the program runs, and crashes on line 11. The fix is to handle the null case, for example tuning?.Length ?? 0, or an if (tuning is null) before using it. In the first example, capo.Length after capo ??= "fret 2" gets no warning: the compiler understood that capo can’t be null any more. Lesson 8 covers exceptions and null safety in depth.
Key takeaways
Section titled “Key takeaways”- A method has a return type (
voidfor none), a name, parameters and a body;returngives back the result and leaves the method. - The compiler checks the number and the types of the arguments, and that every path of a non-
voidmethod returns a value. - Arguments are copies: a method can’t change an
intvariable of the caller, but it can change the items of an array it receives, because the copy is a reference to the same array. - An array has a fixed size,
Length, and indexes from0toLength - 1; going past the end throwsIndexOutOfRangeException. List<T>grows and shrinks withAdd,Insert,RemoveandRemoveAt, and has aCount.string?can benull;?.,??and??=handle it, and the compiler warns when you may use anull.
Exercises
Section titled “Exercises”- Write a method
Averagethat takes anint[]and returns its average as adouble, ornullwhen the array is empty. Call it on a week of practice times,30, 45, 0, 60, 20, 0, 90, and on an empty array.
Solution
// Exercise 1: the average of an array, or null when the array is emptyint[] week = [30, 45, 0, 60, 20, 0, 90];int[] nothing = [];
Console.WriteLine(Average(week));Console.WriteLine(Average(nothing) ?? -1);Console.WriteLine(Average(nothing) is null ? "no practice recorded" : "some practice");
double? Average(int[] values){ if (values.Length == 0) { return null; } int total = 0; foreach (int value in values) { total += value; } return (double)total / values.Length;}35-1no practice recordeddouble? is a double that can also be null: the ? works on value types too. (double)total / values.Length converts before dividing; total / values.Length would be an integer division. Dividing by zero is avoided by the return null at the start. The last line uses the conditional operator condition ? a : b, a short if/else that gives a value.
- Store the twelve note names (
C,C#, …B) in an array, once. Write a methodTransposethat takes aList<string>of note names and a number of semitones, positive or negative, and returns a new list with every note moved. Check that the original list doesn’t change.
Solution
exercises/l04_ex_transpose.cs:
// Exercise 2: transpose a chord, a list of note names, by a number of semitonesstring[] chromatic = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
List<string> cMajor = ["C", "E", "G"];Console.WriteLine(string.Join(" ", Transpose(cMajor, 2)));Console.WriteLine(string.Join(" ", Transpose(cMajor, 7)));Console.WriteLine(string.Join(" ", Transpose(["A", "C", "E"], -3)));Console.WriteLine(string.Join(" ", cMajor)); // unchanged: Transpose returns a new list
List<string> Transpose(List<string> notes, int semitones){ List<string> result = []; foreach (string note in notes) { int index = Array.IndexOf(chromatic, note); int moved = ((index + semitones) % 12 + 12) % 12; // + 12 keeps negative steps in 0..11 result.Add(chromatic[moved]); } return result;}D F# AG B DF# A C#C E GC major moved up 2 semitones is D major, up 7 is G major; A minor moved down 3 is F♯ minor. In C#, % keeps the sign of the left number: moving C (index 0) down 3 semitones gives (0 - 3) % 12, which is -3, not a valid index. Adding 12 then taking % 12 again always gives an index from 0 to 11: here, 9, the A.
Transpose reads chromatic, a variable of the top-level code, without receiving it as a parameter: a local function can. It’s convenient here, but it hides what the method depends on. Lesson 5 shows how a class holds such shared data. A note that isn’t in the array, such as Bb, gives -1 from Array.IndexOf and a wrong result: improving that is a good extra exercise.
- Read all the lines of the input until its end, store them in a list, then print the number of lines and the longest one, or
(no lines)if there were none. Write the search for the longest line as a method that returnsstring?.
Solution
exercises/l04_ex_longest_line.cs:
// Exercise 3: read every line until the end of the input, then print the count and the longest lineList<string> lines = [];string? line;while ((line = Console.ReadLine()) != null){ lines.Add(line);}
string? longest = Longest(lines);Console.WriteLine($"{lines.Count} lines");Console.WriteLine($"Longest: {longest ?? "(no lines)"}");
string? Longest(List<string> values){ string? best = null; foreach (string value in values) { if (best == null || value.Length > best.Length) { best = value; } } return best;}With four tunings as input (input/l04_ex_longest_line.txt):
4 linesLongest: Standard: E2 A2 D3 G3 B3 E4best starts at null, meaning “no line seen yet”. best == null || value.Length > best.Length never reads best.Length when best is null, because || stops at the first true; the compiler knows it, and doesn’t warn. With an empty input, the method returns null, and ?? prints (no lines).