Week 4: Notes

initializing arrays and lists

As we saw in an earlier lecture, when you create an array you can specify a set of initial values:

int[] a = {10, 20, 30, 40};

You can also initialize rectangular and jagged arrays with values:

int[,] b = {{1, 4, 5}, {2, 3, 6}};    // rectangular
int[][] c = {                           // jagged
    new int[] {2, 3},
    new int[] {3, 4, 5}
};

It's also possible to initalize a List with values, or even a List of List objects (which is like a jagged array):

List<int> d = new() {10, 20, 30, 40};

List<List<int>> e = new() {
    new() {2, 3},
    new() {3, 4, 5},
};

Notice that the syntax for arrays and lists is not quite the same.

C# 12 includes a new feature called collection expressions that allows you to initialze arrays and lists using a nicer, more uniform syntax. Using collection expressions, we may rewrite the above examples like this:

int[] a = [10, 20, 30, 40];

int[][] c = [[2, 3], [3, 4, 5]];

List<int> d = [ 10, 20, 30, 40 ];

List<List<int>> e = [[2, 3], [3, 4, 5]];

This is convenient. However ReCodEx only has C# 11, so you cannot use this syntax in any ReCodEx program at this time.

Also note that you cannot use a collection expression to initialize a rectangular array.

value and reference types

Every type in C# is either a value type or a reference type.

When a variable's type is a value type, the variable holds a value. An assignment between variables of value type copies the value from one variable to another.

When a variable's type is a reference type, the variable holds a reference to an object. An assignment between variables of reference type makes them refer to the same object.

For example:

int[] a = { 4, 5, 6 };
int[] b = a;       // now b and a refer to the same array
a[1] = 7;
WriteLine(b[1]);   // writes 7

As we have noted before, every type in C# has a certain default value. For example, the default value of int is 0. When you create an array without providing initial values, every element will be initialized to the default value of the element type. So in the declaration

int[] a = new int[100];

every array element will initially be zero.

In C# the default value of every reference type is null. And so if you create an array of any reference type, all its elements will initially be null. That is true even if the type is not nullable! For example:

string[] a = new string[10];   // array of non-nullable strings
string?[] b = new string[10];  // array of nullable strings

The elements in both of these arrays will initially be null. However you cannot assign null to any element of a:

a[3] = null;   // ERROR: converting null to non-nullable type

Arguably this is an inconsistency in the language. It exists for historical reasons. In older versions of C# there were no nullable types, and a value of any reference type could be null. When nullable types were added to C# a few years ago, the default value of reference types remained null, presumably for backward compatibility.

optional parameters

You can make a method 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 method, 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);

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

Notice that if the second overloaded constructor above did not exist, then the call new Vec(5) would call the first constructor, since 5 can be implicitly converted to a double. When both constructors exist, the call new Vec(5) is potentially ambiguous, but C# resolves the ambiguity by choosing the constructor that does not require an implicit conversion.

static members

A field or method may be static. Static members are shared by all instances of a class. For example:

class Person {
    string name;
    int id;

    static int next_id;

    public Person(string name) {
        this.name = name;
        this.id = next_id++;
    }

    public static int get_next_id() {
        return next_id;
    }
}

In this class the static field next_id holds the next id that will be assigned to a newly created Person. A static method such as get_next_id() is invoked on a class, not on an instance:

WriteLine(Person.get_next_id());

Sometimes we may reasonably implement a method either as an instance method or a static method. For example:

class Point {
    double x, y;

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

    public double dist(Point p) =>
        Sqrt((p.x - this.x) * (p.x - this.x) +
             (p.y - this.y) * (p.y - this.y));

    public static double dist(Point p, Point q) =>
        Sqrt((p.x - q.x) * (p.x - q.x) +
             (p.y - q.y) * (p.y - q.y));

}

Thie class has two overloaded versions of a dist() method that computes the distance between two points. The first is an instance method, and can be invoked like this:

Point p = new Point(3, 4);
Point q = new Point(5, 6);

WriteLine(p.dist(q));

The second is a static method, and can be invoked like this:

WriteLine(Point.dist(p, q));

Notice that either an instance method or static method may access private fields of any instance of the class that it belongs to.

Which version of dist() is better? This is a question of style. Some people may prefer the static version because it is more symmetric.

static classes

A class may be marked static to indicate that all of its members are static. For example, we might use a static class to hold numeric utility methods:

static class Util {
    static int gcd(int a, int b) =>
        a == 0 ? b : gcd(b, a % b);

    static bool coprime(int a, int b) =>
        gcd(a, b) == 1;
}

No instance of a static class may ever exist. If you try to create one using the new() operator, you'll receive a compile-time error.

It may be helpful to import a static class with 'using static' so that you can access its members without specifying a prefix:

using static Util;

properties

A property is syntactically like a field, but acts like a method. Specifically, it contains a getter and/or a setter, which are blocks of code that run when the caller retrieves or updates the property's value.

As an example, here's a class that implements a stack of integers:

class IntStack {
    int[] a;
    int n;

    public IntStack(int limit) {
        a = new int[limit];
    }

    public bool isEmpty {
        get {
            return n == 0;
        }
    }

    public int limit {
        get {
            return a.Length;
        }
        set {
            Debug.Assert(value >= n, "new limit is too small");
            int[] b = new int[value];
            for (int i = 0; i < n; ++i)
                b[i] = a[i];
            a = b;
        }
    }

    public void push(int i) {
        a[n++] = i;
    }

    public int pop() {
        return a[--n];
    }
}

The class has a boolean property isEmpty that's true if the stack is empty. This property has a getter but no setter, so it can only be read. For example:

IntStack s = new IntStack(5);
WriteLine(s.isEmpty);  // writes true

When the caller retrieves the property, the get block runs and returns a boolean value.

The class also has a property limit with both a getter and setter. We might write

IntStack s = new IntStack(5);
WriteLine(s.limit);  // writes 5
s.limit = 10;        // increase the maximum size
WriteLine(s.limit);  // writes 10

When the caller retrieves the property, the getter runs. When the caller sets the property, as in the line 's.limit = 10' above, the setter runs. Inside any setter, the keyword value refers to the value that is being set. In this specific example, it is 10.

You can use expression syntax to define getters or setters. The isEmpty property above could be written as

public bool isEmpty {
    get => n == 0;
}

If a property has only a getter but no setter, it can be written using an even simpler syntax:

public bool isEmpty => n == 0;

indexers

An indexer allows you to define custom getter and/or setter blocks that run when an instance of your class is accessed using the array-like syntax a[i].

For example, here is a vector class that includes properties dims and length, plus an indexer:

class Vec {
    double[] a;

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

    public int dims => a.Length;    // number of dimensions

    public double length {
        get {
            double t = 0.0;
            foreach (double d in a)
                t += d * d;
            return t;
        }
    }

    // indexer
    public double this[int i] {
        get {
            return a[i];
        }

        set {
            a[i] = value;
        }
    }
}

A caller can invoke this indexer as if v itself were an array:

Vec v = new(2.0, 4.0, 8.0);
v[1] = 5.5;           // invokes the setter
v[2] = v[0] + 1.0;    // invokes both the getter and setter

An indexer getter may be written using expression-valued syntax, so we could simplify the get block above as follows:

get => a[i];

The indexer defined above has return type double and uses an index parameter of type int. In general, an indexer may have any return type and any index parameter type. For example, a class representing a dictionary from strings to strings might have an indexer like this:

class StringDict {
    public string this[string s] { ... }
}

An indexer may even take multiple arguments. For example, a class representing a matrix might have an indexer like this:

class Matrix {
    public double this[int r, int c] { ... }
}

overloaded operators

You may defined overloaded operators for a class, which redefine the meaning of built-in operators such as + and * when invoked on instances of the class. For example, let's add an overloaded + operator to the Vec class we saw above:

public static Vec operator + (Vec v, Vec w) {
    Debug.Assert(v.dims == w.dims, "incompatible vectors");

    double[] b = new double[v.dims];
    for (int i = 0 ; i < v.dims ; ++i)
        b[i] = v[i] + w[i];

    return new Vec(b);
}

Now we can write this:

    Vec v = new(2.0, 5.0, 10.0);
    Vec w = new(1.0, 3,0, 9.9);
    Vec x = v + w;

An overloaded operator must be public and static.

Notice that the overloaded operator above is invoking the class's own indexer (in the line 'b[i] = v[i] + w[i]') and also the dims property.

You may overload most of the built-in operators available in C#, including

constants

Another kind of class member is constants. For example:

class Stack {
    const int LIMIT = 100;

    int[] a = new a[LIMIT];
    ...
}

Like other members, a constant may be either public or private.

enums

An enum type holds one of a fixed set of constant values:

enum Suit {
    Club, Diamond, Heart, Spade
}

To refer to one of these values, prefix it with the type name:

Suit s = Suit.Diamond;

If you'd like to be able to access an enum's values without the type name prefix, include a using static declaration at the top of your source file:

using static Suit;

Then you will be able to write, for example:

Suit s = Diamond;

Internally, an enumerated value is stored as an integer. Each constant in an enum is assigned an integer value, starting from 0. For example, in the enumeration above, Diamond is assigned the value 1.

Explicit conversions exist between each enum type and int in both directions:

Suit s = Suit.Diamond;
int i = (int) s;    // convert a Suit to an int
Suit t = (Suit) i;  // convert an int to a Suit