Week 6: Notes

command-line arguments

Up until now, we've always written the Main() method of a C# program with this signature:

static void Main() {
    ...

Alternatively, Main() may take an array of command-line arguments. Then it will look like this:

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

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

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

Let's run 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.

exceptions

Like Python and most other modern languages, C# has exceptions. Code may throw an exception to indicate that an exceptional situation has occurred, typically some sort of error. In an addition, the C# language itself may throw an exception in some situations, such as if code attempts to access an array element that's out of bounds, or attempts to access a property of null. When an exception is thrown, it will pass up the call stack, aborting the execution of any methods in progress until some block of code catches the exception and continues execution. If the exception is not caught, the program will print an error message and terminate.

An exception in C# is an object, specifically any object belonging to the System.Exception class or any of its subclasses. A large number of exception classes are built into the standard library including IndexOutOfRangeException, NullReferenceException, FormatException, InvalidOperationException, and many others.

throwing an exception

The throw statement throws an exception, either of a built-in or user-defined exception class. For example:

class OpException : Exception {
    char op;

    public OpException(char op) {
        this.op = op;
    }
}

int compute(char op, int a, int b) =>
    op switch {
        '+' => a + b,
        '-' => a - b,
        '*' => a * b,
        _ => throw new OpException(op)
    };

catching exceptions

The try statement attempts to execute a block of code. It may have one or more catch clauses, each of which will execute if a certain type of exception is thrown. For example:

  static void Main() {
      StreamReader reader;
      try {
          reader = new StreamReader("numbers");
      } catch (FileNotFoundException e) {
          WriteLine("can't find input file: " + e.FileName);
          return;
      } catch (DirectoryNotFoundException) {
          WriteLine("invalid path");
          return;
      }
      

When an exception is caught, the catch block (called an exception handler) executes. As you can see in the code above, a catch block may optionally specify a variable to receive the exception object.

A catch block may rethrow the exception it caught, or even a different exception. If the catch block does not throw an exception, execution resumes below the try statement.

disposing resources with 'using'

Certain classes in the standard C# library implement the interface IDisposable, which has a single method:

void Dispose ();

This method frees any external resources associated with an object. You should call it when you are finished using an object.

Fort example, the StreamReader and StreamWriter classes (which are commonly used to read from and write to files) implement IDisposable. The Dispose() method in these classes performs the same task as the Close() method: it closes a file or other stream. It is especially important to call Close() or Dispose() when writing to a file - if you do not, some output may not be written!

C# includes a feature that makes it easy to dispose resources automatically. Specifically, when you declare any variable you may precede it with the using keyword. Then the object in that variable will automatically be freed using Dispose() 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 r = new StreamWriter(filename);

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

local methods

A local method is a method defined inside another method. In some situations these may be quite useful. For example, consider a method dfs() that performs a depth-first search on a graph in adjacency-list representation. Our method will contain a nested method visit() that performs the recursive search:

void dfs(int[][] g) {
    var visited = new bool[g.Length];

    void visit(int v) {
        WriteLine($"visiting {v}");
        visited[v] = true;
        foreach (int w in g[v])
            if (!visited[w])
                visit(w);
    }

    visit(0);
}

A local method may access parameters and local variables in its containing method. For example, the local method visit() above accesses the parameter g and the local variable visited. We could write visit() outside the dfs() method, but then visit() would need to take both g and visited as parameters, which would be less convenient.

generic methods

So far we have used generics only to write entire classes. C# also allows us to write generic methods that take one or more type parameters, even outside a generic class. Occasionally this is useful. For example, suppose that we want to write a method that swaps two values of any type T. We can write this as as a generic method:

void swap<T>(ref T a, ref T b) {
    T t = a;
    a = b;
    b = t;
}

nested types

In C# a class may be nested inside another class. You may want to use a nested class when you write a helper class that's useful only inside another class. For example:

class LinkedList {
    class Node {
        public int i;
        public Node next;
        public Node(int i, Node next) { this.i = i; this.next = next; }
    }
  
    Node head;
  
    public void prepend(int i) {
        head = new Node(i, head);
    }
}

The Node class is nested inside LinkedList. It's visible inside that class, but since the Node class is not explicitly marked as public, it is not accessible from outside LinkedList.

A class that's nested inside a generic class may use all the type variables of the containing class. This can be quite convenient. For example, let's make the previous example generic:

class LinkedList<T> {
    class Node {
        public T val;
        public Node next;
        public Node(T val, Node next) { this.val = val; this.next = next; }
    }
  
    Node head;
  
    public void prepend(T val) {
        head = new Node(val, head);
    }
}

Notice that we do not need to declare Node as a generic class Node<T>. It can use the type variable T found in the containing class LinkedList<T>.

If Node were not nested in this example, we'd have to declare it as a generic class Node<T> since it would be outside the scope of the type variable T. And then inside the LinkedList class we would have to write Node<T> whenever we referred to that class, which would be less convenient.

delegates

A delegate is a value that represents a function or method. It's similar to to a function object in Python, or a function pointer in languages such as C.

The delegate keyword declares a new delegate type. For example:

  delegate bool IntCondition(int i);

With this declaration, an IntCondition is a type of delegate that takes an integer argument and returns a boolean. We can now declare a variable of type IntCondition, and use it to refer to a method of corresponding type:

  static bool isOdd(int i) => i % 2 == 1;

  static void Main() {
      IntCondition c = isOdd;
      

We can invoke the delegate using function call syntax:

    WriteLine(c(4));    //  writes False

In the example above, the delegate c refers to a static method odd. A delegate may also refer to an instance method, in which case it actually references a particular object on which the method will be invoked. For example:

  class Interval {
      public int low, high;
      public Interval(int low, int high) { this.low = low; this.high = high; }
  
      public bool contains(int i) {
          return low <= i && i <= high;
      }
  }

  static void Main() {
      IntCondition c = new Interval(1, 5).contains;
      IntCondition d = new Interval(3, 7).contains;
      WriteLine(c(2));  // writes True
      WriteLine(d(2));  // writes False
  }

Here is a method that counts how many elements in an array of integers satisfy an arbitrary condition:

  static int count(int[] a, IntCondition cond) {
      int n = 0;
      foreach (int i in a)
          if (cond(i))
              ++n;
      return n;
  }

We can invoke this method as follows:

  static bool isEven(int i) => i % 2 == 0;
  
  int[] a = { 3, 4, 5, 6, 7 };
  WriteLine(count(a, isEven));  // writes 2

Delegates may be generic:

  delegate bool Condition<T>(T t);  // maps type T to bool

Here is the count() method from above, rewritten to work on an array of any type T. Notice that the method itself must also be generic (indicated by the "<T>" after the method name).

  static int count<T>(T[] a, Condition<T> cond) {
      int n = 0;
      foreach (T val in a)
          if (cond(val))
              ++n;
      return n;
  }

The standard library contains several useful generic delegate types. The built-in type Predicate<T> is exactly equivalent to the type Condition<T> that we just defined:

delegate bool Predicate<T>(T arg);

Additionally, the built-in type Func<T, U> represents an arbitrary function from type T to type U:

delegate U Func<T, U>(T arg);

lambda expressions

A lambda expression is an anonymous function that can appear inside another expression. (It's similar to an lambda expression in Python, which we saw last semester).

For example, here's a generic method map() that applies a Func<T, U> to every element of an array of type T[], returning a new array of type U[]:

   U[] map<T, U>(T[] a, Func<T, U> f) {
       U[] b = new U[a.Length];
       for (int i = 0; i < a.Length ; ++i)
           b[i] = f(a[i]);
       return b;
   }

We can define a named method and pass it to map():

  int plus2(int i) => i + 2;

  int[] add2(int[] a) {
      return map(a, plus2);
  }

Alternatively, we can invoke map() using a lambda expression:

  int[] add2(int[] a) {
return map(a, i => i + 2);
}

Here, i => i + 2 is a lambda expression. It's an anonymous function that takes an integer parameter i and returns the value i + 2.

Like a local method, a lambda expression may refer to parameters or local variables in its containing method. For example, suppose we want to write a method that adds a given value k to each element in an array. We could write a local method and pass it to map():

  int[] add_k(int[] a, int k) {
      int f(int i) {
          return i + k;
      }

      return map(a, f);
  }

Or we can use a lambda expression that adds k directly:

  int[] add_k(int[] a, int k) {
return map(a, i => i + k);
}

events

An event is a class member that lets callers register event handlers that will receive notifications. Each event handler is a delegate. When an event is raised (i.e. fires), a notification is sent to each registered event handler. Each notification includes arguments matching the event's delegate type.

Events are useful for implementing the observer pattern, in which one or more observers may want to hear about changes to an object. A common example of this pattern is a model-view architecture, in which the view observes the model and displays the model's data. In such an architecture we want the model to be unaware of the view. Using an event, a view can register to find out when the model has changed, without giving the model specific knowledge of the view class.

Here's an array class including an event that is raised whenever any array element changes:

delegate void Notify(int index, int old, int now);

class WatchableArray {
    int[] a;
  
    public WatchableArray(int n) {
        a = new int[n];
    }
  
    public event Notify changed;
  
    public int this[int i] {
        get => a[i];

        set {
            int prev = a[i];
            a[i] = value;
            if (changed != null)
                changed(i, prev, a[i]);  // fire the event to notify observers
        }
    }
}

Notice that the event declaration includes a delegate type, and that we can raise an event using method call syntax.

Use the += operator to register a handler with an event. For example, we can create an instance of the WatchableArray class and register an event handler:

  void onChange(int index, int old, int now) {
      WriteLine($"a[{index}] changed from {old} to {now}");
  }
  
  public void Main() {
      WatchableArray a = new WatchableArray(5);
      a.changed += onChange;
      

  }

If some method later calls

  a[3] = 4;

then the above event handler will run, and will print a message such as

  a[3] changed from 0 to 4

Be warned: if you attempt to raise an event that has no registered handlers, you will get a NullPointerException. In my opinion this is a weakness in the C# event system: if would be nicer if raising such an event did nothing. However, this is how it works. So in the example above, we need to write

if (changed != null)
    changed(i, prev, a[i]);  // fire the event to notify observers

to guard against this condition.