Lecture 5

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

assignment expressions

An assignment is actually an expression in C#:

          y = (x = 4) + 2;  // assign 4 to x and 6 to y

structs

Structs are similar to classes, but they are value types, not reference types. Here is a simple struct:

struct Point {
  public double x, y;
  
  public Point(double x, double y) { this.x = x; this.y = y; }
  
  public static double distance(Point p, Point q) =>
    Sqrt((p.x - q.x) * 2 + (p.y - q.y) * 2);
}

Structs have some limitations:

It is sometimes more efficient to use structs than classes, since a variable of struct type does not contain an implicit pointer. On the other hand, each time a struct is passed as a method argument, it is copied, which is more expensive than passing a class reference by value. So in some situations structs are less efficient.

I generally recommend that you use structs only for data types that contain just a few values such as integers or doubles. For example, types that represent rational numbers or complex numbers would be good candidates for structs.

interfaces

An interface defines a set of methods, properties and/or indexers that can be implemented by a class. For example:

interface Stack {
  bool isEmpty { get; }
  void push(int i);
  int pop();
}

A class may implement one or more interfaces:

class LinkedStack : Stack {
  Node head;

  public bool isEmpty { get => (head == null); }
  
  public void push(int i) {
    head = new Node(i, head);
  }
  
  public int pop() {
    ...
  }
}

An interface may inherit from one or more other interfaces. For example, we can refactor the Stack interface above as follows:

interface Collection {
  bool isEmpty { get; }
}

interface Stack : Collection {
  void push(int i);
  int pop();
}

interface Queue : Collection {
  void enqueue(int i);
  int dequeue();
}

subtypes

In some situations a class may be a subtype of another type:

For example:

    interface Collection {  }
    interface Stack : Collection {  }
    class SimpleStack : Stack {  }
    class HyperStack : SimpleStack {  }

Here, HyperStack is a subtype of SimpleStack, which is a subtype of Stack, which is a subtype of Collection.

A type may be implicitly converted to any supertype:

    Collection c = new HyperStack();  // implicit conversion from HyperStack to Collection

A type may be explicitly converted to any subtype:

    HyperStack h = (HyperStack) c;    // explicit conversion from Collection to HyperStack

This explicit conversion will fail at runtime if the object in question does not actually belong to the subtype.

explicit member implementation

Usually a class implements interface methods (and other members) implicitly, as in the LinkedStack example above. An implicit member implementation includes the keyword public and is visible through variables of either the class type or the interface type:

LinkedStack ls = new LinkedStack();
ls.push(3);
Stack s = ls;
s.push(4);

Alternatively, a class may implement a member explicitly. An explicit member implementation is prefixed with the interface name and may not be public:

class AlterStack : Stack {

  void Stack.push(int i) {  }
  
}

An explicit member implementation is visbile only through a variable of the interface type, not the class type:

AlterStack a = new AlterStack();
a.push(3);  // invalid - compile error

Stack b = new AlterStack();
b.push(3);  // works fine

Explicit member implementations are useful when a class implements two interfaces and a member with the same name occurs in both interfaces, i.e. when there is a name conflict. In this situation, a class can provide a separate explicit implementation for each interface. But we will not often (if ever) encounter this situation in this course.

is

The is operator returns true if a value belongs to a type. It works with both nullable types and reference types:

    int? i = abc();
    if (i is int)  // true if i != null
      WriteLine(i.Value);
    
    Stack s = getStack();
    if (s is LinkedStack)
      WriteLine("linked");

The is operator can optionally bind a variable. The first example above can be rewritten as

    if (abc() is int i)
        WriteLine(i);

Here is a loop using is:

    while (ReadLine() is string s)
        WriteLine(s);

In this loop, when ReadLine() returns a non-null value, the string variable s receives that value and the loop continues. When ReadLine() returns null, the loop terminates.

as

The as operator checks whether a value belongs to a type. If so, it returns the value; otherwise it returns null:

    Stack s = getStack();
    LinkedStack ls = s as LinkedStack;  // if s was not a LinkedStack, ls will be null

class inheritance

A class may inherit from another class. For example:

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

class AvgStack : LinkedStack {
  int count;
  int sum;
  
  public override void push(int i) {
    base.push(i);
    count += 1;
    sum += i;
  }
  
  public override int pop() {
    int i = base.pop();
    count -= 1;
    sum -= i;
    return i;
  }
  
  public double average {
    get => (double) sum / count;
  }
}

An child class may override methods, properties or indexers in its base class. Only a member marked as virtual can be overridden. Any overriding member must include the override keyword.

A method in a child class can invoke a base class method using the base keyword as in the example above.

Note that an object's behavior is determined entirely by its actual class, not by the type of any containing variable. For example:

    LinkedStack s = new AvgStack();
    s.push(4);
    s.push(6);
    AvgStack t = (AvgStack) s;
    WriteLine(t.average);  //  writes 5

In this example, the calls to the push method invoke the implementation in AvgStack, even though the containing variable has type LinkedStack.