Week 2: Notes

standard library methods

In the last lecture we learned about 5 built-in types in C#: int, long, bool, char, and string. Actually each of these names is an abbreviation for a longer name:

You may use either the short or long names in code:

int i = 44;
System.Int32 i = 44;   // equivalent

Our C# quick reference is organized using the long names, and lists some useful constants and methods for these types.

For example, the section System.Int32 lists constants and methods related to the int type. The MinValue and MaxValue constants may be useful:

int min = int.MinValue;    // smallest possible int
int max = int.MaxValue;    // largest possible int

The quick reference also lists some helpful methods for converting between strings and integers:

static int Parse(string s);
Parse an integer from a string. Throws a FormatException if the string does not contain a valid integer.
string ToString();
Convert an integer to a string.

Notice that Parse() is a static method. That means that we invoke it on the int type directly:

int x = int.Parse("345");

ToString() is not marked with static, so it is an instance method. We invoke it on an instance of the int type, i.e. an integer itself:

int i = 345;
string s = i.ToString();

Later in this course we'll see how to write instance methods and static methods in our own classes.

Last week we also learned about arithmetic operators (+, -, *, /). Note that the + operator concatenates strings:

string s = "one two";
string t = "three four";
string u = s + " " + u;  // "one two three four"

We also learned about comparison operators such as ==, !=, < and >. It may be suprising that you cannot compare strings using operators such as < and >:

if ("chameleon" < "dog")  // ERROR: operator '<' can't be applied to strings
    Console.WriteLine("less");

That's because the comparison order of strings may be language-dependent. For example, "chameleon" appears before "dog" in an English dictionary, but in a Czech dictionary "chameleon" appears later since in Czech "ch" is considered to be a single letter that comes after "h" in the alphabet. So C# disallows this comparison. (Many other languages such as Python are more liberal in this respect and will allow strings to be compared using < or >.)

preincrement and postincrement operators

C# includes two operators for preincrementing or postincrementing a value:

When you use these operators as statements, they are equivalent:

int x = 4;
++x;    // now x is 5
x++;    // now x is 6

However when you use them inside an expression they behave differently. The value of the expression (x++) is the value of x before it is incremented. Similarly, the value of (++x) is the value of x after it is incremented:

int x = 4;
int y = x++;  // now x is 5, and y is 4
int z = ++x;  // now x is 6, and z is 6

It's common to use one of these operators as the iterator in a for loop:

for (int i = 0 ; i < 10 ; ++i)
    Console.WriteLine(i);

In this situation the decision whether to use "++i" or "i++" is a question of style, since they are equivalent.

unsigned types

Last week we learned about the types int (a signed 32-bit integer) and long (a signed 64-bit integer). Here are two more integer types:

arithmetic overflow

Because integer types in C# (unlike Python) have a fixed size, an integer calculation may overflow, i.e. go past the range of allowable values. In this case the higher-order bits of the value will be lost, which effectively computes the value mod 2b for a type with b bits. For example:

uint i = 2_000_000_000;
i *= 3;
Console.WriteLine(i);

6,000,000,000 is outside the range of a 32-bit unsigned integer, so the calculation will overflow and this program will print

1705032704

This value is 6,000,000,000 mod 232.

Overflow for a signed integer type may even produce a negative value:

int i = 1_000_000_000;
WriteLine(i * 3);   // writes -1294967296

numeric and character conversions

C# will perform an implicit conversion between two numeric types if every possible value of the source type is valid in the destination type. For example:

int i;
...
long l = i;   // implicit conversion

This sort of conversion is called a widening conversion because, for example, a long has a wider range than an int.

You can use an explicit conversion to force a conversion between any two numeric types. This is accomplished with a type cast:

ulong l = 5_000_000_000;
uint i = (uint) l;

If the destination type cannot hold the source value, it will wrap around or be truncated. In the example above c will be 705,032,704, which is 5,000,000,000 mod 232.

The conversion from ulong to uint is a narrowing conversion because uint has a narrower range than ulong. C# requires a type cast for any narrowing conversion.

You can implicitly convert a char to any numeric type that can hold a 16-bit unsigned value:

char c = 'ř';
int i = c;   // now i holds the Unicode value for 'ř', i.e. 345

You can explicitly convert any numeric type to a char:

int i = 100;
char c = (char) i;  // now c is 'd', which is ASCII/Unicode character 100

floating-point types

Here are two built-in types that represent floating-point numbers:

The type of a floating-point constant (e.g. 3.4) is double by default, so this will fail:

float f = 3.4;   // error: can't implicitly convert double to float

To create a constant of type float, append the character 'f':

float f = 3.4f;  // OK

I generally recommend using double instead of float, since 64-bit floating-point operations are inexpensive on modern CPUs. (On the other hand, if you have a large array of floating-point values, you can save memory by using float as the element type if you don't care about the extra precision of a double.)

Be careful with division. The following code will print 0, not 0.6:

int i = 3, j = 5;
double d = i / j;       // integer division!
Console.WriteLine(d);   // prints 0

That's because the division operator / will perfom an integer division when given two integer values. To perform a floating-point division, use a type cast:

int i = 3, j = 5;
double d = (double) i / j;  // floating-point division
Console.WriteLine(d);       // prints 0.6

Alternatively, you may multiply by 1.0 to convert an integer to a double:

double d = 1.0 * i / j;  // floating-point division

Note that a float or double may hold the special values NegativeInfinity or PositiveInfinity, which are sometimes useful. For example:

double d = double.PositiveInfinity;

null and nullable types

C# includes a special value null, which is like None in Python. It represents the absence of a value.

Most types cannot hold a null value:

int x = null;    // error: cannot convert null to int

However, you can add the suffix ? to any type to make it nullable. For example, int? is a type that holds either an int, or null:

int? x = null;
int? y = 7;

reading input

The Console.ReadLine() method reads a line of standard input. It returns a value of type string?, which is either a string, or is null (in the case that no more input is available).

So we can write

string? s = Console.ReadLine();
if (s == null)
    Console.WriteLine("no more input");
else
    Console.WriteLine(s);

To keep things simple, often we don't want to worry about the null case. Ad so we can use the null-forgiving operator !, like this:

string s = Console.ReadLine()!;

This operator is like an assertion that a value will not be null. If it actually is null at run time, the program will fail with an exception. After applying this operator to a value of type (string?), we can successfully assign it to a variable of type string.

string interpolation

A string beginning with $ may contain interpolated values enclosed in braces (just like f-strings in Python):

int a = 3, b = 4;
Console.WriteLine($"{a} plus {b} equals {a + b}");

A interpolated value may be followed by a format specifier (preceded by a colon).

There are many predefined format specifiers. Many format specifiers can be followed by an integer called the precision, whose meaning varies among specifiers. Here are two useful specifiers:

For example,

const double pi = 3.14159;
const int i = 1357;
  
Console.WriteLine($"{pi:f2} {i:n0}");

writes

3.14 1,357

Some more format specifiers are listed in our C# quick reference.

foreach

A foreach statement iterates over each element of a collection:

foreach (char c in "hello")
    Console.WriteLine(c);

The only kinds of collections we have learned about so far are strings (but we will soon see others).

This statement is similar to the 'for' statement in Python, which can also loop over various kinds of collections.

A foreach statement always declares an iteration variable whose scope is the body of the statement.

do / while

A do/while loop is similar to a while loop, but checks the loop condition at the bottom of the loop body. For example:

string? s;

do {
    s = ReadLine();
} while (s != "yes" && s != "no");

Note that the body of an ordinary while loop might not execute at all, but the body of a do/while loop will always execute at least once.

In most programs do/while loops are less common than ordinary while loops, but they are still occasionally useful.

conditional operator

The conditional operator (? :) is similar to the if statement, but it is an expression, not a statement. It takes a Boolean value plus two extra values. It returns the first of these values if the boolean is true, or the second if it is false.

For example, the following statement computes the greater of i and j and assigns it to k:

int i, j;
...
int k = i > j ? i : j;     // set k to the maximum of i and j

This operator is also sometimes called the ternary operator since it takes three operands. We also saw this operator in Python, which uses a completely different syntax for it:

# Python code: set k to the maximum of i and j
k = i if i > j else j;   

arrays

C# includes fixed-size arrays. You can allocate a single-dimensional array like this:

int[] a = new int[10];

All elements will be initialized to the element type's default value. The default value for int and other numeric types is 0. The default value for bool is false. The default value for string is null.

Alternatively, you can allocate an array and initialize its elements to specific values:

int[] a = { 3, 4, 5 };

Arrays are indexed from 0, and the ^ operator indexes from the end:

int x = a[1];  // now x is 4
int y = a[^1];  // now y is 5, the last value in the array

An array in C# is a reference type, meaning that more than one variable may refer to the same array. For example:

int[] a = { 10, 20, 30, 40 };
int[] b = a;
a[0] = 100;
WriteLine(b[0]);   // writes 100

The assignment "int[] b = a" does not copy the array. Instead, it sets b to point to the same array as a. (We also saw this phenomenon in Python.)

You can access ranges of arrays just like for strings:

int[] a = { 2, 4, 6, 8, 10 };
int[] c = a[1..4];  // { 4, 6, 8 }

int[] d = a[..3];   // { 2, 4, 6 }
int[] e = a[3..];   // { 8, 10 }

Note that the array range operator makes a copy of a range of elements (just like slice operators in Python). Of course, it follows that creating a range of N elements will take O(N) time.

The Length property returns the length of an array:

int y = a.Length;

An array is a collection, so you can loop over it with foreach:

foreach (int i in b)
    Console.WriteLine(i);

Because arrays have a fixed size, you cannot append a value to an existing array. If you want to append values, probably you should use a List, i.e. a dynamic array (we'll discuss these soon).

rectangular arrays

In C# you can create two kinds of multidimensional arrays: rectangular arrays and jagged arrays.

You can dynamically allocate a rectangular array using the new operator:

int[,] a = new int[3, 4];

Or you can allocate an array and initialize it with a set of values:

int[,] a = { {1, 4, 5}, {2, 3, 6} };

You can access an element of a rectangular array using an expression such as a[2, 5].

An rectangular array may even have 3 or more dimensions, e.g.

int[,,] a = new int[5, 10, 15];    // a 3-dimensional array

In a rectangular array a, a.Length is the total number of elements in the array. This is the product of the lengths of all dimensions. a.Rank is the number of dimensions, and the GetLength() method returns the length of a given dimension (numbered from 0):

int[,,] a = new int[5, 10, 15];     // a 3-dimensional array
Console.WriteLine(a.Length);        // writes 750 (= 5 * 10 * 15)
Console.WriteLine(a.Rank);          // writes 3
Console.WriteLine(a.GetLength(1));  // writes 10

Length and Rank are properties, not methods, so when we access them we don't write (). A property looks like a field (i.e. an attribute), but is not exactly the same as a field since code may run when a property is retrieves. We'll study properties in more detail later in this course, and will learn how to implement properties in our own classes.

jagged arrays

A jagged array is an array of arrays. Unlike in a rectangular multidimensional array, each subarray in a jagged array can have a different length.

You can allocate a jagged array like this:

int[][] a = new int[3][];
a[0] = new int[2];
a[1] = new int[4];
a[2] = new int[5];

If you like, you can initialize a jagged array as you allocate it:

int[][] a = {
    new int[] { 2, 3},
    new int[] { 3, 4, 5},
    new int[] { 1, 3, 5, 7, 9}
};

You can access an element of a jagged array using an expression such as a[2][5].

lists

A dynamic array is an extremely useful data type. The C# standard library includes a type List<T>, which implements a dynamic array. This is just like a list in Python.

The T in List<T> indicates that this is a generic type. We'll study generics in more detail later in this course, and will learn how to implement our own generic types. For now, we only need to understand that T represents any type. We may construct a List<int>, which holds integers, a List<string> which holds strings, or a list of any other type. Note that (unlike in Python) all elements of a list must have a common type.

The List<T> class is in the System.Collections.Generic namespace. As we saw last week, in a new C# project the implicit usings feature is enabled by default. With implicit usings, this namespace is automatically imported, so you can use List<T> without writing a "using" statement.

However, we also learned last week that implicit usings are not enabled on ReCodEx. So if you want to use List<T> in a program that you submit to ReCodEx, you'll need to import its namespace explicitly:

using System.Collections.Generic;

Let's create an empty list that will hold integers, and add a few integers to it:

List<int> a = new();
a.Add(3);
a.Add(5);
a.Add(7);

In C# we can use the new() operator to create a new instance of any type. The .Add() method is just like .append() in Python: it appends an element to a list, and will run in O(1) on average.

Once we have a list, we may get or set its elements by index:

a[0] = a[1] + a[2];
WriteLine(a[2]);

The Count property retrieves the number of elements in a List<T>:

WriteLine($"a has {a.Count} elements");

Our C# quick reference lists more useful methods on List objects. If you look at the documentation for System.Collections.Generic in the quick reference, you'll see that List<T> inherits from IList<T>, which inherits from ICollection<T>, which inherits from IEnumerable<T>. So you'll find methods that work on Lists not only in the section about List<T>, but also in the sections about those supertypes as well. For example, the Insert() and RemoveAt() methods are listed in the documentation for IList<T>, and will work on any list. We'll study C#'s collection class hierarchy in more detail in a later lecture.