Week 3: Notes

using static

I recommend that you write the following at the top of every C# source file:

using static System.Console;

That will allow you to conveniently invoke methods as WriteLine() and ReadLine() without the "Console." prefix:

WriteLine("hello");
string? line = ReadLine();

More generally, a using static declaration imports all static methods from a given class, so that you can use these methods without prefixing with them with the class name. A static method such as WriteLine() is invoked on a class itself, not on an instance of the class. We'll talk more about static methods in the next lecture.

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.

switch

A switch statement is a more compact (and possibly more efficient) alternative to a series of if statements. It looks like this:

switch (i) {
    case 11:
        WriteLine("jack");
        break;
    case 12:
        WriteLine("queen");
        break;
    case 13:
        WriteLine("king");
        break;
    case 1:
    case 14:
        WriteLine("ace");
        break;
    default:
        WriteLine(i);
        break;
}

In this example, the group of statements that matches the value of (i) will run. For example, if i is 12, the code will print "queen". If it's either 1 or 14, it will print "ace".

Each section in a switch statement must end with a break or return statement. The default section is optional, and runs if no other case is matched.

C# also includes switch expressions. Here's an example:

string name = i switch {
    11 => "jack",
    12 => "queen",
    13 => "king",
    1 or 14 => "ace",
    _ => i.ToString()
};

Notice that a switch expression is quite a bit more compact than a switch statement. In it, an underscore (_) represents a default case.

Actually switch statements and expressions can do a lot more than this – for example, they can choose a case based on the type of a value, and can match patterns. We'll discuss these capabilities later in this course.

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:

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

tuples

A tuple holds two or more values. Unlike arrays, tuples have fixed length and can hold values of varying types.

A tuple type is written as a list of element types in parentheses. For example:

(int, string) p = (3, "hello");

We can access a tuple's elements using the names Item1, Item2 and so on:

WriteLine(p.Item1);    // writes 3

WriteLine(p.Item2);    // writes "hello"

You can use multiple assignment to unpack a tuple into a set of variables:

(int x, string s) = p;   // now x is 3, s = "hello"

As another example, you can unpack a tuple into variables using pattern matching in a foreach loop:

(int, int)[] points = { (2, 3), (5, 7), (8, 9) };

foreach ((int x, int y) in points)
    WriteLine($"{x}, {y}");

named tuples

The generic element names Item1 and Item2 are not very informative. Sometimes it is clearer to use a named tuple, which specifies names for the elements. For example:

    (int x, int y) p = (10, 20);

Now we can access the tuple elements by name:

    WriteLine($"x = {p.x}, y = {p.y}");

Be sure to note the difference between the syntax for multiple assignment and the syntax for creating a named tuple – these may look similar at first. The declaration above creates a single variable p with fields p.x and p.y. By contrast, the line

    (int x, int y) = (10, 20);

is a multiple assigment that creates local variables x and y.

You may also create a named tuple using pattern matching in a foreach loop:

(int, int)[] points = { (2, 3), (5, 7), (8, 9) };

foreach ((int x, int y) p in points)
    WriteLine($"{p.x}, {p.y}");

This is equivalent to the foreach loop in the previous section. The choice between these is a matter of style.

expression-bodied functions

In the last lecture we saw basic syntax for defining functions in C#:

int mul(int x, int y) {
    return x * y;
}

This function just returns an expression. C# lets us define functions such as this using a compact syntax:

int mul(int x, int y) => x * y;

This is called an expression-bodied function. I recommend using this syntax when possible.

optional parameters

You can make a function parameter optional by giving it a default value:

void fill(int[] a, int start = 0, int val = -1) {
    for (int i = start ; i < a.Length ; ++i)
        a[i] = val;
}

The caller can omit any optional parameters:

int[] a = new int[10];
fill(a);  // same as fill(a, 0, -1)
fill(a, 5);  // same as fill(a, 5, -1)
fill(a, 7, 3);

Optional parameters must appear after any non-optional parameters in a parameter list.

named arguments

When you invoke any function, you may precede any argument with a parameter name, followed by a colon.

For example, the Substring method in the string class is defined as

public string Substring (int startIndex, int length);

We may invoke it in any of these ways:

string s = "bowling ball";

string t = s.Substring(1, 3);  // "owl"
string u = s.Substring(startIndex: 1, length: 3);  // identical
string v = s.Substring(length: 3, startIndex: 1);  // identical

Notice that named arguments may appear in any order, and must appear after any non-named arguments in an argument list.

If some parameters are optional, you may specify any subset of them you like using named arguments. For example, we may invoke the fill function from the previous section as

fill(a, val: 4)  // same as fill(a, 0, 4)

variable numbers of arguments

If the last parameter to a function is marked with the keyword params and has an array type T[], then in its place the function can receive any number of arguments of type T. The caller may pass individual values separated by commas, or may pass an array of type T[].

For example, this function computes the sum of any number of integers:

int sum(params int[] a) {
    int s = 0;
    foreach (int i in a)
        s += i;
    return s;
}

It can be invoked as

int s = sum(4, 5, 6);

Alternatively, the caller can pass an array:

int[] a = { 4, 5, 6 };
int s = sum(a);

out parameters

A parameter marked with out returns a value to the caller. For example, this function takes two integers and returns their quotient as an ordinary return value. It also returns the remainder via an out parameter:

int divide(int a, int b, out int remainder) {
    remainder = a % b;
    return a / b;
}

You must include the keyword out when passing a variable to an out parameter:

int q, r;
q = divide(3, 4, out r);

You can declare a new variable as you invoke a method with an out parameter. The following is equivalent to the two preceding lines:

int q = divide(3, 4, out int r);

ref parameters

A parameter marked with ref is passed by reference: the method can modify the variable that is passed. For example, here's a function that swaps two integer variables:

void swap(ref int a, ref int b) {
    (a, b) = (b, a);
}

You must include the keyword ref when passing an argument by reference:

int i = 3, j = 4;
swap(ref i, ref j);

WriteLine(i);   // writes 4

classes

A class is a user-defined data type that can contain fields, constructors, methods, and other kinds of members.

As a first example, here's a simple class implementing a point in three dimensions:

class Point {
    public double x, y, z;   // fields

    // constructor
    public Point(double x, double y, double z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public bool is_zero() =>
        x == 0.0 && y == 0.0 && z == 0.0;
}

We may create a Point like this:

Point p = new Point(3.0, 4.0, 6.0);

WriteLine(p.is_zero());   // will write "false"

fields

Each instance of a class contains a set of fields.

A field may be declared with an initial value:

class Foo {
    int x = 3, y = 4;
    
}

If you don't specify an initial value, a field will intially be set to its type's default value (e.g 0 for an int).

access levels

Every member in a class can be either public or private. public members are accessible everywhere, and private members are accessible only within the class.

The default access level is private.

constructors

A constructor makes a new instance of a class. It always has the same name as its containing class.

A constructor will often intialize fields, as in the Point class above.

If a class definition includes no constructors, then C# provides a default constructor that is public and takes no arguments.

this

this is a special value that refers to the object on which a constructor or method was invoked. (It's like the self variable in Python.) In code, you may access a member x of the containing class by writing either "this.x" or simply "x". For example, in the method is_zero() in the Point class above we could have written

this.x == 0.0 && this.y == 0.0 && this.z == 0.0;

but instead we just referred to the fields x, y, and z directly. (This is a significant difference from Python, in which we must always write self.x to access the attribute x.)

On the other hand, in the Point() constructor above we had to write "this.x" to access the field x, since the constructor also has an parameter named x. (If we wrote "x = x" that would assign the parameter to itself, which would do nothing.)

stack class

As another example, here's a class implementing a fixed-size stack:

class Stack {

    // fields

    int[] a;
    int count;
    
    // constructor

    public Stack(int max_size) {    
        a = new int[max_size];
        count = 0;
    }
    
    // methods

    public bool is_empty() =>
        count == 0;
    
    public void push(int i) {
        a[count] = i;
        count += 1;
    }
    
    public int pop() {
        count -= 1;
        return a[count];
    }
}

Notice that in this class the fields are private. Usually it's good programming practice to make fields private, except in classes whose only purpose is to hold data (such as the Point class above). Then the caller can only modify the class's state by calling its public interface.

overloaded methods and constructors

In a class you may declare multiple methods or constructors that have the same name but have different numbers and/or types of parameters. This is called overloading.

For example, here's a class that implements a vector with any number of dimensions:

class Vec {
    double[] a;

    public Vec(params double[] a) {
        this.a = a;
    }

    public Vec(int dims) {
        this.a = new double[dims];
    }

    // Compute the sum of this vector and the vector w.
    public Vec add(Vec w) {
        double[] b = new double[a.Length];
        for (int i = 0 ; i < a.Length ; ++i)
            b[i] = this.a[i] + w.a[i];
        return new Vec(b);
    }
}

Notice that there are two overloaded constructors: one takes any number of parameters representing coordinates, and the other creates a zero vector of any number of dimensions. Let's create two vectors using these constructors:

Vec v = new Vec(2.0, 5.0, 10.0);
Vec w = new Vec(5);   // 5-dimensional zero vector