3. Conditions and loops
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/l03_*.cs, the rejected snippets compile_fail/l03_*.cs and the solutions exercises/l03_*.cs.
Until now, programs ran every line once, from top to bottom. This lesson changes that: a condition runs some lines only in some cases, and a loop runs lines several times.
Comparisons and bool
Section titled “Comparisons and bool”A comparison asks a question whose answer is a bool: true or false.
int fret = 12;
// A comparison gives a bool: true or falseConsole.WriteLine(fret == 12);Console.WriteLine(fret != 12);Console.WriteLine(fret > 5 && fret < 10); // && : both must be trueConsole.WriteLine(fret < 1 || fret > 11); // || : at least one must be trueConsole.WriteLine(!(fret > 5)); // ! : the oppositeTrueFalseFalseTrueFalse| Operator | Meaning | Operator | Meaning | |
|---|---|---|---|---|
== |
equal to | && |
and | |
!= |
not equal to | || |
or | |
<, <= |
less than, or equal | ! |
not | |
>, >= |
greater than, or equal |
Be careful with ==, two equal signs, which compares, and =, one sign, which assigns. The comparison operators and boolean logical operators pages have the complete rules. And and or stop as soon as they know the answer: in fret > 5 && fret < 10, when the first comparison is false, the second one isn’t even computed.
if, else if, else
Section titled “if, else if, else”if runs a block, the lines between braces, only when its condition is true:
// if runs a block only when the condition is trueif (fret == 0){ Console.WriteLine("Open string");}else if (fret == 12){ Console.WriteLine("One octave above the open string");}else{ Console.WriteLine("Somewhere else on the neck");}
// Strings are compared by their contentstring tuning = "E A D G B E";if (tuning == "E A D G B E"){ Console.WriteLine("Standard tuning");}
// A variable declared inside a block only exists in that blockif (fret > 7){ int distance = fret - 7; Console.WriteLine($"{distance} frets above the 7th");}One octave above the open stringStandard tuning5 frets above the 7thThe conditions are checked in order, and only the first block whose condition is true runs. else catches every other case. Both else if and else are optional.
The condition must be a bool. Two classic mistakes are rejected. Writing one equal sign instead of two:
int fret = 5;if (fret = 12){ Console.WriteLine("Octave");}l03_assign_in_if.cs(2,5): error CS0029: Cannot implicitly convert type 'int' to 'bool'l03_assign_in_if.cs(1,5): warning CS0219: The variable 'fret' is assigned but its value is never usedfret = 12 is an assignment, whose value is the int 12, not a bool. In some other languages this compiles and silently changes fret. A string is not a condition either:
string answer = "yes";if (answer)l03_string_condition.cs(2,5): error CS0029: Cannot implicitly convert type 'string' to 'bool'Write if (answer == "yes").
A variable declared inside a block exists only in that block, and in the blocks inside it. That area is its scope:
int fret = 9;if (fret > 7){ int distance = fret - 7;}Console.WriteLine(distance);l03_out_of_scope.cs(6,19): error CS0103: The name 'distance' does not exist in the current contextTo use distance after the if, declare it before the if.
switch
Section titled “switch”When a value is compared with a list of possible values, a switch is easier to read than a chain of else if.
int semitone = 7;
// The switch statement: one case per value, each case ends with breakswitch (semitone){ case 0: Console.WriteLine("Unison"); break; case 7: Console.WriteLine("Perfect fifth"); break; case 12: Console.WriteLine("Octave"); break; default: Console.WriteLine("Another interval"); break;}Perfect fifthThe switch statement jumps to the case that matches, or to default when none does. Each section must end with break (or return, lesson 4). In C and JavaScript, a missing break lets the program fall into the next case; C# refuses it:
switch (semitone){ case 7: Console.WriteLine("Perfect fifth"); case 12: Console.WriteLine("Octave"); break;}l03_fall_through.cs(4,5): error CS0163: Control cannot fall through from one case label ('case 7:') to anotherThe switch expression
Section titled “The switch expression”Often, each case only computes a value. The switch expression says it in fewer lines: the value, switch, then one arm per case, pattern => result, separated by commas.
// The switch expression computes a value: one arm per pattern, _ matches everything elsestring name = semitone switch{ 0 => "C", 1 => "C#", 2 => "D", 3 => "D#", 4 => "E", 5 => "F", 6 => "F#", 7 => "G", 8 => "G#", 9 => "A", 10 => "A#", 11 => "B", _ => "not a semitone between 0 and 11",};Console.WriteLine($"Semitone {semitone} above C is {name}");
// Patterns can compare: the first arm that matches winsforeach (int fret in new[] { 0, 3, 7, 12, 17, 30 }){ string zone = fret switch { 0 => "open string", < 5 => "first position", < 12 => "middle of the neck", 12 => "octave", <= 24 => "high on the neck", _ => "no such fret", }; Console.WriteLine($"Fret {fret}: {zone}");}Semitone 7 above C is GFret 0: open stringFret 3: first positionFret 7: middle of the neckFret 12: octaveFret 17: high on the neckFret 30: no such fret- The arms are tried from top to bottom, and the first one that matches gives the value. Fret 3 matches the arm
< 5, less than 5, before the arm less than 12 is tried. - A pattern can be a value, such as
12, or a comparison, such as< 5, less than 5. The patterns page shows the others:and,or,not, and many more. _, the discard, matches everything: it’s thedefaultof a switch expression.- The loop
foreach (int fret in new[] { … })runs the block once for each number; loops come next.
Without _, a value that no arm matches has nothing to produce. The compiler warns, and the program fails when it happens:
int stringNumber = 7;
// No arm for 7, and no _ arm: the compiler warns, and the program fails at run timestring open = stringNumber switch{ 1 => "E4", 2 => "B3", 3 => "G3", 4 => "D3", 5 => "A2", 6 => "E2",};Console.WriteLine(open);l03_switch_warning.cs(4,28): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '0' is not covered.Unhandled exception. System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input.Unmatched value was 7. at <PrivateImplementationDetails>.ThrowSwitchExpressionException(Object unmatchedValue) at Program.<Main>$(String[] args) in C:\Users\spare\source\repos\learn\code\csharp-beginner\examples\l03_switch_warning.cs:line 4A warning, unlike an error, doesn’t stop the build: the program runs, and here it stops with an exception, an error that happens while the program runs. The lines starting with at are the stack trace: where the program was. The last one points to line 4 of the file. Lesson 8 is about exceptions.
Two things to know about warnings. First, read them: this one announced the crash. Second, dotnet run shows them only when it compiles; if you run the same unchanged file again, it doesn’t compile, and the warning isn’t printed again. dotnet clean l03_switch_warning.cs forgets the compiled program, and the next run shows the warning again. The course’s check.sh does that before every run.
A loop repeats a block. C# has four iteration statements:
// while: repeats as long as the condition is trueint countdown = 3;while (countdown > 0){ Console.WriteLine($"{countdown}..."); countdown--; // same as countdown = countdown - 1}Console.WriteLine("Play!");
// for: start; condition; stepfor (int fret = 0; fret <= 12; fret += 3){ Console.Write($"{fret} ");}Console.WriteLine();
// foreach: every item of a collection, here every character of a stringforeach (char letter in "EADGBE"){ Console.Write($"[{letter}]");}Console.WriteLine();
// break leaves the loop, continue goes to the next turnfor (int i = 1; i <= 10; i++){ if (i % 2 == 0) { continue; // skip even numbers } if (i > 7) { break; // stop after 7 } Console.Write($"{i} ");}Console.WriteLine();
// do-while: the block runs at least once, the condition is checked afterint tries = 0;do{ tries++; Console.WriteLine($"Try {tries}");} while (tries < 2);3...2...1...Play!0 3 6 9 12[E][A][D][G][B][E]1 3 5 7Try 1Try 2| Loop | Use it when | Checks the condition |
|---|---|---|
while (condition) |
you don’t know in advance how many turns | before each turn |
do { … } while (condition); |
the block must run at least once | after each turn |
for (start; condition; step) |
you count: from 0 to 12, by 3 | before each turn |
foreach (type item in collection) |
you visit every item of a collection | — |
countdown--subtracts 1,i++adds 1, andfret += 3adds 3: short forms ofcountdown = countdown - 1andfret = fret + 3.- A
forhas three parts separated by semicolons: what to do once at the start (int fret = 0), the condition checked before each turn (fret <= 12), and what to do after each turn (fret += 3). Its variablefretexists only inside the loop. foreachtakes each item in turn. A string is a collection ofchar; arrays and lists, in lesson 4, are collections too.continueskips the rest of the block and starts the next turn;breakleaves the loop at once.
A while whose condition never becomes false runs forever. If a program seems stuck, press Ctrl+C in the terminal to stop it.
Loops inside loops: the fretboard
Section titled “Loops inside loops: the fretboard”A loop can contain another loop. For each string, the inner loop visits each fret. This program prints the notes of the first five frets of a guitar in standard tuning, the Tuning.Default of Guitar Alchemist:
// The first five frets of a guitar in standard tuning (E2 A2 D3 G3 B3 E4, Tuning.Default in GA).// Each note is a number of semitones: C is 0, C# is 1, ... B is 11, and the next C is 12 again.for (int stringNumber = 6; stringNumber >= 1; stringNumber--){ // Semitones of the open string above C int open = stringNumber switch { 6 => 4, // E 5 => 9, // A 4 => 2, // D 3 => 7, // G 2 => 11, // B _ => 4, // E (string 1) };
Console.Write($"String {stringNumber}:"); for (int fret = 0; fret <= 5; fret++) { int semitone = (open + fret) % 12; // % 12 folds 12 back to 0: the octave string name = semitone switch { 0 => "C", 1 => "C#", 2 => "D", 3 => "D#", 4 => "E", 5 => "F", 6 => "F#", 7 => "G", 8 => "G#", 9 => "A", 10 => "A#", _ => "B", }; Console.Write($" {name,-2}"); } Console.WriteLine();}String 6: E F F# G G# AString 5: A A# B C C# DString 4: D D# E F F# GString 3: G G# A A# B CString 2: B C C# D D# EString 1: E F F# G G# AGuitarists number the strings from the highest, string 1, to the lowest, string 6; the outer loop counts down so that string 6 is printed first. On the B string, fret 1 is (11 + 1) % 12, which is 0: C. The % operator makes the numbers go round like the hours on a clock, which is exactly how note names repeat every octave. The music theory course goes much further with the same idea.
The same notes on the neck: each line is a string, string 6 at the top as in the output, and each column is a fret. Fret 0 is the open string, left of the nut, the bar where the strings start; the highlighted C is fret 1 of the B string.
Reading until the answer is right
Section titled “Reading until the answer is right”A while loop can keep asking for input until it gets what it needs:
// Guess the fret: the loop reads answers until one is rightint secret = 7;int tries = 0;bool found = false;
while (!found){ Console.Write("Which fret? "); string? line = Console.ReadLine(); if (line == null) { Console.WriteLine("No more input."); break; } if (!int.TryParse(line, out int guess)) { Console.WriteLine($"\"{line}\" is not a number."); continue; }
tries++; if (guess < secret) { Console.WriteLine("Higher."); } else if (guess > secret) { Console.WriteLine("Lower."); } else { found = true; Console.WriteLine($"Found in {tries} tries."); }}Typing 12, five, 5 and 7:
Which fret? 12Lower.Which fret? five"five" is not a number.Which fret? 5Higher.Which fret? 7Found in 3 tries.continue goes back to the question without counting the bad answer as a try. When the input ends, ReadLine returns null and break leaves the loop: without it, a program whose input comes from a file, as in the CI, would ask forever. On a keyboard, Ctrl+Z then Enter on Windows, or Ctrl+D on Linux and macOS, ends the input.
Finding a mistake with the debugger
Section titled “Finding a mistake with the debugger”When a program gives a wrong result, you need to see what it does, line by line. The simplest way is to add Console.WriteLine calls that print the variables, run again, and remove them afterwards. It works everywhere.
A debugger does better: it pauses the program on a line you choose, a breakpoint, shows the value of every variable, and lets you run one line at a time. The three editors of lesson 1 have one:
| Action | Visual Studio Code | Visual Studio | Rider |
|---|---|---|---|
| Set or remove a breakpoint | F9, or click left of the line number | F9 | click left of the line |
| Start with the debugger | F5 | F5 | the bug icon |
| Run the next line | F10 (step over) | F10 | step over |
| Go into a method called on this line | F11 (step into) | F11 | step into |
| Continue to the next breakpoint | F5 | F5 | resume |
Try it on the fretboard program: put a breakpoint on the line int semitone = (open + fret) % 12;, start with the debugger, and look at stringNumber, open and fret in the Variables or Locals panel. Each time you continue, the program stops again at the next fret. You can also type an expression such as (open + fret) % 12 in the Watch panel to see its value.
The keys in this table are the defaults of each editor on Windows and Linux; the Rider keymap on macOS differs. See debugging in VS Code, the Visual Studio debugger for beginners, and debugging in Rider. To verify: I haven’t yet debugged these examples in each of the three editors, neither as single-file apps nor inside a project made with dotnet new console.
Key takeaways
Section titled “Key takeaways”- A comparison gives a
bool; and (&&), or (||) and not (!) combine them. Two equal signs compare, one equal sign assigns. if,else ifandelserun the first block whose condition istrue; a variable declared in a block exists only in that block.- A
switchstatement needs abreakat the end of each case; aswitchexpression computes a value, tries its arms from top to bottom, and should end with_. whilerepeats while a condition is true,forcounts,foreachvisits every item of a collection;breakleaves the loop andcontinueskips to the next turn.- A warning doesn’t stop the build, but often announces a problem; an unhandled exception stops the program and prints where it happened.
- A debugger pauses on breakpoints and shows the variables, one line at a time.
Exercises
Section titled “Exercises”- Print the numbers from 1 to 15 on one line, but print
Fizzinstead of multiples of 3,Buzzinstead of multiples of 5, andFizzBuzzfor multiples of both. Hint: a switch expression can look at two values at once, written(a, b) switch.
Solution
// Exercise 1: FizzBuzz from 1 to 15, with a switch expression on two remaindersfor (int i = 1; i <= 15; i++){ string text = (i % 3, i % 5) switch { (0, 0) => "FizzBuzz", (0, _) => "Fizz", (_, 0) => "Buzz", _ => i.ToString(), }; Console.Write($"{text} ");}Console.WriteLine();1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz(i % 3, i % 5) groups the two remainders in a tuple, and each arm checks both. (0, _) means “divisible by 3, whatever the second remainder”. The order matters: (0, 0) must come first, or 15 would print Fizz. With if, you’d write if (i % 3 == 0 && i % 5 == 0) first, then else if (i % 3 == 0), and so on.
- Ask for a note name (
C,C#, …B) and print the 13 notes of the chromatic scale from that note up to the same note one octave higher. Print a message if the note is unknown.
Solution
exercises/l03_ex_chromatic.cs:
// Exercise 2: read a note name and print the 13 notes of the chromatic scale from it, up to its octaveConsole.Write("Starting note? ");string? input = Console.ReadLine();
int start = input switch{ "C" => 0, "C#" => 1, "D" => 2, "D#" => 3, "E" => 4, "F" => 5, "F#" => 6, "G" => 7, "G#" => 8, "A" => 9, "A#" => 10, "B" => 11, _ => -1,};
if (start == -1){ Console.WriteLine($"Unknown note: {input}");}else{ for (int step = 0; step <= 12; step++) { string name = ((start + step) % 12) switch { 0 => "C", 1 => "C#", 2 => "D", 3 => "D#", 4 => "E", 5 => "F", 6 => "F#", 7 => "G", 8 => "G#", 9 => "A", 10 => "A#", _ => "B", }; Console.Write($"{name} "); } Console.WriteLine();}With A:
Starting note? AA A# B C C# D D# E F F# G G# AThe first switch expression turns the name into a number, with -1 meaning “unknown”: a switch expression works on strings too. The loop runs 13 times, step from 0 to 12, and % 12 brings the numbers above 11 back to the start. Typing the same note names twice isn’t great: lesson 4 stores them once, in an array.
- Read practice times in minutes, one per line, until an empty line or the end of the input. Skip lines that aren’t whole numbers or that are negative, saying so. Then print the number of valid days, the total in minutes, and the total in hours and minutes.
Solution
// Exercise 3: add up the practice minutes typed one per line; an empty line ends the inputint total = 0;int days = 0;while (true){ string? line = Console.ReadLine(); if (line == null || line == "") { break; } if (!int.TryParse(line, out int minutes) || minutes < 0) { Console.WriteLine($"Skipped: {line}"); continue; } total += minutes; days++;}Console.WriteLine($"{days} days, {total} minutes, {total / 60} h {total % 60} min");With the lines 30, 45, forty, -5, 60, an empty line, then 90 (input/l03_ex_total.txt), the output is:
Skipped: fortySkipped: -53 days, 135 minutes, 2 h 15 minwhile (true) never stops by itself: the break inside decides. The 90 after the empty line is never read. In !int.TryParse(line, out int minutes) || minutes < 0, the || computes minutes < 0 only when TryParse succeeded, so minutes always holds the parsed value when it’s compared.
Sources
Section titled “Sources”- Selection statements:
if,elseandswitch, theswitchexpression, patterns - Iteration statements:
for,foreach,doandwhile, jump statements:breakandcontinue - Comparison operators, boolean logical operators
- Compiler warning CS8509,
SwitchExpressionException - Debugging C# in VS Code, Visual Studio debugger feature tour, Rider: debug code