Week 3: Notes

This week's topics are covered in Essential C#: see ch. 3 More with Data Types (Nullable Modifier), ch. 5 Methods and Parameters (Advanced Method Parameters, Optional Parameters), ch. 6 Classes (Properties), ch. 9 Value Types (Enums), ch. 10 Well-Formed Types (Operator Overloading), ch. 17 Building Custom Collections (Providing an Indexer).

Here is a summary of the C# elements we discussed:

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

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

optional parameters

You can make a method parameter optional by giving it a default value:

  void fill(int[] a, int start = 0, int val = -1) {
    for (int i = start ; i < a.Length ; ++i)
      a[i] = val;
  }

The caller can omit any optional parameters:

  int[] a = new int[10];
  fill(a);  // same as fill(a, 0, -1)
  fill(a, 5);  // same as fill(a, 5, -1)
  fill(a, 7, 3);

Optional parameters must appear after any non-optional parameters in a parameter list.

named arguments

When you invoke any method, you may precede any argument with a parameter name, followed by a colon.

For example, the Substring method in the string class is defined as

  public string Substring (int startIndex, int length);

We may invoke it in any of these ways:

  string s = "bowling ball";
  string t = s.Substring(1, 3);  // "owl"
  string u = s.Substring(startIndex: 1, length: 3);  // identical
  string v = s.Substring(length: 3, startIndex: 1);  // identical

Notice that named arguments may appear in any order, and must appear after any non-named arguments in an argument list.

If some method parameters are optional, you may specify any subset of them you like using named arguments. For example, we may invoke the fill method from the previous section as

fill(a, val: 4)  // same as fill(a, 0, 4)

ref and out parameters

A parameter marked with ref is passed by reference: the method can modify the variable that is passed. Here's a method that swaps two variables:

    static void swap(ref int a, ref int b) {
      int t = a;
      a = b;
      b = t;
    }

You must include the keyword ref when passing an argument by reference:

    int i = 3, j = 4;
    swap(ref i, ref j);

A parameter marked with out returns a value to the caller. This method takes two integers and returns both their sum and product:

    static void sumAndProduct(int a, int b, out int sum, out int product) {
      sum = a + b;
      product = a * b;
    }

You must include the keyword out when passing a variable to an out parameter:

    int s, p;
    sumAndProduct(3, 4, out s, out p);

You can declare a new variable as you invoke a method with an out parameter. The following is equivalent to the two preceding lines:

    sumAndProduct(3, 4, out int s, out int p);

parameter arrays (params)

If the last parameter to a method is marked with params and has type T[], then in its place the method can receive any number of arguments of type T. The caller may pass individual values separated by commas, or may pass an array of type T[].

For example, this method receives a parameter array of integers and returns their sum:

    static int sum(params int[] a) {
      int s = 0;
      foreach (int i in a)
        s += i;
      return s;
    }

It can be invoked as

    int s = sum(4, 5, 6);

Alternatively, the caller can pass an array:

    int[] a = { 4, 5, 6 };
    int s = sum(a);

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[] a;
  ...
  
  public int length {
    get {
      return a.Length;
    }
    set {
      a = 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 => a.Length;

      set => a = 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 a[i]; }
        set { a[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 Vector {
      double[] a;
  
      public Vector(params double[] a) { this.a = a; }
  
      public static Vector operator + (Vector v, Vector w) {
        double[] b = new double[v.a.Length];
        for (int i = 0 ; i < v.a.Length ; ++i)
          b[i] = v.a[i] + w.a[i];
        return new Vector(b);
      }
    }

The operator above can be invoked like this:

    Vector v = new Vector(2.0, 5.0, 10.0);
    Vector w = new Vector(1.0, 3,0, 9.9);
    Vector x = v + w;

An overloaded operator must be public and static.

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