Week 3: Notes

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; 

character conversions

In Python, we used the chr() and ord() functions to convert numeric codes into characters and vice versa. In C# there are no such functions. Instead, we can accomplish the same task through a simple assignment.

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

tuples

A tuple holds two or more values. Unlike arrays, tuples have fixed length and can hold values of varying types. We write tuples using a syntax that's just like Python: for example, (3, "hello", false) is a tuple containing an int, a string, and a bool.

We can assign tuples to variables in several different ways that may look superficially similar. First, we may unpack each element of a tuple into a separate variable:

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

The code above produces separate variables x and y. We can also perform a tuple assignment into existing variables:

(x, y) = (30, 40);  // assign 30 to x, and assign 40 to y
(x, y) = (y, x);    // swap x and y

Alternatively, we may assign an entire tuple to a single variable:

(int, int) p = (10, 20);
(int, string) q = (3, "hello");

Above, (int, int) and (int, string) are tuple types. A tuple type is written as a list of element types in parentheses.

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

(int, int) p = (10, 20);
WriteLine(p.Item1);    // writes 10
WriteLine(p.Item2);    // writes 20

The names Item1 and Item2 are pretty ugly. Alternatively, we can specify our own names inside a tuple type:

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

This is called a named tuple. We can now access the tuple elements by name:

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

This looks quite a bit nicer.

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 can use any of the assignment forms above in a foreach loop. For example:

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

foreach ((int x, int y) in points)	// assign to variables x and y
    WriteLine($"{x}, {y}");

foreach ((int, int) p in points)
    WriteLine($"{p.Item1}, {p.Item2}");  // a bit ugly
foreach ((int x, int y) p in points) // assign to a named tuple
    WriteLine($"{p.x}, {p.y}");

The choice between these is a matter of style.

type aliases

It can be awkward to write named tuple types repeatedly in code:

(int x, int y) p = (10, 20);
int z = p.x + p.y;
(int x, int y) q = p;

As an alternative to this, you can create a type alias via a using statement at the top of your source file:

using Point = (int x, int y);

With this alias, we can write the code above more concisely:

Point p = (10, 20);
int z = p.x + p.y;
Point q = p;

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);
    }
}

expression-bodied functions

We have seen basic syntax for defining functions in C#:

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

The function above 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.

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) {
    int t = a;
    a = b;
    b = t;
}

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

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);

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(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).

methods

In the Point class above, is_zero() is a method. We write methods using the same syntax that we previously saw for functions.

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

example: stack class

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.