3. The Basics of the C# Language
3.1. Introduction
We’ll start by treating C# as a traditional programming language. We’ll cover classes in the next chapter. In a program, there are two main components:
- data;
- the instructions that manipulate them.
We generally strive to separate data from instructions:

All the examples in this chapter are single-file applications: a single file, [.cs], without a project file ([.csproj]), which is run using the command [dotnet run basics-01.cs]. This feature, introduced with .NET 10, was covered in the previous chapter. The examples are located in the [03-basics] folder of the course code. They run in the French locale (fr-FR), as on a French Windows machine: real numbers are displayed with a decimal comma.
3.2. The Minimal Program
3.2.1. High-level instructions
The program [basics-01.cs] is as follows:
Results:
Comments:
- The program contains neither classes nor methods: the statements are written directly in the file. These are called high-level statements (C# 9, 2020). A single program file can contain them;
- Line 3: The [WriteLine] method of the [Console] class prints a line to the screen;
- line 5: The class [Console] belongs to the namespace [System]. Its full name is [System.Console]. A namespace groups related types, just as a folder groups files;
- line 5: the program has an array [args] containing the command-line arguments. We’ll come back to this in the “Main Program Arguments” section. The number of elements in the array is [args.Length]. The + operator concatenates a string and a number here: the number is first converted to a string.
With two command-line arguments (everything following [--] is passed to the program):
3.2.2. The classic form with a Main method
The 2008 course (C# 3.0) did not cover high-level instructions. The same program was written as follows: [basics-02.cs]:
Results:
Comments:
- line 2: the directive [using] imports the namespace [System]. You can then write [Console] instead of [System.Console];
- Line 4: The class is placed in the [Basics] namespace. Starting with C# 10, you can write [namespace Basics;] (file-scope namespace): the entire file then belongs to this namespace, without curly braces or additional indentation. This is the form we will use from now on;
- Line 6: In C#, all code belongs to a class. The class here is called [Program];
- Line 9: The static method [Main] is the program’s entry point: this is the method the system executes upon launch. Its parameter, [args], receives the command-line arguments.
The two programs are equivalent. When it encounters high-level instructions, the compiler generates a class and a method named [Main] that contain these instructions, as well as the parameter [string[] args]. High-level instructions are therefore simply a shorthand form of the standard syntax. They are well-suited for the small programs in this chapter; the standard syntax remains useful when you want to inspect the class that contains [Main].
3.2.3. Implicit using statements
The program [basics-01.cs] uses the class [Console] without the [using System;] directive. This is possible because the .NET SDK automatically adds a generated file containing [global using] directives (C# 10) to every console program, which apply to all files in the program. For [basics-01.cs], this file contains:
These are the implicit using statements: the most commonly used namespaces are imported automatically. A namespace not included in this list, such as [System.Globalization], must always be imported using a [using] directive at the top of the file. In the program [basics-02.cs], the directive [using System;] is therefore redundant: we left it in to show the 2008 format.
3.3. C# Data
C# uses the following data types:
- integers;
- real numbers;
- decimal numbers;
- characters and character strings;
- Booleans;
- objects.
3.3.1. Predefined data types
C# Type | Type .NET | Data represented | Literal suffix | Encoding | Value range |
[char] | [Char] (S) | character | 2 bytes | Unicode character (UTF-16) | |
[string] | [String] (C) | character string | reference to a Unicode character sequence | ||
[int] | [Int32] (S) | integer | 4 bytes | [-2³¹, 2³¹-1] or [-2147483648, 2147483647] | |
[uint] | [UInt32] (S) | unsigned integer | U | 4 bytes | [0, 2³²-1] or [0, 4294967295] |
[long] | [Int64] (S) | integer | L | 8 bytes | [-2⁶³, 2⁶³-1] or [-9223372036854775808, 9223372036854775807] |
[ulong] | [UInt64] (S) | unsigned integer | UL | 8 bytes | [0, 2⁶⁴-1] or [0, 18446744073709551615] |
[sbyte] | [SByte] (S) | integer | 1 byte | [-2⁷, 2⁷-1] or [-128, 127] | |
[byte] | [Byte] (S) | unsigned integer | 1 byte | [0, 2⁸-1] or [0, 255] | |
[short] | [Int16] (S) | integer | 2 bytes | [-2¹⁵, 2¹⁵-1] or [-32768, 32767] | |
[ushort] | [UInt16] (S) | unsigned integer | 2 bytes | [0, 2¹⁶-1] or [0, 65535] | |
[float] | [Single] (S) | real number | F | 4 bytes | [1,5 10⁻⁴⁵, 3,4 10³⁸] in absolute value, 7 significant digits |
[double] | [Double] (S) | real number | D | 8 bytes | [5,0 10⁻³²⁴, 1,7 10³⁰⁸] in absolute value, 15 to 16 significant digits |
[decimal] | [Decimal] (S) | decimal number | M | 16 bytes | [1,0 10⁻²⁸, 7,9 10²⁸] as an absolute value, 28 to 29 significant digits |
[bool] | [Boolean] (S) | Boolean | 1 byte | [true], [false] | |
[object] | [Object] (C) | object reference | reference to an object of any type |
Above, we have listed the equivalent .NET types for each C# type, marked with (S) if the type is a structure and (C) if it is a class. We see that there are two possible names for a 32-bit integer: [int] and [Int32]. The type [int] is a C# keyword. [Int32] is a structure belonging to the [System] namespace: its full name is [System.Int32]. The type [int] is a C# alias that refers to the structure .NET [System.Int32]. Similarly, the C# type [string] is an alias for the .NET [System.String] type. [System.String] is a class, not a structure. The two concepts are similar, but with the following fundamental difference:
- a variable of type structure directly contains its value: this is called a value type;
- a variable of a class type contains the address of the object (its reference); this is known as a reference type.
In addition, there are two less common types:
- [nint] and [nuint] (C# 9) are signed and unsigned integers whose size is that of a memory address: 4 bytes on a 32-bit system, 8 bytes on a 64-bit system. They are primarily used to interact with native code;
- [System.Half] (.NET 5) is a 2-byte, low-precision floating-point number used in graphics computing and machine learning. It is not a predefined language type: it does not have a C# keyword.
A structure, like a class, is a complex type with properties and methods. Thus, we can write:
The literal 3 is by default of type C# [int], and therefore of type .NET [System.Int32]. This structure has a method [GetType()] that returns an object describing the data type. Among the properties of this object, [FullName] is the full name of the type. The literal 3 is therefore a more complex object than it appears at first glance. The question mark in [string?] will be explained a little later.
The program [basics-03.cs] illustrates these various points:
Results:
Comments:
- line 2: declaration of an integer [integer] initialized to 2;
- line 16: the statement [Console.WriteLine("... {0} ... {1} ...", param0, param1, ...)] displays the text specified as its first parameter (the format) on the screen, replacing each {i} with the value of the i-th parameter following the format. Parameters are numbered starting from 0;
- line 17: the type of a variable [v] is obtained via [v.GetType().FullName]. The size in bytes of a predefined value type [T] is obtained via [sizeof(T)];
- line 20: Since the type [string] refers to a class and not a structure, the operator [sizeof] cannot be used;
- line 33: [sizeof(nint)] is not allowed in regular code because the size of [nint] is only known at runtime. The property [nint.Size] specifies that it is 8 bytes on the 64-bit machine being used;
- line 35: You cannot write [Half h = 1.5;]: there is no implicit conversion from a [double] to a [Half]. The notation [(Half)] is an explicit conversion (known as a “cast” in English), which we will examine later;
- lines 40–42: The properties [MinValue] and [MaxValue] of the numeric types specify the bounds of their value domains.
The output displays the type names .NET rather than the C# aliases. Note the decimal comma for floating-point numbers: the numbers are converted to strings according to the system’s locale, in this case French.
3.3.2. Notation for Literal Data
Data | Examples of literals |
[int] integer (32 bits) | 145, -7, 0xFF (hexadecimal), 0b1010_1010 (binary) |
Integer [long] (64 bits), suffix L | 100000L, 1_000_000_000_000L |
unsigned integer, suffix U or UL | 4000000000U, 10UL |
floating-point [double] | 134.789, -45E-18 (-45 × 10⁻¹⁸) |
real [float], suffix F | 134.789F, -45E-18F |
real [decimal], suffix M | 100000.50M |
character [char] | 'A', 'b', '\n', '\u00e9' |
string [string] | "today", "c:\\chap1", @"c:\chap1", $"{name} is {age} years old", """plain text""" |
boolean [bool] | [true], [false] |
date | [new DateTime(1954, 10, 13)] (year, month, day) for 10/13/1954 |
In the source code, floating-point numbers are always written with a decimal point, regardless of the system’s locale. Since C# 7, integers can be written in binary (prefixed with [0b]) and groups of digits can be separated by the _ character to improve readability: [1_000_000] is the integer one million.
A standard string literal can contain escape sequences introduced by the \ character: "\n" represents the end-of-line character and not the sequence of the two characters \ and n, "\t" represents a tab, "\"" represents a double quote, and "\\" represents the \ character itself. To write the Windows path c:\chap1\paragraph3, you must therefore write "c:\\chap1\\paragraph3". You can also write @"c:\chap1\paragraph3": the @"text" syntax specifies that the text should be taken exactly as written, without interpreting the \ character. This is called a verbatim string.
C# now offers two other types of strings:
- the interpolated string (C# 6) $"...": expressions enclosed in curly braces are evaluated, and their values are inserted into the string. An expression can be followed by a format specifier after the colon: [$"{d1:F2}"] displays the floating-point number [d1] with two decimal places. This is the most readable way to construct a string, and we will use it from now on instead of the {0}, {1}, ... formats;
- The raw string (C# 11) """...""" : It begins and ends with at least three quotation marks. It can contain quotation marks and unescaped \ characters, and can span multiple lines. Spaces preceding the closing quotation marks are removed from each line, allowing the text to be indented with the code. This is very useful for writing JSON, SQL, or HTML.
The program [basics-04.cs] demonstrates these different notations:
Results:
Comments:
- line 3: [0xFF] is 255 and [0b1010_1010] is 170;
- line 7: -45E-18 is displayed as -4,5E-17: this is the same value, normalized;
- line 12: the sequence \u00e9 denotes the Unicode character with hexadecimal code 00E9, i.e., é;
- line 16: a date. The method [ToShortDateString] displays it in the short format used in French culture. For a date without a time, .NET 6 added the type [DateOnly]; we’ll revisit this in Chapter 5;
- lines 20–22: the two strings are identical, as shown in the output;
- line 30: an interpolated string. The expressions [name], [age], and [age + 10] are evaluated and then inserted into the string;
- line 32: a few formats: F1 (one decimal place), X (hexadecimal), B8 (8-digit binary, .NET 8), N0 (thousands separator, no decimal place);
- line 33: a custom date format: day of the week, day, month in words, year;
- lines 36–41: a multi-line raw string. Quotation marks and the \ character are written as-is. The four spaces preceding the closing quotation marks have been removed from each line;
- line 44: an interpolated raw string. With two $ characters, expressions are written between double curly braces {{ and }}: the single curly braces, which appear frequently in JSON, remain plain text.
3.3.3. Data Declaration
3.3.3.1. Role of declarations
A program manipulates data characterized by a name and a type. This data is stored in memory. When the program is compiled, the compiler assigns each piece of data a memory location characterized by an address and a size. It does this based on the declarations made by the programmer. Furthermore, these declarations allow the compiler to detect programming errors. Thus, the operation [x = x * 2;] will be declared invalid if [x] is a character string.
3.3.3.2. Declaring Constants
The syntax for declaring a constant is as follows:
For example:
Why declare constants?
- The program is easier to read if the constant is given a meaningful name;
- it is easier to modify the program if the “constant” needs to be changed. Thus, if the VAT rate changes to 0.25, the only modification needed will be to the statement that defines its value: [const double VatRate = 0.25;]. If 0.2 had been used explicitly in the program, numerous statements would need to be modified.
The value of a constant must be known at compile time: a number, a string, a Boolean, or an expression that can be calculated from other constants. By convention, in .NET, constant names begin with a capital letter.
3.3.3.3. Variable Declaration
A variable is identified by a name and is associated with a data type. C# distinguishes between uppercase and lowercase letters: the variables [END] and [end] are different.
Variables can be initialized when they are declared. The syntax for declaring one or more variables is:
where [type] is a predefined type or a type defined by the programmer. Initialization is optional; however, C# does not allow the use of a local variable that has not yet been assigned a value.
You can also omit the type of a variable by using the keyword [var] in place of the type:
The keyword [var] does not mean that the variable has no specific type: it has the type of the value assigned to it, as determined by the compiler. Initialization is required here so that the compiler can deduce the type. We use [var] when the type is obvious from the value.
The program [basics-05.cs] is as follows:
Results:
Comments:
- lines 2–3: two constants;
- line 8: one explicitly typed data item;
- line 9: [GetType().Name] is the short name of the type, [GetType().FullName] is its full name;
- Line 11: an implicitly typed value. Because 3 is of type [int], [j] will be of type [int];
- line 13: Because [DateTime.Now] (the current date and time) is of type [DateTime], [today] will be of type [DateTime];
- lines 15–18: the inferred type depends on how the literal is written: 3.5 is a [double], 3.5F is a [float], and 10L is a [long];
- Line 21: three variables declared on the same line; only [a] and [c] are initialized. [b] is assigned a value on the next line.
A variable implicitly typed by the keyword [var] cannot change its type afterward. Thus, after line 13, you could not write:
because [today] is of type [DateTime]. We will see in Chapter 5, with LINQ, that it is possible to create a type “on the fly” within an expression: this is an anonymous type, to which the programmer has not assigned a name. If data of an anonymous type is to be assigned to a variable, the only way to declare that variable is to use the keyword [var].
3.3.4. Nullable Types
A variable of a value type, such as [int], always contains a value. It is sometimes useful to be able to express the absence of a value: an unknown age, an empty field in a database. The type [int?] (C# 2) is the type [int] with the value [null] added to it, which means “no value.” This is referred to as a nullable value type. The principle applies to all value types: [double?], [bool?], [DateTime?], ...
A variable of a reference type, such as [string], can always have the value [null]: this means that it does not reference any object. Attempting to use a member of such a variable—for example, its property [Length]—will result in a runtime error ([NullReferenceException]), one of the most common errors in C#. Starting with C# 8, the compiler can help prevent this: with nullable reference types—enabled by default in projects created using the [dotnet new] command and in single-file applications—
- the type [string] denotes a string that never evaluates to [null];
- the type [string?] refers to a string that can be [null].
The compiler tracks the possible values of variables and issues a warning when there is a risk of using the value [null]: assigning [null] to a variable [string], using [Length] on a variable [string?] that has not been tested, etc. Unlike [int?], [string?] does not create a new type at runtime: it is a directive intended for the compiler.
We’ll encounter these types right from the first examples, because the method [Console.ReadLine()], which reads a line typed on the keyboard, returns a [string?]: it returns [null] when there is nothing left to read (end of the input stream). The program [basics-06.cs] is as follows:
Results when entering text via the keyboard:
Results when the user presses Ctrl-D (Ctrl-Z followed by Enter on Windows), which closes the input stream:
Comments:
- line 3: [age] does not contain a value;
- line 4: the [HasValue] property indicates whether the variable contains a value; the [Value] property provides that value;
- line 8: a [int?] cannot be directly assigned to a [int] since it may be equal to [null]. An explicit conversion is required, which throws an exception if [age] is equal to [null]. We will discuss the safer [??] operator later;
- line 12: a string that may be equal to [null];
- line 15: a string that never equals [null]. Writing [name = null;] would trigger a compiler warning;
- line 20: [Console.ReadLine()] evaluates to [string?]. Declaring [line] as type [string] would trigger a warning;
- line 21: the value [null] is checked;
- line 29: in the [else] branch, the compiler knows that [line] is not equal to [null]: it issues no warning for the use of [line.Length].
3.3.5. Conversions between numbers and strings
Conversion | Method |
number -> string | [number.ToString()] or an interpolated string [$"{number}"] |
string -> int | [int.Parse(str)] or [int.TryParse(str, out int number)] |
string -> long | [long.Parse(str)] or [long.TryParse(str, out long number)] |
string -> double | [double.Parse(str)] or [double.TryParse(str, out double number)] |
string -> float | [float.Parse(str)] or [float.TryParse(str, out float number)] |
string -> decimal | [decimal.Parse(str)] or [decimal.TryParse(str, out decimal number)] |
Converting a string to a number may fail if the string does not represent a valid number. The [Parse] method then generates a fatal error called an exception. This error can be handled by the following [try / catch] clause:
If the function does not throw an exception, execution proceeds to the next statement; otherwise, it proceeds to the body of the [catch] clause, then to the next statement. We will revisit exception handling in the section dedicated to it.
The [TryParse] method (.NET 2.0) avoids the exception: it returns the boolean [true] if the conversion succeeds, [false] otherwise, and stores the resulting number in a variable passed using the keyword [out]. Starting with C# 7, this variable can be declared within the call itself: [int.TryParse(str, out var number)]. The [out] passing mode will be explained in the section on parameter passing. When incorrect input is expected—for example, from a keyboard—[TryParse] is preferable to [Parse]: an input error is not an exceptional situation.
The program [basics-07.cs] demonstrates several techniques for converting between numbers and character strings. The function [Display] displays the value of its parameter on the screen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | |
Results:
Comments:
- line 2: the [System.Globalization] namespace, which contains the [CultureInfo] class, is not included in the implicit using statements;
- Lines 5–8: the data to be converted;
- lines 11–14: every object has a method [ToString()] that converts it to a string;
- lines 25–30: the exception that [int.Parse] may throw is handled. [e.Message] is the error message associated with the [e] exception. The .NET exception messages are in English: .NET 10 does not provide a French translation;
- line 47: the string uses a decimal comma, in accordance with the system’s French locale;
- line 73: [TryParse] succeeds and stores 123 in the variable [i2], declared in the call;
- line 77: [TryParse] fails and returns [false]; the variable [i3] is then assigned the value 0;
- line 83: the program’s current locale is fr-FR;
- line 86: In the French locale, the period is not a decimal separator: the conversion fails;
- line 94: the invariant locale [CultureInfo.InvariantCulture] uses the decimal point, regardless of the machine. It is used to read and write numbers in files or in exchanges between programs, which must not depend on the user’s language;
- lines 95–96: the same number converted to a string according to the current locale (100,87) and according to the invariant locale (100.87);
- line 97: [TryParse] also accepts a locale;
- lines 103–105: a local function. In a high-level programming language, you cannot write methods, since there are no classes; instead, you write local functions, which can be called before they are defined. They are discussed in the section “Local Functions and Tuples.”
The 2008 course stated that real numbers in string form must use a comma: this is true only in the French culture. A program that reads numbers must always know in which culture they were written.
3.3.6. Data Arrays
A C# array is an object that allows data of the same type to be grouped under a single identifier. It is declared as follows:
[n] is the number of data elements the array can contain. The syntax [array[i]] refers to data element number i, where i belongs to the range [0, n-1]. Any reference to data item [array[i]] where i is not in the range [0, n-1] causes an exception. Upon creation, each element of the array is assigned the default value for its type: 0 for numbers, [false] for booleans, and [null] for reference types. An array can be initialized at the same time it is declared:
or, more simply:
or, starting with C# 12, using a collection expression:
We will use this last form from now on. Arrays have a property [Length], which represents the number of elements they contain.
A two-dimensional array is declared as follows:
where [n] is the number of rows and [m] is the number of columns. The syntax [array[i, j]] refers to element j in row i. The two-dimensional array can also be initialized at the same time it is declared:
The number of elements in each dimension is obtained using the method [GetLength(i)], where i=0 represents the dimension corresponding to the first index, i=1 represents the dimension corresponding to the second index, and so on. The number of dimensions is obtained using the property [Rank], and the total number of elements using the property [Length].
An array of arrays (also known as a staircase array) is declared as follows:
The declaration above creates an array of n elements. Each element, [array[i]], is a one-dimensional array reference. These references are not initialized by the declaration above; they have the value [null]. Unlike a two-dimensional array, each row can have a different length.
C# 8 introduced two convenient notations:
- backward indexing: [array[^1]] is the last element, [array[^2]] is the second-to-last, and so on. [array[^1]] is equivalent to [array[array.Length - 1]];
- the range: [array[1..3]] is a new array consisting of the elements with indices 1 and 2. The start index is included, and the end index is excluded. The bounds can be omitted: [array[2..]] ranges from index 2 to the end, and [array[..^1]] ranges from the beginning to the second-to-last element, inclusive.
The program [basics-08.cs] combines these different arrays:
Results:
Comments:
- line 3: a one-dimensional array is initialized;
- line 4: a loop [for] iterates over indices from 0 to [integers.Length - 1]. The loop [for] is shown below;
- line 10: a two-dimensional array (2 rows, 2 columns);
- lines 11–13: [GetLength(0)] is the number of rows, [GetLength(1)] is the number of columns;
- line 21: an array [names] with 3 elements. Each element is a reference to an array of type [string[]], currently [null];
- line 24: the 3 elements of the array [names] are initialized. [names[i]] now references an array of i+1 strings;
- line 31: [names[i][j]] is the jth element of the array referenced by [names[i]]. Since [names[i]] is an array, [names[i].Length] is the number of its elements. The expression “name” + i + j is evaluated from left to right: “name” + i yields a string, to which j is concatenated;
- lines 44–45: arrays created without initial values. The type [string?[]] is an array whose elements can be [null];
- line 49: a collection expression;
- line 51: the method [string.Join] constructs a string using the array’s elements, separated by the string passed as the first parameter;
- line 53: the last and second-to-last elements;
- line 55: the elements at indices 1 and 2;
- lines 60–61: a range creates a copy: modifying [slice] does not modify [values].
3.4. Basic C# Statements
There are:
- basic statements executed by the computer;
- instructions that control the flow of the program.
Basic instructions become clear when considering the structure of a microcomputer and its peripherals:

- 1: reading information from the keyboard;
- 2: processing information;
- 3: writing information to the screen.
3.4.1. Writing to the Screen
There are various instructions for writing to the screen:
where [expression] is any data that can be converted to a character string for display on the screen. All C# or .NET objects have a [ToString()] method that is used to perform this conversion. The [Write] method does the same thing as [WriteLine] without wrapping to the next line.
Class [System.Console] provides access to screen-writing operations ([Write], [WriteLine]). It has two properties, [Out] and [Error], which are write streams of type [TextWriter]:
- [Console.WriteLine()] is equivalent to [Console.Out.WriteLine()] and writes to the stream [Out], which is usually associated with the screen;
- [Console.Error.WriteLine()] writes to the [Error] stream, which is also usually associated with the screen.
The streams [Out] and [Error] can be redirected to files at runtime, as we will see shortly.
3.4.2. Reading Data Entered via the Keyboard
The data stream from the keyboard is represented by the object [Console.In] of type [TextReader]. This object type allows you to read a line of text using the [ReadLine] method:
The [Console] class provides a method, [ReadLine], which is associated by default with the [In] stream. Therefore, you can write:
The line typed on the keyboard, without the end-of-line character, is stored in the variable [line] and can then be processed by the program. As we have seen, [ReadLine] returns [null] when the input stream is exhausted: this is why the variable is of type [string?]. The stream [In] can be redirected to a file, just like the streams [Out] and [Error].
3.4.3. Example of Input/Output
The program [basics-09.cs] illustrates keyboard/screen input and output operations:
Results:
Comments:
- Line 4: [obj] is an object reference;
- Line 5: [obj] is displayed on the screen. The method [obj.ToString()] is called to retrieve the text to be displayed;
- line 9: the text is written to the [Error] stream;
- line 10: the same thing, but with a format. The first parameter, "i={0}", is the display format; the other parameters are the expressions to be displayed. The {n} elements are positional parameters: at runtime, {n} is replaced by the value of the nth expression;
- lines 13–14: a prompt is displayed without a line break, then the line typed on the keyboard is read;
- line 15: in an interpolated string, a value like [null] would produce an empty string: no warning is issued.
In the results:
- line 2: the output generated by line 5 of the code. The method
*[obj.ToString()]displayed the type name of the variable*[obj]*:*[System.Object]*. The type [object] is a C# alias for the type .NET [System.Object]*; - Lines 3–4: The two entries for the [Error] stream appear on the screen as those for the [Out] stream.
3.4.4. Input/Output Redirection
Windows, Linux, and macOS recognize three standard devices:
- the standard input device, which by default refers to the keyboard and is numbered 0;
- the standard output device, which by default refers to the screen and is numbered 1;
- the standard error device, which is set to the screen by default and is numbered 2.
In C#, the write stream [Console.Out] writes to device 1, the write stream [Console.Error] writes to device 2, and the read stream [Console.In] reads data from device 0.
When you run a program in a command window, you can specify which devices will be 0, 1, and 2 for the program being executed. After the program arguments, you can redirect the standard input and output devices to files:
Redirection | Meaning |
0<in.txt or <in.txt | Standard input stream No. 0 is redirected to the file in.txt. In the program, the stream [Console.In] will therefore read its data from the file in.txt |
1>out.txt or >out.txt | redirects output #1 to the file out.txt. In the program, the stream [Console.Out] will therefore write its data to the file out.txt |
1>>out.txt or >>out.txt | Same as above, but the written data is appended to the current contents of the out.txt file |
2>err.txt | redirects output #2 to the file err.txt. In the program, the stream [Console.Error] will therefore write its data to the file err.txt |
2>>err.txt | Same as above, but the written data is appended to the current contents of the file err.txt |
1>out.txt 2>err.txt | Devices 1 and 2 are both redirected to files |
Note that to redirect a program’s input/output streams to files, the program does not need to be modified: the operating system determines the nature of devices 0, 1, and 2. Consider the following program, [basics-10.cs]:
The 2008 course generated an executable using Visual Studio and then ran it in a DOS window. Today, the command [dotnet run] is sufficient: the redirects are applied to the launched program. Here is a session in a Linux terminal (bash):
Comments:
- line 1: the string "test" is placed in the file in.txt;
- line 2: display the contents of the file in.txt for verification;
- line 4: run the program. The stream [In] is redirected to the file in.txt, the stream [Out] to the file out.txt, and the stream [Error] to the file err.txt. The execution does not result in any output;
- lines 5–6: The contents of the file out.txt show that the file in.txt was read and that writing to the stream [Out] was redirected to out.txt;
- lines 7–8: similar verification for the file err.txt;
- lines 9–10: only the [Error] stream is redirected: the screen displays only the write operation to the [Out] stream;
- lines 11–12: only the [Out] stream is redirected: the screen displays only the write operation on the [Error] stream.
It is clear that the streams [Out] and [Error] do not write to the same devices, since they could be redirected separately. The [Error] stream is intended for error messages: this allows you to redirect a program’s output to a file while still seeing its error messages on the screen.
The operators >, >>, and 2> are written the same way in the Windows command prompt ([cmd]), in PowerShell, and in bash. The < operator is recognized by [cmd] and bash, but not by PowerShell, which reserves it for future use: in PowerShell, it is used to send a file to a program’s standard input via a pipe (|). The following commands, provided for illustrative purposes (they were not executed for this course), do on Windows what the bash session above does:
3.4.5. Assigning the value of an expression to a variable
Here we are interested in the operation [variable = expression;]. The expression can be arithmetic, relational, Boolean, string, etc.
3.4.5.1. Interpretation of the assignment operation
The operation [variable = expression;] is itself an expression that is evaluated as follows:
- the right-hand side of the assignment is evaluated: the result is a value V;
- the value V is assigned to the variable;
- the value V is also the value of the assignment, now viewed as an expression.
Thus, the operation [V1 = V2 = expression] is valid. Since the = operator is evaluated from right to left, we get [V1 = (V2 = expression)]. The expression [V2 = expression] is evaluated and has the value V. Its evaluation caused V to be assigned to V2. The next = operator is then evaluated as [V1 = V]. The value of this expression is still V; its evaluation causes V to be assigned to V1. Thus, the operation [V1 = V2 = expression] is an expression whose evaluation:
- causes the value of [expression] to be assigned to the variables V1 and V2;
- returns the value of [expression].
This can be generalized to an expression of the form [V1 = V2 = ... = Vn = expression].
3.4.5.2. Arithmetic Expressions
The operators used in arithmetic expressions are as follows:
Operator | Role |
+ | addition |
- | subtraction |
* | multiplication |
/ | division: the result is the exact quotient if at least one of the operands is a real number. If both operands are integers, the result is the integer quotient. Thus, 5/2 equals 2 and 5.0/2 equals 2.5 |
% | remainder of division (modulo), regardless of the nature of the operands. The remainder has the same sign as the dividend: -5 % 2 equals -1 |
There are various mathematical functions. Here are a few:
Function | Role |
[double Sqrt(double x)] | square root |
[double Cos(double x)], [Sin], [Tan] | cosine, sine, tangent (angle in radians) |
[double Pow(double x, double y)] | x to the power of y |
[double Exp(double x)] | exponential |
[double Log(double x)], [Log10], [Log2] | natural logarithm, decimal logarithm, base-2 logarithm |
[Abs(x)], [Max(x, y)], [Min(x, y)] | absolute value, maximum, minimum (for all numeric types) |
[Floor(x)], [Ceiling(x)], [Truncate(x)] | lower integer part, upper integer part, truncation |
[Round(x)], [Round(x, n)] | rounding to the nearest integer, to n decimal places |
[Math.PI], [Math.E] | the constants π and e |
All these functions are defined in a .NET class named Math. When using them, you must prefix them with the name of the class in which they are defined. Thus, you would write:
The 2008 course included the complete definition of the [Math] class here: Today, it can be found in the online documentation for .NET, learn.microsoft.com/dotnet/api/system.math.
The [basics-11.cs] program is as follows:
Results:
Comments:
- line 3: the division of two integers is an integer division;
- line 5: the remainder has the same sign as the dividend; the % operator also applies to real numbers;
- line 8: dividing a real number by zero does not cause an error; it returns infinity (displayed as ∞) or the special value NaN (“Not a Number”) for 0/0. However, dividing an integer by zero throws a [DivideByZeroException] exception;
- lines 11–12: the example above;
- Line 18: [Floor] rounds down, [Ceiling] rounds up, [Truncate] rounds to zero;
- line 20: by default, [Math.Round] rounds a value that is exactly halfway to the nearest even number (“banker’s” rounding): 2.5 becomes 2 and 3.5 becomes 4;
- line 21: for the rounding method taught in school (2.5 becomes 3), specify the rounding mode;
- line 25: operator precedence, as shown below;
- line 29: a multiple assignment.
3.4.5.3. Order of Operations in Evaluating Arithmetic Expressions
The order of operations when evaluating an arithmetic expression is as follows (from highest to lowest priority):
Operators within the same block [ ] have the same precedence. When precedence is equal, evaluation proceeds from left to right: 10 - 4 - 3 evaluates to (10 - 4) - 3 = 3.
3.4.5.4. Relational Expressions
The operators are as follows: <, <=, ==, !=, >, >=. Their precedence is:
- >, >=, <, <=;
- ==, !=.
The result of a relational expression is the Boolean [false] if the expression is false, and [true] otherwise.
Comparing Two Characters
Let C1 and C2 be two characters. They can be compared using the operators <, <=, ==, !=, >, >=. In this case, it is their Unicode codes—which are numbers—that are compared. According to the Unicode order, the following relationships hold:
Comparison of Two Strings
They are compared character by character. The first inequality encountered between two characters results in an inequality of the same direction for the strings. Consider comparing the strings "Cart" and "Cat":
This last inequality allows us to conclude that "Cart" < "Cat." Consider comparing the strings "Cat" and "Catkin." They are equal until the string "Cat" is exhausted. In this case, the exhausted string is declared the "smaller" one. We therefore have the relation "Cat" < "Catkin."
Functions for Comparing Two Strings
You can use the relational operators == and != to test whether two strings are equal, or the method [Equals] of the class [System.String]. For the relations <, <=, >, and >=, the operators do not apply to strings: you must use the [CompareTo] method of the [System.String] class or the static [string.Compare] method. They return an integer:
- zero if the two strings are equal;
- positive if string #1 is greater than string #2;
- negative if string #1 is less than string #2.
There is a subtlety: [CompareTo] and [string.Compare] do not simply compare Unicode codes. They follow the sorting rules of the current culture, as in a dictionary: in French, “élan” comes before “face,” even though the code for ‘é’ (233) is greater than that for ‘f’ (102). To choose the comparison mode, [string.Compare] and [Equals] accept a parameter of type [StringComparison]:
- [StringComparison.Ordinal]: character-by-character comparison of Unicode codes. This is the fastest comparison and should be used for identifiers, keywords, and filenames;
- [StringComparison.OrdinalIgnoreCase]: same as above, ignoring case;
- [StringComparison.CurrentCulture] (the default for [CompareTo]): follows standard cultural conventions for sorting text intended for users.
The == operator and the [Equals] method without parameters perform an ordinal comparison.
The program [basics-12.cs] is as follows:
Results:
Comments:
- line 3: a relational expression;
- line 7: character comparison;
- line 8: the [(int)] conversion of a character returns its Unicode code;
- line 12: the variable [n] is assigned a negative integer because "cat" < "dog";
- line 13: the variable [equal] has the value [true] if the two strings are equal, [false] otherwise;
- Line 15: The operators == and != are used to check whether two strings are equal. In a string interpolation, \" represents a quotation mark;
- line 16: the string that is exhausted first is the shorter one;
- Lines 19–20: Comparisons based on French conventions: “élan” comes before “face,” and “Cat” comes after “cat” (when the number of letters is the same, lowercase letters come before uppercase ones);
- lines 22–23: ordinal comparisons: the result here is the difference between the codes of the first differing characters: 233 – 102 = 131 and 67 – 99 = –32;
- lines 25–27: when case is ignored, “Cat” and “cat” are equal.
3.4.5.5. Boolean expressions
The operators that can be used are AND (&&), OR (||), and NOT (!). The result of a Boolean expression is a Boolean value. Operator precedence:
- ! ;
- && ;
- ||.
Relational operators have higher precedence than the && and || operators. These two operators evaluate their right-hand operand only if necessary: in [a && b], b is not evaluated if a is false; in [a || b], b is not evaluated if a is true. This is called “short-circuit evaluation.” Therefore, it is safe to write [text != null && text.Length > 0].
3.4.5.6. Bitwise Operations
Let [i] and [j] be two integers:
Operation | Role |
i << n | Shifts i n bits to the left. The incoming bits are zeros |
i >> n | Shift i n bits to the right. If i is a signed integer ([sbyte], [short], [int], [long]), the sign bit is preserved |
i >>> n | unsigned right shift (C# 11): the incoming bits are zeros, even if i is signed |
i & j | performs the bitwise logical AND of i and j |
i | j | performs the bitwise logical OR of i and j |
~i | complements i to 1 |
i ^ j | performs the EXCLUSIVE OR operation on i and j bit by bit |
The program [basics-13.cs] is as follows:
Results:
Comments:
- line 4: the format {0:x4} displays parameter #0 in hexadecimal (x) with 4 digits (4);
- line 7: operations on the bits of a [short] result in a [int]; these are converted back to [short] to display 4 hexadecimal digits. Without conversion, ~i would be displayed as 8 digits (ffffff9b);
- line 10: the B16 format displays a 16-digit binary integer. We check, bit by bit, the logical AND of i and j;
- line 13: the [>>] shift preserves the sign bit of m, which is negative; the [>>>] shift fills with zeros.
3.4.5.7. Combination of Operators
[a = a + b] can be written as [a += b]; [a = a - b] can be written as [a -= b]. The same applies to the operators /, %, *, <<, >>, >>>, &, |, ^. Thus, [a = a / 2;] can be written as [a /= 2;].
3.4.5.8. Increment and Decrement Operators
The notation [variable++] means [variable = variable + 1] or [variable += 1]. The notation [variable--] means [variable = variable - 1] or [variable -= 1]. When used in an expression, these operators have two forms:
- [i++] (postfix): the value of the expression is the value of i before the increment;
- [++i] (prefixed): the value of the expression is the value of i after the increment.
3.4.5.9. The ternary operator ?:
The expression [expr_cond ? expr1 : expr2] is evaluated as follows:
- The expression [expr_cond] is evaluated. This is a Boolean expression;
- if it is true, the value of the expression is that of [expr1], and [expr2] is not evaluated;
- if it is false, the opposite occurs: the value of the expression is that of [expr2], and [expr1] is not evaluated.
The operation [i = (j > 4 ? j + 1 : j - 1);] will assign j+1 to the variable i if j > 4, and j-1 otherwise. This is the same as writing [if (j > 4) i = j + 1; else i = j - 1;], but it is more concise.
3.4.5.10. The operators ??, ??=, and ?.
These operators make it easier to work with values [null]:
- [a ?? b] (C# 2) evaluates to a if a is not equal to [null], and b otherwise. It is used to provide a fallback value: [string name = Console.ReadLine() ?? "";]. The result is no longer nullable;
- [a ??= b] (C# 8) assigns b to a only if a is equal to [null];
- [a?.Member] (C# 6) is [null] if a is [null], otherwise [a.Member]. Thus, [text?.Length] does not cause an error when [text] is equal to [null]; the result is of type [int?].
The program [basics-14.cs] combines these different operators:
Results:
Comments:
- line 4: a Boolean expression;
- lines 9–17: combined operators: 10+5=15, 15*2=30, 30/4=7 (integer division), 7%4=3, 3<<2=12;
- line 22: [j] is assigned 5, then [i] is set to 6;
- line 23: [i] goes to 7, then [k] receives 7;
- lines 30–31: two ternary operators;
- line 36: [input] equals [null]: [name] receives "unknown". The type of [name] is [string], not [string?];
- line 38: the ?? operator allows you to convert from a [int?] to a [int] without risking an exception;
- Lines 42–43: The first assignment occurs because [title] is equal to [null]; the second assignment does not occur;
- line 47: [text] equals [null], so [length] does too;
- line 48: the operators are combined: if [length] equals [null], the string "null" is displayed.
3.4.5.11. General Operator Precedence
The following table lists the operator precedence, from highest to lowest:
Operators | Associativity |
[x.y] [x?.y] [f(x)] [a[i]] [x++] [x--] [new] [typeof] [checked] [nameof] | lr |
[+x] [-x] [!x] [~x] [++x] [--x] [^x] [(T)x] (type change) | rl |
[x..y] (range) | lr |
[switch] (switch expression) | lr |
[*] [/] [%] | lr |
[+] [-] | lr |
[<<] [>>] [>>>] | lr |
[<] [<=] [>] [>=] [is] [as] | lr |
[==] [!=] | lr |
[&] | lr |
[^] | lr |
[|] | lr |
[&&] | lr |
[||] | lr |
[??] | rl |
[? :] (ternary) | rl |
[=] [+=] [-=] [??=] etc., [=>] | rl |
lr indicates that, when operators have equal precedence, left-to-right precedence is observed: when an expression contains operators of the same precedence, the operator farthest to the left is evaluated first. rl indicates right-to-left precedence. When in doubt, don’t hesitate to use parentheses: they also make the expression more readable.
3.4.5.12. Type Casting
In an expression, it is possible to temporarily change the representation of a value. This is called “type casting.” The syntax for changing the type of a value in an expression is as follows:
The value then takes on the specified type. This results in a change in the value’s representation. We distinguish between:
- Implicit conversions, performed automatically by the compiler when they do not result in any loss of information: from a [int] to a [long], from a [int] to a [double], from a [char] to a [int], and so on. The conversion from a [long] to a [double] is also implicit, although it may lose precision beyond 2⁵³;
- explicit conversions, which may result in a loss of information and must be explicitly requested by the programmer using the (type) notation: from a [double] to a [int] (the decimal part is lost), from a [long] to a [int] (the most significant bits are lost), etc.
By default, an overflow in an integer calculation or an integer conversion is not reported: the result is simply false. The keyword [checked], on the other hand, causes an overflow to trigger a [OverflowException] exception. It applies to an expression, [checked(expression)], or to a block of instructions, [checked { ... }].
The program [basics-15.cs] is as follows:
Results:
Comments:
- Line 3: [f1] will have the value 0. The division 3/4 is an integer division since both operands are of type [int]; its result, 0, is then converted to [float];
- Line 4: [(float)i] is the value of [i] converted to [float]. We now have a division between a real number of type [float] and an integer of type [int]: the value of [j] is also transformed into [float], and then the two real numbers are divided. [f2] has a value of 0.75. In the operation [(float)i], [i] is a value encoded exactly as a 4-byte number, while [(float)i] is the same value encoded as an approximate 4-byte real number. This transcoding occurs only during the calculation: the variable [i] always retains its type [int];
- lines 8–9: implicit conversions;
- line 14: converting a floating-point number to an integer truncates the decimal part;
- line 15: to round, we use [Math.Round], which yields a [double], then we convert;
- line 18: 3 billion does not fit into a [int]: only the least significant 32 bits are retained, and the result no longer makes sense;
- Lines 22–23: Conversions between characters and Unicode codes; the second is implicit;
- line 28: the capacity overflow goes unnoticed: the smallest possible [int] is obtained;
- line 33: with [checked], the overflow triggers a [OverflowException] exception;
- lines 42–45: a block of [checked]: the conversion of [large] to [int] also throws an exception.
If the overflow occurs in an expression consisting solely of constants, such as [int.MaxValue + 1], the compiler detects it and rejects the program.
3.5. Program Flow Control Statements
3.5.1. Stop
The [Exit] method of the [Environment] class allows you to stop the execution of a program:
[Exit] terminates the current process and returns control to the calling process, which is typically the command interpreter. The value of [status], known as the return code, can be used by the command interpreter. By convention, 0 indicates that the program completed successfully, while any other value indicates an error. On Windows, the return code is available in the [%ERRORLEVEL%] variable at the command prompt and in the [$LASTEXITCODE] variable within PowerShell; on Linux and macOS, it is available in the [$?] variable of the shell. Thus:
will terminate the program with a return code of 0. With high-level statements, a [return] statement also terminates the program (return code 0), unless it is located within a function.
The program [basics-16.cs] receives its return code as an argument:
Results in a bash terminal:
Comments:
- line 4: We check that an argument has been passed and that it is an integer. The program’s arguments are described later;
- line 7: if this is not the case, the program terminates with a return code of 1;
- line 8: this instruction will never be executed, but the compiler does not know that [Environment.Exit] never returns control. Without it, the compiler would assume that the next line could be reached with an uninitialized [code] variable and would reject the program;
- line 11: terminates with the return code passed as an argument. The following line is not executed;
- lines 4–9: The command [echo $?] displays the return code of the last program executed: 3, then 1.
3.5.2. Simple decision structure
Notes:
- the condition is enclosed in parentheses;
- each action ends with a semicolon;
- curly braces are not followed by a semicolon;
- curly braces are only required if there is more than one action; however, they are often included even for a single action, for readability and to avoid errors when adding an action;
- the clause [else] may be omitted;
- there is no [then] keyword.
The algorithmic equivalent of this structure is the if...then...else structure:
Example:
Choice structures can be nested. This sometimes leads to the problem illustrated by the program [basics-17.cs]:
Results:
Comments:
- Lines 4–8: Which [if] does the [else] on line 7 refer to? The rule is that a [else] always refers to the nearest [if]: [if (n > 6)], line 5. The code indentation, which has no effect on the compiler, was chosen to illustrate this;
- lines 12–21: here, we wanted to place a [else] next to [if (n2 > 1)], and not a [else] next to [if (n2 > 6)]. Because of the previous rule, we are required to enclose [if (n2 > 1) { ... }] in curly braces;
- lines 27–41: a sequence of mutually exclusive choices is written using successive [else if]. The conditions are tested in order; the first true condition determines the action to be executed.
3.5.3. Case Structure
3.5.3.1. The switch statement
The syntax is as follows:
Notes:
- The value of the control expression in [switch] can be an integer, a character, a character string, an enumeration value, etc.;
- the control expression is enclosed in parentheses;
- The clause [default] may be absent;
- the values vi are possible values of the expression. If the expression evaluates to vi, the actions following the clause [case vi] are executed;
- the statement [break] exits the case structure;
- Each block of statements associated with a value vi must end with a branch statement ([break], [return], [throw], ...); otherwise, the compiler reports an error. However, multiple [case] clauses can follow one another without any instructions between them: in this case, they share the same actions.
Example in algorithmics:
3.5.3.2. The switch expression
Very often, a case structure is used solely to select a value. C# 8 introduced the switch expression for this purpose:
The expression is compared successively to the patterns; the value associated with the first matching pattern is the result. The pattern \_ matches all values: it acts as a wildcard. A pattern can be:
- a constant: [0], ["saturday"];
- a relational pattern (C# 9): [< 0], [>= 18];
- a combination of patterns with [and], [or], and [not] (C# 9): [> 0 and <= 10], ["saturday" or "sunday"].
These same patterns are used with the operator [is]: [day is "saturday" or "sunday"] is true if [day] is "saturday" or "sunday". We will see other forms of these in the following chapters.
The program [basics-18.cs] is as follows:
Results:
Comments:
- lines 4–15: the C# implementation of the algorithm above;
- line 7: in the high-level instructions, [return] terminates the program;
- lines 24–25: two values for the same actions;
- lines 34–38: a switch expression on a string. The patterns are combined with [or];
- line 49: the pattern [not] with the operator [is];
- lines 53–60: two local functions;
- lines 63–69: a local function whose body is a switch expression. The => notation replaces the body enclosed in curly braces and the statement [return]; this is referred to as an expression body. The relational patterns classify the integer [n].
3.5.4. Repetition Structures
3.5.4.1. Known number of repetitions
For structure
The syntax is as follows:
Notes:
- The three arguments of [for] are enclosed in parentheses and separated by semicolons;
- Each action in [for] is terminated by a semicolon;
- curly braces are only required if there is more than one action;
- the curly brace is not followed by a semicolon.
The algorithmic equivalent is the structure for:
which can be translated into a while structure:
Foreach structure
The syntax is as follows:
Notes:
- [collection] is an enumerable collection of objects. The enumerable collection we are already familiar with is the array; we will see others ([List<T>], [Dictionary<K, V>], ...) in Chapter 5;
- [Type] is the type of the objects in the collection. For an array, this would be the type of the array elements. We can write [var] instead;
- [variable] is a local variable for the loop that will successively take on all the values in the collection. It cannot be modified within the loop.
3.5.4.2. Unknown number of iterations
While loop
The loop continues as long as the condition is true. The loop may never be executed. The notes are the same as for [for]. The corresponding algorithmic structure is the while structure:
"Do While" Loop
The syntax is as follows:
The loop continues until the condition becomes false. Here, the loop is executed at least once. The corresponding algorithmic structure is the “repeat … until” structure:
General loop structure (for)
The syntax is as follows:
The loop continues as long as the condition is true (evaluated before each iteration). The [start_instructions] statements are executed before entering the loop for the first time. The [end_of_loop_instructions] statements are executed after each iteration of the loop. The various instructions in [start_instructions] and [end_of_loop_instructions] are separated by commas. The corresponding algorithmic structure is as follows:
3.5.4.3. Loop Control Statements
Instruction | Role |
[break] | Exits the loop [for], [foreach], [while], [do ... while] |
[continue] | advances to the next iteration of the loops [for], [foreach], [while], [do ... while] |
3.5.4.4. Examples
The program [basics-19.cs] is as follows:
Results:
Comments:
- lines 3–30: four ways to calculate the sum of the first 10 integers;
- lines 4–8: a classic [for];
- lines 10–13: all the work is done in the loop-ending statements; the loop body is empty. The 2008 course used [for (...) ;]: C# accepts this empty statement, but the compiler issues a warning (“possible invalid empty statement”), because a semicolon mistakenly placed after a [for] is a classic error. An empty block { } expresses the same thing unambiguously;
- line 28: [i++] returns the value of [i] before it is incremented: [i] is added to [sum], then [i] is incremented;
- line 34: the most common form of [for]: the loop variable is declared in [for] and exists only within the loop;
- lines 41–42: the loop [foreach] iterates through the elements of the array [friends];
- lines 51–58: [continue] moves to the next iteration for even numbers; [break] terminates the loop as soon as [k] exceeds 7;
- Lines 66–74: An infinite loop [while (true)] that exits via [break]. This form is convenient when the termination condition is calculated in the middle of the loop. We will use it for keyboard input.
3.6. Exception Handling
Many .NET methods are likely to throw exceptions, i.e., errors. When a method is likely to throw an exception, the programmer should handle it to make programs more error-tolerant: you should always avoid a sudden termination of an application.
3.6.1. The try/catch/finally Structure
Exception handling follows this pattern:
If the code in the [try] block does not throw an exception, execution proceeds to the next statement; otherwise, execution proceeds to the body of the [catch] block, then to the next statement. Note the following points:
- [e] is an object of type [Exception] or a derived type. There are many types of exceptions: [IndexOutOfRangeException], [FormatException], [OverflowException], [ArgumentException], etc. By writing [catch (Exception e)], you indicate that you want to handle all types of exceptions. If the code for the [try] clause is likely to generate multiple types of exceptions, you may want to be more specific by handling the exception with multiple [catch] clauses:
The [catch] clauses are evaluated in order; the first one whose type matches the exception is executed. A [catch (Exception e)] clause must therefore be placed last;
- a clause [finally] can be added to the clauses [try / catch]:
Whether or not an exception occurs, the code in the [finally] clause is always executed. For example, you can use it to close a file;
- in clause [catch], you may not want to use the available object [Exception]. Instead of writing [catch (Exception e) { ... }], you would then write [catch (Exception) { ... }] or, more simply, [catch { ... }];
- the class [Exception] has a property [Message] that details the error that occurred. To display it, write:
- The class [Exception] has a method [ToString] that returns a string indicating the exception type, the value of the [Message] property, and the call stack—that is, the list of methods that were executing at the time of the error, along with the source file name and line number. This is the most useful information for the developer, but it is not intended for the user. In this course, we will most often display the name of the exception type, [ex.GetType().Name], and its message.
3.6.2. A First Example
The program [basics-20.cs] demonstrates an exception generated by accessing a nonexistent array element:
Results:
Comments:
- line 16: this statement throws an exception because the array [tab] does not have an element at index 100;
- line 20: the exception is of type [System.IndexOutOfRangeException] (line 10 of the results);
- line 21: the program exits;
- Lines 23–25: The clause [finally] was executed (line 11 of the results), even though the clause [catch] contained a statement [return]. Note that the [finally] clause is always executed;
- line 27: because of [return], this line is not executed.
3.6.3. A controlled entry
Here is another example, [basics-21.cs], where we handle the exception caused by converting a string that does not represent an integer to an integer:
Results:
Comments:
- line 4: if the input stream is closed, [ReadLine] returns [null]: it is replaced with an empty string. The variable [name] can therefore be of type [string];
- Lines 8–23: the age input loop;
- line 15: the line entered via the keyboard is converted to an integer using the [int.Parse] method. This method throws an exception if the conversion is not possible. This is why the operation was placed within a [try / catch];
- line 16: If this line is reached, it means the conversion was successful. However, we verify that the resulting integer is greater than or equal to 1;
- line 18: if a [FormatException] exception is thrown, control flows to [catch], where no action is taken. Thus, the Boolean variable [ageOK], initialized to [false], remains at [false]. The 2008 course code wrote [catch { }], which catches all exceptions: this is not recommended, because it also masks errors that were not anticipated;
- lines 21–23: An error message is displayed if the age is incorrect.
This program has a weakness: an age such as 99999999999 causes a [OverflowException] exception, which is not caught and causes the program to terminate. We could add a [catch] clause; however, it is simpler to use [int.TryParse], as the example application below will do. We will reserve exceptions for truly exceptional situations.
3.6.4. Multiple catch clauses, filters, and exception throws
The program itself can throw an exception using the [throw] statement:
Execution of the current method stops, and the exception is propagated up through the calling methods until a [catch] clause handles it; if none does, the program terminates and displays the exception. In a [catch] clause, the [throw;] statement (without parameters) rethrows the current exception, for example, after logging it.
Starting with C# 6, a [catch] clause can be followed by a [when (condition)] filter: the clause is only executed if the condition is true. Starting with C# 7, [throw] can also be used as an expression, to the right of the ?? operator or within a ternary operator.
The program [basics-22.cs] illustrates these possibilities:
Results:
Comments:
- line 2: simulated entries, some valid, some not;
- line 7: the local function [ReadAge] can throw several types of exceptions;
- line 11: this clause only handles a [FormatException] if the entry is empty;
- line 15: the other [FormatException];
- line 19: a number that is too large for a [int];
- line 23: the exception thrown by [ReadAge] itself;
- line 35: an expression [throw]: if [name] is equal to [null], a [ArgumentNullException] exception is thrown. The operator [nameof] (C# 6) returns the name of a variable as a string; [ArgumentNullException] adds it to its message (line 9 of the results);
- Line 52: The [ReadAge] function throws an exception if the age is out of bounds.
3.7. Sample Application - V1
We aim to write a program to calculate a taxpayer’s tax liability. We’ll consider the simplified case of a taxpayer who has only a single salary to report. The figures are taken from the 2004 tax schedule for 2003 income and are used as-is: this is a programming exercise, not a calculation of current taxes. This application will serve as our guiding thread: we will revisit it in the following chapters, incorporating classes, files, a database, and then network functionality.
- We calculate the number of shares for the employee: shares = nbChildren/2 + 1 if he is unmarried, nbChildren/2 + 2 if he is married, where nbChildren is the number of his children;
- if he has at least three children, he receives an additional half-share;
- his taxable income R is calculated as R = 0.72 * S, where S is his annual salary;
- his family quotient is calculated as QF = R / shares;
- his tax I is calculated using the following table:
threshold | coeffR | coeffN |
4962 | 0 | 0 |
8382 | 0.068 | 291.09 |
14,753 | 0.191 | 1,322.92 |
23,888 | 0.283 | 2,668.39 |
38,868 | 0.374 | 4,846.98 |
47,932 | 0.426 | 6,883.66 |
0 | 0.481 | 9,505.54 |
Each row has 3 fields. To calculate tax I, find the first row where QF is less than or equal to the limit. For example, if QF = 5,000, the row found will be:
Tax I is then equal to 0.068 * R - 291.09 * shares, truncated to the euro. If QF is such that the condition QF ≤ limit is never satisfied, the coefficients from the last line are used: I = 0.481 * R - 9505.54 * shares. The limit 0 in the last row is therefore never used.
The corresponding C# program [tax-v1.cs] is as follows:
Comments:
- Lines 5–7: Numeric values are suffixed with M to be of type [decimal]. The [decimal] type encodes decimal numbers exactly, such as 0.068; this is the type used for financial calculations. With a [double], 0.068 would be only a binary approximation;
- lines 10–21: entering marital status, within a [while (true)] loop that exits via [break] when the answer is correct;
- line 14: [Console.ReadLine()] returns the string C1 as typed on the keyboard, or [null] (replaced by ""); [C1.Trim()] removes the leading and trailing spaces from C1 and returns the string C2; [C2.ToLower()] returns the string C2 converted to lowercase. This accepts both Y and y;
- line 15: a pattern combined with [or];
- line 19: error messages are written to the stream [Error];
- line 21: the Boolean [married] receives the value [true] or [false] from the relation [answer == "y"];
- line 28: [TryParse] returns [false] if the input is not an integer; the condition [nbChildren >= 0] is then not evaluated (the && operator is short-circuited). [TryParse] accepts a string [null]: it then returns [false]. Unlike the 2008 version, no exceptions are used;
- line 40: same applies to the salary;
- Line 48: You cannot simply write nbChildren / 2: if nbChildren is equal to 3, you would get 3/2, an integer division that would result in 1, not 1.5. Since the divisor 2M is of type [decimal], the division is performed as [decimal]. The 2008 course material used [(decimal)nbChildren / 2], which amounts to the same thing;
- Lines 62–65: determining the tax bracket. The calculation stops at the first threshold that is greater than or equal to the family quotient, and no later than the last tax bracket. The 2008 version initially wrote QF in the last cell of the limit table to ensure it stopped: this altered the tax schedule data; the condition [i < limits.Length - 1] avoids this side effect;
- Line 69: The tax is truncated to the euro by converting it to [int].
Here are some examples of the output:
These are the same results as in the 2008 course: 4,282 euros for a married taxpayer with 2 children and 2,959 euros with 3 children, for an annual salary of 60,000 euros. A single person with no children and the same salary pays 11,519 euros. In the second run, the input “60000 euros” is rejected: [int.TryParse] accepts only strings that represent an exact integer (leading and trailing spaces are allowed).
3.8. Main Program Arguments
The main function [Main] can accept an array of strings [string[]] as a parameter. This array contains the command-line arguments used to launch the application. Thus, if program P is launched with the command:
and if the function [Main] is declared as follows:
then we will have args[0]="arg0", args[1]="arg1", and so on. With high-level statements, there is no [Main] method written by the programmer, but the array [args] is available in the same way: it is the parameter of the [Main] method generated by the compiler.
With [dotnet run], the arguments for the program are placed after two dashes ([--]), which separate them from the options of the [dotnet run] command itself:
The 2008 course specified the arguments in the Visual Studio project properties; today, the command line is sufficient. Here is an example, [basics-23.cs]:
Results:
An argument that contains spaces must be enclosed in quotation marks:
The same program written using a traditional [Main] method: [basics-24.cs]:
Results:
Comments:
- line 2: a file-scope namespace (C# 10);
- line 4: a static class contains only static members;
- line 6: the program's entry point. Since this file contains no high-level statements, the compiler looks for a static method named [Main];
- line 8: the rest is identical to [basics-23.cs].
The signature [static void Main()] is valid if the function [Main] takes no parameters. [Main] can also return a [int], which is then the program’s return code: [static int Main(string[] args)].
3.9. Enumerations
An enumeration is a data type whose value domain consists of a set of named integer constants. Consider a program that needs to handle exam grades. There are five possible grades: Pass, FairlyGood, Good, VeryGood, and Excellent. We can then define an enumeration for these five constants:
Internally, these five constants are encoded as consecutive integers starting with 0 for the first constant, 1 for the next, and so on. We can also set these values explicitly: [enum Honours { Pass = 10, FairlyGood = 12, ... }]. A variable can be declared to take its values from the enumeration:
A variable can be compared to the different possible values in the enumeration:
Just as the simple type [int] is equivalent to the structure [System.Int32], every enumeration derives from the class [System.Enum]. This class has static methods that allow you to retrieve the values and names of an enumerated type:
- [Enum.GetValues<Honours>()] (.NET 5) returns the array of values for the enumeration [Honours];
- [Enum.GetNames<Honours>()] returns the array of their names;
- [Enum.TryParse(str, out Honours m)] converts a string into an enumeration value.
The 2008 course described [Enum.GetValues(typeof(Honours))]: this form, which is still valid, returns an untyped array that must then be converted. The generic form [GetValues<Honours>()] directly returns an array of type [Honours[]].
The program [basics-25.cs] illustrates what has just been described:
Results:
Comments:
- line 3: a variable of type [Honours];
- line 5: an enumeration value is displayed by its name;
- lines 12–14: the list of values. The [(int)m] conversion returns the integer value of the constant;
- line 17: the list of names;
- line 19: the reverse conversion, from an integer to the enumeration;
- line 22: the conversion of a string to an enumeration value. Enumeration values are compared like the integers that encode them;
- lines 27–31: enumerations work well with switch statements;
- line 34: the underlying integer type is [int] by default;
- line 37: the declaration of the enumeration. In a file containing high-level statements, type declarations (enumerations, classes, etc.) are placed after the statements.
An enumeration whose values are powers of 2 (1, 2, 4, 8, ...) can be assigned the attribute [Flags]: its values are then combined using the | operator (bitwise OR) to represent a set of options, for example the access rights to a file (read | write).
3.10. Passing Parameters to a Function
Here we focus on how parameters are passed to a function. Consider the following function:
In the function definition, on line 1, [a] is called a formal parameter. It is included solely for the purposes of defining the function [ChangeInt]. It could just as easily have been named [b]. Now let’s consider an example of how this function is used:
Here, in the statement on line 2, [age] is the actual parameter that will pass its value to the formal parameter [a]. We are interested in how a formal parameter retrieves the value of an actual parameter.
3.10.1. Passing Modes
C# supports the following passing modes:
- by value (the default mode): the value of the actual parameter is copied into the corresponding formal parameter. These are two distinct entities: if the function modifies the formal parameter, the actual parameter remains unchanged. This mode is suitable for a function’s input parameters;
- by reference with [ref]: the actual parameter and the formal parameter are one and the same entity. If the function modifies the formal parameter, the actual parameter is also modified. The keyword [ref] must appear in both the function definition and the call. The actual parameter must have a value before the call. This mode is suitable for input-output parameters;
- by reference with [out]: same as [ref], but the parameter is an output-only parameter: the actual parameter does not need an initial value, and the function must assign a value to the formal parameter. This is the mode used by the [TryParse] methods. Starting with C# 7, the variable can be declared in the call: [out var x] or [out int x];
- by read-only reference with [in] (C# 7.2): the parameter is passed by reference, which avoids copying, but the function cannot modify it. This mode is only useful for large structures; for predefined types, passing by value is just as efficient.
The program [basics-26.cs] illustrates these different modes:
Results:
Comments:
- Lines 3–5: The value 20 of the actual parameter [age] was copied to the formal parameter [a] (line 35). This parameter was then modified (line 37). The actual parameter remained unchanged;
- lines 8–10: With [ref], the actual parameter [age2] followed the modification of the formal parameter [a2];
- lines 13–15: the variable [age3] is not initialized before the call; this is possible with [out];
- line 17: the variable [age4] is declared in the call;
- lines 21–22: Swapping two values requires passing by reference; with passing by value, the function would swap copies;
- line 27: a read-only pass. The keyword [in] is optional in the call. The result 120.0 has one decimal place because the product of two [decimal] values preserves the decimal places of its operands: 100 × 1.2 = 120.0;
- Lines 30–32: An array is a reference type; what is passed by value is the array’s reference. The function [ModifyArray] therefore operates on the same array as the caller: the modification made by [t[0]] (line 69) is visible to the caller. However, setting [t] to point to another array (line 71) only modifies the local copy of the reference.
If line 8 is replaced with [int age2;], the compiler rejects the program:
This issue can be circumvented by assigning an initial value to [age2], or by replacing the keyword [ref] with the keyword [out]: this indicates that the parameter is solely an output parameter. Similarly, if the function [InclTax] attempts to modify its parameter [in]:
3.10.2. Optional parameters, named arguments, variable number of parameters
C# 4 introduced optional parameters: a parameter can be given a default value in the function definition; the caller can then omit it. Optional parameters are placed after the required parameters. It also introduced named arguments: when calling the function, an actual parameter can be preceded by the name of the formal parameter followed by a colon. This allows an optional parameter that precedes other parameters to be omitted, or for parameters to be provided in any order.
The keyword [params] allows you to write a function with a variable number of parameters: the last formal parameter is an array, and the caller can pass either an array or a list of values (which may be empty), which the compiler arranges into an array. Starting with C# 13, the parameter [params] can also be of another collection type, such as [List<T>] or [ReadOnlySpan<T>].
The program [basics-27.cs] is as follows:
Results:
Comments:
- Line 17: The function [Greet] has one required parameter, [name], and two optional parameters, [greeting] and [uppercase];
- Line 3: Both optional parameters take their default values;
- line 4: only [uppercase] takes its default value;
- line 6: using the named argument, we pass [uppercase] without passing [greeting];
- line 7: With named arguments, the order is arbitrary;
- line 23: [Sum] accepts any number of integers;
- lines 10–13: [Sum] is called without any parameters, with three integers, and then with an array;
- Line 15: Many .NET methods, such as [string.Join] or [Console.WriteLine] with a format, have a [params] parameter.
3.11. Local Functions and Tuples
3.11.1. Local functions
A local function (C# 7) is a function defined within a method or, as in our examples, among top-level statements. It can only be used in that location. Since our programs no longer have classes, local functions replace the static methods from the 2008 examples. A local function:
- can be called before it is defined;
- can use the local variables surrounding it, unless it is declared as [static]: the [static] keyword (C# 8) ensures that it depends only on its parameters, making it easier to understand. We declare our local functions [static] whenever possible;
- may have an expression body: [static int Square(int x) => x * x;] is equivalent to a body like [{ return x * x; }].
In a larger program, we write classes and methods: this is the subject of the next chapter.
3.11.2. Tuples
A tuple (C# 7) groups multiple values—which may be of different types—into a single entity. The type [(int, string)] denotes a pair consisting of an integer and a string; its elements are called [Item1] and [Item2]. They can be named: [(int Number, string Name)]. A tuple can be deconstructed: [var (number, name) = t;] assigns its elements to two variables.
Tuples allow a function to return multiple values without using parameters [out]: this is often more readable.
The program [basics-28.cs] is as follows:
Results:
Comments:
- line 3: a static local function with an expression body;
- lines 7–8: a non-static local function: it modifies the program variable [counter];
- line 14: a tuple whose elements are unnamed. When a tuple is printed, its elements appear in parentheses;
- lines 17–18: two tuples with named elements: the first by its type, the second by the names given in the expression;
- line 21: the unpacking of the tuple [t3];
- line 25: the function [Divide] returns the quotient and remainder of the integer division; the result is unpacked directly;
- line 27: here, the tuple is retained, and the names of its elements, [Min] and [Max], are used;
- line 30: From .NET 6, [Math.DivRem] also returns a tuple (quotient, remainder);
- line 35: swapping two variables can be written on a single line using a tuple, without a temporary variable;
- line 38: the type returned by the function is a named tuple;
- lines 40–48: a function that calculates the minimum and maximum of an array in a single pass.