Week 2: Notes

unsigned integral types

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

numeric 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:

byte b;
...
int i = b;   // implicit conversion

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

int i = 300;
byte c = (byte) i;

If the destination type cannot hold the source value, it will wrap around or be truncated. In the example above c will be 44, which is 300 mod 256.

short and long names for types

C# types such as int and long are abbreviations for longer names such as System.Int32 and System.Int64. 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. 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

floating-point types

Here are two 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 may want to 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

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

double d = double.PositiveInfinity;

for

A for loop in C# looks like this:

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

To be more precise, a for statement contains three clauses, separated by semicolons, plus a loop body.

The code

for (initializer; condition; iterator)
    body;

is precisely equivalent to

initializer;
while (condition) {
    body;
    iterator;
}

Note that the intializer, condition, and/or iterator may be empty. An empty condition is equivalent to true. They may even all be absent:

for (;;)
    Console.WriteLine("hi");

This is the same as

while (true)
    Console.WriteLine("hi");

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.

break and continue

C# includes the break and continue statements. You can use them in any loop, and they work just like in Python:

for (int i = 0 ; i < 5 ; ++i) {
    if (i == 1)
        continue;  // skip this iteration
    if (i == 3)
        break;     // exit the loop
    Console.WriteLine(i);
}

The code above will print

0
2

characters, revisited

In the last lecture we briefly introduced the char type, which represents a 16-bit Unicode character. A character constant is enclosed in single quotes, e.g. 'x' or 'ř'.

Let's consider characters in a bit more detail. There are 216 = 65,536 possible 16-bit values. Unfortunately, this is not enough to represent every possible Unicode character, since Unicode code points may be as large as 10FFFF16 = 1,114,11110. So not every Unicode character will fit in a C# char. This is somewhat unfortunate, and is partially due to the fact that Unicode had a smaller range of characters when C# was designed over 20 years ago.

Fortunately, most common characters in living world languages have Unicode values that will fit in 16 bits. However, emoji and other graphical characters may not. For example, the tomato character (🍅) has a Unicode value of 1F34516, which is larger than the largest possible 16-bit value FFFF16. So it won't fit in a C# char:

char c = '🍅';  // error: too many characters in literal

However it can be stored in a C# string:

string t = "🍅";

This is a string of length 2, since it must be stored as two chars:

Console.WriteLine(t.Length);  // writes 2

By contrast, Python has 32-bit characters, so this string would have length 1 in Python (which arguably makes more sense).

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

indexing strings

In the last lecture we briefly introduced the string type. A string is an immutable sequence of char values. We can access characters in a string using the [] operator:

string w = "watermelon";
char c = w[0];  // now c = 'w'
char d = w[2];  // now c = 't'

An index preceded by ^ counts from the end of the string (like a negative index in Python):

char e = w[^1];  // now e = 'n'

It's possible to extract a range of characters from a string:

string w = "watermelon";
string x = w[2..5];  // now x = "ter"
string y = w[5..];   // now y = "melon"

Notice that the range above includes all characters from the character w[2] up to but not including the character w[5]. (It's just like "w[2:5]" in Python.)

The + operator concatenates strings:

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

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.

interpolated strings

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.

arrays

You can allocate a single-dimensional array like this:

int[] a = new int[10];

In this allocation 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

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 }

The Length property returns the length of an array:

int y = a.Length;  // now y is 5

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

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

Above, I wrote that the default value for the string type is null. That's a bit strange, because normally a string in C# cannot hold null:

string s = null;  // ERROR: converting null to non-nullable type

Despite this, if I allocate an array of string values they will all initially be null:

string[] a = new string[5];

string b = a[0];  // now b is null!

This is an inconsistency in the language. It exists for historical reasons. In older versions of C# a string could be null, but then the language changed a few years ago to become stricter, so now the type string? is required for values that might either be a string or null. Unfortunately this change introduced a certain inconsistency, since elements of a string array are still initialized to null, just as they were before the change.

rectangular arrays

There are 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

The Length property returns the total number of elements in a rectangular array. This is the product of the lengths of all dimensions. The Rank property returns 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

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].

functions

A function can take one or more parameters, each with a specific type. It also has a return type. For example:

double abc(double d, int a) {
    return d / (2 * a);
}

// Compute the sum of all values in an array of ints.
int sum(int[] a) {
    int s = 0;
    foreach (int i in a)
        s += i;
    return s;
}

The return statement returns a value from a function, and may be called anywhere from within the function body (just like in Python). The first function above returns a double, and the second returns an int.

If a function's return type is void, then it does not return a value:

void countdown(int i) {
    while (i > 0) {
        Console.WriteLine(i);
        --i;
    }
}

Note that ReCodEx will not accept code with top-level functions, i.e. functions that are outside of any class. If you write code with top-level functions and want to submit it to ReCodEx, you will need to move the functions inside a class by making them be static methods of the class. (As we'll see in a future lecture, a static method is something like a function that belongs to a class.) So you could write e.g.

using System;

class Top {
    static void countdown(int i) {
        while (i > 0) {
            Console.WriteLine(i);
            --i;
        }
    }

    static void Main() {
        countdown(10);
    }
}