Lecture 4

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

increment/decrement operators

The increment operator ++ increases a variable or field by 1. It comes in two forms. With the pre-increment operator, the expression's value is the value of the variable after it is incremented:

    int i = 4;
    int j = ++i;  // now i = 5 and j = 5

With the post-increment operator, the expression's value is the value of the variable before it is incremented:

    int i = 4;
    int j = i++;  // now i = 5 and j = 4

The decrement operator -- also has pre-decrement and post-decrement forms, and works similarly.

reference equality

Note that when comparing values of reference type, by default the == operator uses reference equality: it returns true only if two objects are actually the same object, and do not consider objects' fields. For example:

    class Foo {
      int i;
      public Foo(int i) { this.i = i; }
    }

    WriteLine(new Foo(3) == new Foo(3));  // writes False

(A particular class may overload these operators to test equality in some different way.)

nullable types

Value types such as int and bool cannot hold null. However, each such type can be made nullable by appending a ? character to its name. A nullable type holds either an instance of its base type, or the value null. For example:

    int? x = 7;
    if (x > 3)
       x = null;

A value of the base type can be converted to a nullable type implicitly:

    int y = abc();
    int? z = y;

To convert from a nullable type to its base type, use an explicit cast or access the Value property. These are equivalent, and will fail with an exception at run time if the value is null.

    int? a = xyz();
    int b = (int) a;
    int c = a.Value;  // equivalent

null-coalescing and null-conditional operators

The null-coalescing operator ?? operates on a value of any nullable type, including reference types. If the value is null, the operator returns its second operand; otherwise the value itself is returned. For example:

    int? x = abc();
    
    int y = x ?? 0;
    int z = (x == null) ? 0 : x.Value;  // equivalent

The null-conditional operator ?. operates on a value of any nullable type, including reference types. It invokes a method if the value is non-null. If the value is null, the operator returns null. For example:

    string s = abc();
    string t = s?.Trim();

It is often convenient to chain method calls using this operator:

    string t = s?.Trim()?.ToLower();

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;

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 constructors and classes

A static constructor always takes no arguments. It cannot be invoked explicitly, and runs automatically when a class is accessed for the first time. For example:

    class Foo {
      static int x = 1;
      static int y;

      static Foo() {   // a static constructor
          y = x + 1;
      }
   
      static void inc() { x += 1; }
    }

You may mark a class itself as static to indicate that it may only have static members; there can be no instances of such a class.

properties

A property is syntactically like a field, but contains a getter and/or a setter, which are methods that run when the caller retrieves or updates the property's value. Inside the setter, the keyword value refers to the value that is being set.

Here is a partial listing of a class Vector that includes a property length:

class Vector {
  double[] v;
  ...
  
  public int length {
    get {
      return v.Length;
    }
    set {
      v = new double[value];
    }
  }
}

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

    public int length {
      get => v.Length;

      set => v = new double[value];
    }

indexers

An indexer allows you to define custom getter and/or setter methods that run when an instance of your class is accessed using the array-like syntax a[i]. For example, we can extend the Vector class above with an indexer that retrieves and sets elements of the underlying array v:

    public double this[int i] {
        get { return v[i]; }
        set { v[i] = value; }
    }

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

    Vector v = new Vector(...);
    v[3] = 77.2;
    v[4] = v[5] + 1.0;

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.

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:

    class Foo {
      int i;
  
      public Foo(int i) { this.i = i; }
  
      public static Foo operator + (Foo a, Foo b) =>
        new Foo(a.i + b.i);
    }

The operator above can be invoked like this:

    Foo f = new Foo(3);
    Foo g = new Foo(4);
    Foo h = f + g;

An overloaded operator must be public and static.

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