C# Quick Reference

This is a summary of the subset of C# that we have learned so far in Programming II. For more details on any of the features below, see the Essential C# textbook or the C# reference pages.

table of contents

comments

A single-line comment begins with // and extends to the end of the line:

x += 1;    // increment x 

Comments delimited with /* and */ can extend over multiple lines:

/*
  this is a comment with
  multiple lines
*/

types

Each short type name in C# abbreviates a longer name. For example, int is the same as System.Int32. You can see the full names in the Library Quick Reference. You can use the shorter and longer names interchangeably.

integer types

A literal integer (or floating-point value) may contain embedded underscores for readability:

    int i = 1_000_000_000;    // 1 billion

floating-point types

bool

The bool type represents a Boolean value, namely either true or false.

char

A char is a 16-bit Unicode character.

A character constant is enclosed in single quotes, e.g. 'x' or 'ř'. A character constant containing a backslash is an escape sequence. Here are some common escape sequences:

To create a character constant representing a single quote or backslash, use one of the sequences above:

    WriteLine('\\');  // writes a backslash
    WriteLine('\'');  // writes a single quote

string

A string is an immutable string of characters. A string constant is enclosed in double quotes, e.g. "hello".

You may access individual characters of a string using square brackets:

    string s = "spire";

    char c = s[2];    // now c = 'i'

Note that characters are indexed starting from 0.

verbatim strings

Strings may normally contain the escape sequences described above. But you may prefix a string with @ to create a verbatim string in which the backslash is an ordinary character. A verbatim string may even contain newlines:

    WriteLine(@"\one\
    \two\
    \three\"
    );

writes

\one\
\two\
\three\

interpolated values

A string beginning with $ may contain interpolated values enclosed in braces:

    int a = 3, b = 4;

    WriteLine($"{a} plus {b} equals {a + b}");

A interpolated value may be followed by a format specifier (preceded by a colon).

There are many predefined format specifiers. Many format specifiers can be followed by an integer called the precision, whose meaning varies among specifiers. Here are a few of the most useful:

For example,

    const int i = 1357;
    const double pi = 3.14159;

    WriteLine($"{i:d6} {pi:f2} {i:n0}");

writes

    001357 3.14 1,357

arrays

You can allocate an array like this:

    int[] i = new int[10];

Arrays are indexed from 0.

You can allocate and initialize an array at the same time:

    i = new int[3] { 3, 4, 5 };

If you allocate and initialize an array as part of a variable declaration, you can skip the new operator:

    int[] i = { 3, 4, 5 };

The Length property returns the length of an array.

multidimensional arrays

You can similarly allocate a multidimensional array using the new operator:

    int[,] i = new int[3, 4];

You can initialize a multidimensional array as you allocate it:

    i = new int[2, 2] { {1, 4}, {2, 3} };

Once again, in a variable declaration you can skip the new operator:

    int[,] i = { {1, 4}, {2, 3} };

For a multidimensional array, the Length property returns the total number of elements in the array. This is the product of the lengths of all dimensions. Also see Rank and GetLength in the Library Quick Reference.

jagged arrays

A jagged array is an array of arrays. This is not the same thing as a multidimensional array. A multidimensional array has a rectangular shape, whereas each array in a jagged array can have a different length.

You can allocate a jagged array like this:

    int[][] i = new int[3][];
    i[0] = new int[2];
    i[1] = new int[4];
    i[2] = new int[5];

If you like, you can initialize a jagged array as you allocate it:

    int[][] i = {
      new int[] { 2, 3},
      new int[] { 3, 4, 5},
      new int[] { 1, 3, 5, 7, 9}
    };

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

value and reference types

Every type in C# is either a value type or a reference type.

When a variable's type is a value type, the variable holds a value. An assignment between variables of value type copies the value from one variable to another.

When a variable's type is a reference type, the variable holds a pointer to an object. An assignment between variables of reference type makes them point to the same object.

For example:

    int[] a = { 4, 5, 6 };
    int[] b = a;       // now b and a point to the same array
    a[1] = 7;
    WriteLine(b[1]);   // writes 7

null

Any variable of reference type may hold the special value null.

Note especially that null is not the same as the empty string.

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

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.

operators

equality operators

Note that when comparing values of reference type, by default these operators use reference equality: they return 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.)

relational operators

arithmetic binary operators

assignment operator

An assignment is actually an expression in C#:

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

compound assignment operators

boolean operators

conditional operator

The conditional operator (? :) is similar to the if statement, but it is an expression, not a statement. It takes a Boolean value and two extra values. It returns the first of these values if the boolean is true, or the second if it is false. For example, the following statement computes the greater of i and j and assigns it to k:

    int i, j;
    ...
    int k = (i > j ? i : j);

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.

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();

lifted operators

Many operators have lifted versions which operate on nullable types.

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

default

The default operator returns the default value for a type:

    WriteLine(default(int));   // writes 0

default is most useful inside a generic class, where it can act on a type parameter. For example, a dynamic array class could have this code:

class DynArray<T> {
  T[] a;
  int count;

  

  public T this[int index] {
    // return default value if out of bounds
    get => index < count ? a[index] : default(T);

    set => 
  }
}

conversions

C# will perform an implicit conversion between two numeric types if every possible value of the source type is valid in the destination type. For example:

    short s;
    ...
    int i = s;   // implicit conversion

You can also implicitly convert a char to any numeric type that can hold a 16-bit unsigned value:

    char c = 'ř';
    int i = c;   // now i holds the Unicode value for 'ř', i.e. 345

You can use an explicit conversion to force a conversion between any two numeric types. This is accomplished with a type cast:

    int i = 1000;
    short s = (short) i;

If the destination type cannot hold the source value, it will wrap around or be truncated.

You can explicitly convert any numeric type to a char:

    int i = 100;
    char c = (char) i;  // now c is 'd', which is ASCII/Unicode character 100

statements

local variable declarations

A local variable may optionally have an initial value:

    int i = 3, j = 4;

Implicitly typed variables

When you declare a local variable, you can use the var keyword to tell the compiler to infer its type. For example:

    var list = new List<int>(5);

This is equivalent to

    List<int> list = new List<int>(5);

local constant declarations

A local constant is like a local variable, but is fixed at compile time:

    const int Million = 1_000_000;

if

    if (i > 0)
      WriteLine("positive");
    else if (i == 0)
      WriteLine("zero");
    else
      WriteLine("negative");

An if statement executes a statement (or block) if the given value is true. If the statement has an else clause, it is executed if the given value is false.

while

    while (i < 10) {
      sum = sum + i;
      i += 1;
    }

A while loop loops as long as the given condition is true.

do / while

    do {
      s = ReadLine();
    } while (s != "yes" && s != "no");

A do/while loop is like a while loop, but checks the loop condition at the bottom of the loop body.

for

    for (int i = 0, j = 0 ; i < 10 ; i += 1, j += 2)
      WriteLine(i + j);

A for statement contains three clauses, separated by semicolons, plus a loop body.

foreach

    foreach (char c in "hello")
      Write(c + " ");

foreach iterates over each element of an object that implements IEnumerable<T>. (See the class library reference for details about this interface.) Such objects include

A foreach statement always declares an iteration variable whose scope is the body of the statement.

break

    for (int i = 1 ; i <= 10 ; ++i) {
      if (n % i == 0)
        break;

      WriteLine(i);
    }

A break statement breaks out of the nearest enclosing while, do/while, for or foreach loop.

continue

    while (i < 100) {
      if (n % i == 0)
        continue;

      WriteLine(i);
      n += 1;
    }

A continue statement continues with the next iteration of the nearest enclosing while, do/while, for or foreach loop.

return

    static int add(int x, int y) {
      return x + y;
    }

The return statement returns a value immediately from the enclosing method.

switch

    switch (i) {
      case 1:
        WriteLine("one");
        break;
        
      case 2:
        WriteLine("two");
        break;
      
      default:
        WriteLine("other");
        break;
    }

The switch statement is a more compact (and possibly more efficient) alternative to a series of if statements. The default section is optional. Each case in a switch statement must end with a break statement.

goto

    again:
      WriteLine(i);
      i += 1;
      if (i < 10)
        goto again;

The goto statement jumps to another point in a block of code. You should rarely if ever use it.

throw

The throw statement throws an exception, which can be any object belonging to the System.Exception class or any of its subclasses. The exception will pass up the call stack, aborting the execution of any methods in progress until it is caught with a try...catch block at some point higher on the call stack. If the exception is not caught, the program will terminate.

try

The try statement attempts to execute a block of code. It may have a set of catch clauses and/or a finally clause.

A catch clause catches all exceptions of a certain type. For example:

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

The code above will catch an exception of class FileNotFoundException, or of any subclass of it. When an exception is caught, the catch block (called an exception handler) executes. The catch block may itself rethrow the given exception, or even a different exception. If the catch block does not throw an exception, execution resumes below the try statement.

A finally clause will always execute, even if an exception is thrown inside the body of the try statement. For example:

  StreamReader reader = ;
  StreamWriter writer = ;
  try {
    while (reader.ReadLine() is string s)
      writer.WriteLine(transform(s));
  } finally {
    reader.Close();
    writer.Close();
  }

In this close, reader and writer will be closed even if an exception occurs within the try body (for example, within the transform method). Note that a finally clause does not itself catch an exception, which will continue to pass up the call stack.

The preceding example is equivalent to

  StreamReader reader = ;
  StreamWriter writer = ;
  try {
    while (reader.ReadLine() is string s)
      writer.WriteLine(transform(s));
  } catch (Exception e) {
    reader.Close();
    writer.Close();
    throw e;
  }

yield return

An iterator is a special kind of method that generates a sequence of values. Each time that the caller requests the next value in the sequence, an iterator's code runs until it calls the yield return statement, which yields the next value in the sequence. At that point the iterator is suspended until the caller requests the next value, at which point the code continues executing until the next yield return statement, and so on. When execution reaches the end of the iterator method body, the sequence is complete.

An iterator must have return type IEnumerable<T> (or IEnumerator<T>) for some (concrete or generic) type T. Here is a simple iterator:

  static IEnumerable<int> range(int start, int end) {
    for (int i = start ; i <= end ; ++i)
      yield return i;
  }

Now, for example, we can add the squares of the numbers from 1 to 10 like this:

  using System.Linq;

  int sum = range(1, 10).Select(i => i * i).Sum();

methods

A method is like a function, but belongs to a class. In a method declaration, the return type precedes the method name:

    static int mul(int x, int y) {
      return x * y;
    }

To call a method, specify values for each of its parameters:

    int i = mul(16, 36);   // now i = 576

expression-bodied methods

If a method’s body is a simple expression, the method can be defined using a compact syntax:

    static int mul(int x, int y) => x * y;

overloaded methods

You may declare multiple methods that have the same name but have different numbers and/or types of parameters. This is called method overloading. For example:

  static int more(int i) => i + 1

  static int more(short s) => s + 2
  
  static string more(string s) => s + " "

When calling an overloaded method, sometimes there is more than one candidate method. For example, consider this method call:

  byte b = 77;
  WriteLine(more(b));

Both of the first two method declarations above are candidates for this call, since byte is implicitly convertible to both int and short. In this situation, C# will favor the overload that involves converting to a more specific type. short is more specific than int, so in the example above C# will call the method

  int more(short s)

and will write the value 79.

For any types T and U, C# considers T to be more specific than U if there is an implicit conversion from T to U, but none from to U.

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);

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)

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);

nested methods

Methods in C# may be nested. A nested method appears inside the body of another method. For example:

  static void Main1(string[] args) {
    WriteLine("hello");

    double arg(int n) => double.Parse(args[n]);

    double d = arg(0);
    double e = arg(1);    
    WriteLine(d + e);
  }

A nested method may access parameters and local variables in its containing method. For example, the nested method arg above accesses the args parameter.

generic methods

A method may be generic: it may take one or more type parameters. For example:

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

generic method constraints

A method's type parameters may have constraints. For example:

public static void copy<T, U>(T[] a, U[] b)
                   where T : U {
    for (int i = 0 ; i < a.Length ; ++i)
      b[i] = a[i];
  }

For details about the types of constraints that are allowed, see the section "Generic class constraints", below.

extension methods

You can add extension methods to an existing class. An extension method can be used syntactically as if belonged to a class, even though it is written outside the class.

For example, suppose that we are using a C# library that provides this type:

  class Vector {
    public double dx, dy;
    public Vector(double dx, double dy) { this.dx = dx; this.dy = dy; }
  }

We wish that the author of the class had provided a length method that calculates the length of a Vector. Since we cannot modify the class, we can write an extension method:

  static class Util {
    public static double length(this Vector v) =>
        Sqrt(v.dx * v.dx + v.dy * v.dy);
  }

Now we can call the method as if it had been defined inside the Vector class itself:

  Vector v = new Vector(3.0, 4.0);
  WriteLine(v.length());  // writes 5.0

Note that an extension method must be static and must be contained in a static class.

classes

A class is an abstract data type that can contain fields, constructors, methods, properties, indexers, overloaded operators and other kinds of members.

Here is a definition for a simple class representing a point in two dimensions. It has two fields, one constructor and two methods:

using static System.Math;

class Point {
  double x, y;
  
  public Point(double x, double y) { this.x = x; this.y = y; }
  
  public void move(double dx, double dy) {
    x += dx;
    y += dy;
  }

  public double distanceFromOrigin() {
    return Sqrt(x * x + y * y);
  }
}

access levels

Every member in a class can be either public, protected or private.

The default access level is private.

fields

Each instance of a class contains a set of fields.

A field may be declared with an initial value:

class Foo {
  int x = 3, y = 4;
  

If you don't specify an initial value, a field will intially be set to its type's default value (e.g 0 for an int).

Code in any method, constructor or other member may refer to fields by name and may modify their values.

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; }
}

constructors

A constructor makes a new instance of a class. It always has the same name as its containing class.

To call a constructor, use the new operator and pass any required arguments:

    Point p = new Point(3.0, 4.0);

If a class definition includes no constructors, then C# provides a default constructor that takes no arguments.

Constructors may be overloaded. A constructor may call another constructor; this is called constructor chaining. For example, we could add a second constructor to the Point class that chains to the constructor with two parameters:

    public Point(double x) : this(x, 0.0) {  }

instance methods

An instance method is invoked on an instance of a class. Any additional arguments appear between parentheses:

    Point p = new Point(3.0, 4.0);  // a constructor call
    p.move(1.0, 2.0);               // a method call

For more information about methods, see the Methods section above.

this

this is a special value that refers to the object on which a method, constructor or other member was invoked.

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

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 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);
  }
}

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

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 LimitStack(5);
    s.push(4);
    s.push(6);

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

Note that C# supports only single class inheritance: a class may inherit from only one other class.

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.

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.

Here is an example of an abstract class that maintains a count of objects in a stack. Any concrete subclass (for example, a class implementing a stack using a linked list) 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; }
}

generic classes

A class may be generic, taking one or more type parameters:

class Pair<T> {
  public T first, second;
  
  public Pair(T first, T second) {
    this.first = first; this.second = second;
  }
}

generic class constraints

A class's type parameters may include constraints. For example:

class Maximizer<T> where T : IComparable<T> {
  T _max;
  bool empty;
  
  public void add(T t) {
    if (empty || t.CompareTo(_max) > 0)
      _max = t;
    empty = false;
  }
  
  public T max { get => _max; }
}

Each type constraint can have one of the following forms:

static members

A field, constructor, method, property or indexer may be static. Static members are shared by all instances of a class.

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

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

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

A static method is invoked on a class, not on an instance:

    Foo.inc();

Static methods may access private members of any instance of their class. For example, we can add a static method to the Point class that compares two Point objects and returns true if they are equal:

  public static bool eq(Point p, Point q) =>
    p.x == q.x && p.y == q.y;

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. For example, we could declare the class Foo above as

    static class Foo {
      ...

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();
}

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.

generic interfaces

An interface may be generic, taking one or more type parameters. For example:

interface Map<K, V> {
  V this[K key] { get; set; }
}

covariance

Suppose that C is a generic class or interface that has a single type parameter, and that U is a subtype of T. By default, C's type parameter is invariant: the types C<T> and C<U> are not convertible to each other.

Classes' type parameters in C# are always invariant. An interface's type parameters, however, may be marked as covariant using the out keyword. If I is a generic interface with a covariant type parameter and U is a subtype of T, then I<U> is convertible to I<T>.

If an interface's type parameter T is covariant, then the interface's methods and properties may not receive any values of type T as parameters.

For example, suppose that we've written a generic class DynArray representing a dynamic array:

class DynArray<T> {
  T[] a = new T[1];
  int count;
  
  public int length { get {  } set {  } }
  
  public void add(T t) {  }
  
  public T this[int index] { get {  } set {  } }
  
  
}

And suppose that we have an abstract class Shape with subclasses Rectangle, Circle and Triangle:

abstract class Shape {
  public abstract double area { get; }
}

class Rectangle : Shape {
  public double width, height;
  public Rectangle(double width, double height) {  }
  
  public override double area { get => width * height; }
}

class Circle : Shape {
  public double radius;
  public Circle(double radius) { this.radius = radius; }

}

class Triangle : Shape {  }

Finally, suppose that we have a method areaSum that adds the area of all Shapes in a dynamic array:

  public static double areaSum(DynArray<Shape> a) {
    double sum = 0;
    for (int i = 0 ; i < a.length ; ++i)
      sum += a[i].area;
    return sum;
  }

The following code will not compile:

  DynArray<Rectangle> r = new DynArray<Rectangle>();
  r.add(new Rectangle(10, 2));
  r.add(new Rectangle(20, 4));
  double a = areaSum(r);   // ???

That's because the type DynArray<Rectangle> is not convertible to DynArray<Shape>. Such a conversion would be unsafe, since it would allow the following:

  public static void addCircle(DynArray<Shape> a) {
    a.add(new Circle(5.0));
  }

  DynArray<Rectangle> r = new DynArray<Rectangle>();
  addCircle(r);  // ???

Here is an interface with an covariant type parameter:

interface ReadOnlyArray<out T> {
  int length { get; }
  T this[int index] { get; }
}

Suppose that the DynArray class above implements this interface:

class DynArray<T> : ReadOnlyArray<T> {  }

And suppose that we modify areaSum to take a ReadOnlyArray<Shape> as its argument:

  public static double areaSum(ReadOnlyArray<Shape> a) {
    double sum = 0;
    for (int i = 0 ; i < a.length ; ++i)
      sum += a[i].area;
    return sum;
  }

Now we may pass a DynArray<Rectangle> to areaSum:

  DynArray<Rectangle> r = new DynArray<Rectangle>();
  r.add(new Rectangle(10, 2));
  r.add(new Rectangle(20, 4));
  double a = areaSum(r);  // will now compile

You will get a compile-time error if you attempt to mark the following interface's type parameter T as covariant, since the interface's methods and properties receive values of type T:

interface Arr<T> {
  int length { get; set; }
  void add (T t);
  T this[int index] { get; set; }
}

Contravariance is a complement to covariance. You can mark a type parameter as contravariant using the in keyword. If I is a generic interface with a contravariant type parameter and U is a subtype of T, then I<T> is convertible to I<U>.

array covariance

Surprisingly, arrays in C# are covariant. For example, the following code will compile:

Rectangle[] a = new Rectangle[5];
Shape[] b = a;   // Rectangle[] is convertible to Shape[]
b[0] = new Circle(4.0);

But the last statement above will fail at run time since b is actually a Rectangle[] and a Circle cannot be added to a Rectangle[].

I (and many other people) believe that this array covariance is a design flaw in the C# language. It has the following negative consequences:

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.

delegates

A delegate holds a reference to a method. It is similar to a function pointer in languages such as Pascal or C. (A delegate is also sometimes called a closure in functional programming.)

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 hold a reference to a method:

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

  static void Main() {
    IntCondition c = isOdd;
    

We can invoke the delegate using method 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) => 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:

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

lambda expressions

A lambda expression is an anonymous function that can appear inside another expression.

For example, here is a delegate type for a function from integers to integers:

delegate int IntFun(int i);

And here is a method that applies a given function to every element of an array of integers:

  static int[] map(int[] a, IntFun f) {
    int[] b = new int[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:

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

  static int[] add2(int[] a) => map(a, plus2);

Alternatively, we can invoke map using a lambda expression:

  static int[] add2(int[] a) => map(a, i => i + 2);

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

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

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

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

  static int[] add_k(int[] a, int k) => map(a, i => i + k);

The lambda expressions in the examples above are expression lambdas, writen using a compact syntax similar to the expression syntax for methods. Alternatively, a lambda expression can be written as a statement lambda, which can include one or more statements and can use the return statement to return a value. For example, we can rewrite the last example above like this:

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

functions as return values

In the examples above we've seen that a method can take a delegate (i.e. a function) as an argument. A method can also return a delegate constructed using a lambda expression.

Here is a simple example:

  delegate bool IntCondition(int i);
  static IntCondition divisibleBy(int k) =>
    i => (i % k == 0);

Now we can invoke this method and use the delegate that it returns:

  IntCondition div3 = divisibleBy(3);
  WriteLine(div3(6));  // writes 'True'
  WriteLine(div3(7));  // writes 'False'

In this example, note that the delegate returned by divisibleBy can refer to the parameter k even after the method divisibleBy has returned! To put it differently, the lambda expression i => (i % k == 0) has captured the parameter k. Local variables may also be captured by a lambda expression.

Using lambda expressions we can write functions that transform other functions. This is a powerful technique that you may explore further in more advanced courses about functional programming. Here are just a couple of examples of this nature. First, here is a function that composes two functions f and g, returning the function (f g), which is defined as (f g)(x) = f(g(x)):

  static IntFun compose(IntFun f, IntFun g) =>
    i => f(g(i));

We can call compose as follows:

  IntFun f = compose(i => i * 2, i => i + 1);  // now f(x) = 2 * (x + 1)
  WriteLine(f(4));   // writes 10

Second, here's a function that computes the nth power of a function f, defined as

fn(x) = f(f(...(f(x))) [ f appears n times in the preceding expression ]

  static IntFun power(IntFun f, int n) =>
    i => {
      for (int j = 0 ; j < n ; ++j)
        i = f(i);
      return i;
    };

Addition to a power is multiplication:

  IntFun f = power(i => i + 10, 4);
  WriteLine(f(2));  // writes 42

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 is 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;
      changed(i, prev, a[i]);
    }
  }
}

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 foo() {
    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

namespaces

Types in the class library are grouped into namespaces. For example:

The using statement must appear at the top of a source file. It imports types from a namespace, allowing you to use them without specifying a namespace every time. For example, if a source file includes

   using System.IO;

then it may refer to StreamReader without any namespace prefix.

top-level programs

    using static System.Console;

    class Hello {
      static void Main() {
        WriteLine("hello, world");
      }
    }