Week 4: Notes

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.

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 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 may discuss these capabilities later in this course.

command-line arguments

In many programs we may wish to access command-line arguments.

In a new-style program with no main class or Main() method, the command-line arguments are accessible via a predefined variable args of type string[]. For example, here is a one-line program:

Console.WriteLine($"Hello, {args[0]}.");

Let's run it:

$ dotnet run Fred
Hello, Fred.

If a C# program has a main class, then the Main() method may take no arguments (as we have seen before), or may take an array of command-line arguments:

static void Main(string[] args) {
    ...

For example, here's a program that echoes each argument to the output:

class Top {
    static void Main(string[] args) { 
        foreach (string s in args)
            Console.WriteLine(s);
    }
}

Let's try it:

$ dotnet run one two three
one
two
three
$

Notice that (unlike in Python) the name of the program is not present in the args[] array.

file input/output

We may use the StreamReader and StreamWriter classes in the System.IO namespace to read from and write to files. You can create a StreamReader or StreamWriter using the new operator, passing a filename. For example:

StreamReader sr = new("myfile.txt");

If you want to read all lines from a file, you don't actually need to use a StreamReader. As we saw in an earlier lecture, you can just use File.ReadLines():

int sum = 0;

foreach (string line in File.ReadLines("nums.txt"))
    sum += int.Parse(line);

closing files with 'using'

When you are finished with a StreamReader or StreamWriter, you should close it. You can call the Close() method for this purpose. It is especially important to close a StreamWriter - if you do not, some output may not be written!

C# includes a feature that makes it easy to close files (or other resources) automatically. When you declare any variable, you may precede it with the using keyword. Then the object in that variable will automatically be freed as soon as code exits the block of code containing the variable declaration (and even if it exits via an exception that was thrown).

For example:

using StreamWriter sr = new(filename);

This feature may remind you of Python's with statement, which has a similar purpose.

example: cp

Using the ideas above, we can write the classic Unix utility cp, which copies one file to another:

if (args.Length != 2) {
    Console.WriteLine("usage: dotnet run <from> <to>");
    return;
}

string from = args[0], to = args[1];

using StreamWriter sw = new(to);
foreach (string line in File.ReadLines(from))
    sw.WriteLine(line);

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

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.

constants

A class may contain constant members. For example:

class Stack {
    const int LIMIT = 100;

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

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

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