Lecture 6

Here are notes about some topics we covered in lecture 6. For more details, see the Essential C# textbook or the C# reference pages.

lifted operators

Many operators have lifted versions which operate on nullable types.

readonly fields

A field marked as readonly can be modified only in a constructor or field initializer. This attribute can be used to make a class immutable:

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

single and multiple inheritance

In C# (like in Java), a class can implement multiple interfaces, and an interface can inherit from multiple interface. However, a class can inherit from only one other class. In other words, C# supports single class inheritance but multiple interface inheritance.

There are languages such as C++ that allow a class to inherit behavior from multiple superclasses. This adds significant complexity to the language, and C#'s single class inheritance is a design compromise that is adequate in most situations.

calling base class constructors

A child class constructor can invoke a base class constructor using the base keyword. In the example below, the TwoLimitStack constructor calls the LimitStack constructor in this way.

class LinkedStack : Stack {
  Node head;
  
  public virtual void push(int i) { ... }
  public virtual int pop() { ... }
  public bool isEmpty { ... }
}

class LimitStack : LinkedStack {
  protected int count;
  protected int limit;
  
  public LimitStack(int limit) { this.limit = limit; }

  public override void push(int i) {
    if (count == limit) {
      WriteLine("stack is full");
      return;
    }
    ++count;
    base.push(i);
  }
  
  public override int pop() {
    --count;
    return base.pop();
  }
}

class TwoLimitStack : LimitStack {
  int threshold;
  
  public TwoLimitStack(int threshold, int limit) : base(limit) {
    this.threshold = threshold;
  }
  
  public override void push(int i) {
    if (count == limit - threshold)
      WriteLine("warning: stack is getting full");
    base.push(i);
  }
}

The 'protected' access modifier

We have already seen the public and private access modifiers, which can be attached to class members such as methods, properties or indexers. public members are accessible everywhere; private members are accessible within a class. The default access level in C# is private.

A third access modifier protected indicates that a member is visible only within the defining class and any of its subclasses. The LimitStack class above includes two protected fields.

abstract classes and methods

A class marked abstract cannot be instantiated using the new operator; only subclasses of it can be instantiated. Abstact classes may have abstract methods, which have a method signature but no body. Any concrete (non-abstract) subclasses of an abstract class must provide an implementation for every abstract method. Any abstract method is implicitly virtual, and may not be private.

Here is an example of an abstract class CountingStack that maintains a count of objects in a stack. Any concrete subclass such as LinkedCountingStack must provide implementations of the push2 and pop2 methods.

abstract class CountingStack {
  int _count;
  
  protected abstract void push2(int i);
  protected abstract int pop2();
  
  public void push(int i) {
    ++_count;
    push2(i);
  }
  
  public int pop() {
    --_count;
    return pop2();
  }
  
  public bool isEmpty { get => (_count == 0); }
  
  public int count { get => _count; }
}

class LinkedCountingStack : CountingStack {
  Node head;
  
  public override void push2(int i) {
    head = new Node(i, head);
  }
  
  public override int pop2() {
    int i = head.i;
    head = head.next;
    return i;
  }
}

sealed classes and methods

If a class is marked as sealed, no other class may derive from it. If an overriding method is marked as sealed, no subclass may override it further.

Sealed classes and methods are mostly useful if you are writing a library of classes that will be used by someone else. In this case, you may want to prevent the other programmer from extending your classes/methods in a way that might break if you change your code. You will probably use the sealed modifier rarely in your own programs.

The 'object' class

All types in C# derive from a top-level class called object. The full name for this class is System.Object. The object class includes the virtual method ToString and several other methods such as Equals, which will learn more about in a following lecture.

inheritance versus containment

In object-oriented design, sometimes you will have an existing class A and will be designing a new class B that is similar to A. You may sometimes face the choice of whether B should inherit from A or should contain an instance of A as a member.

For example, consider the dynamic array class we saw a couple of lectures ago. Here, we've extended its length property so that it is settable. By setting the length, you can either grow or shrink the array.

class DynArray {
  int[] a = new int[1];
  int count;
  
  public int length {
    get => count;
    set {
      if (value < count)
        count = value;
      else
        while (count < value)
          add(0);
    }
  }
  
  public void add(int i) {
    if (count == a.Length) {
      int[] b = new int[count * 2];
      for (int j = 0 ; j < count ; ++j)
        b[j] = a[j];
      a = b;
    }
    
    a[count++] = i;
  }
  
  public int this[int index] {
    get => a[index];
    set => a[index] = value;
  }
}

Now suppose that we want to implement a stack class ArrayStack that uses a dynamic array as its backing store. Should ArrayStack inherit from DynArray, or should it contain a DynArray as a member?

When considering in general the question of whether B should inherit from A, it is useful to ask whether a B is actually an A in the underlying domain. A pigeon is actually a bird, for example.

Inexperienced programmers sometimes use inheritance when containment is more appropriate. When you inherit from a class, you expose that class's public interface as part of your own class. If your own class does not represent an object that should actually belong to the superclass, this can lead to confusing code.

In this particular example, I would implement ArrayStack using a DynArray as a member:

class ArrayStack {
  DynArray a = new DynArray();
  
  public void push(int i) { a.add(i); }
  
  public int pop() {
    int i = a[a.length - 1];
    a.length -= 1;
    return i;
  }
  
  public bool isEmpty { get => a.length == 0; }
}

model/view separation

When writing many object-oriented programs, it is good design practice to use separate classes for the model and the view. A model class implements the underlying data and/or logic in some domain. A view class presents the model to the user in some way, e.g. via a text or graphical interface. The model class knows nothing about the view, and could be reused for various kinds of views. Similarly, the view class should know nothing about the logic of the underlying domain.

Even if a program has only a single view, model/view separation often leads to a nice design, since it partitions the program into two independent and smaller problems.

For example, suppose that you are writing a program to play Tic-Tac-Toe. A model class might hold the state of the board: it knows where each player has played. The model class can also check to see whether either player has won. But a model class should not know that one player's moves will be presented using the character 'X' or 'O', or that the board itself will be displayed using any kind of text representation, such as using the characters "---" to separate squares.