Up until now, we've always written the Main() method of a C# program with this signature:
static void Main() { ...
Alternatively, Main() may take an array of command-line arguments. Then it will look like this:
static void Main(string[] args) { ...
For example, here's a program that simply echoes each argument to the output:
class Top { static void Main(string[] args) { foreach (string s in args) Console.WriteLine(s); } }
Let's run it:
$ dotnet run one two three one two three $
Notice that (unlike in Python) the name of the program is not present in the args[] array.
Like Python and most other modern languages, C#
has exceptions. Code may throw an exception to indicate
that an exceptional situation has occurred, typically some sort of
error. In an addition, the C# language itself may throw an exception
in some situations, such as if code attempts to access an array
element that's out of bounds, or attempts to access a property of
null
. When an exception is thrown, it will
pass up the call stack, aborting the execution of any methods in
progress until some block of code catches the
exception and continues execution. If the exception is not caught,
the program will print an
error message and terminate.
An exception in C# is an object, specifically any object belonging to the System.Exception class or any of its subclasses. A large number of exception classes are built into the standard library including IndexOutOfRangeException, NullReferenceException, FormatException, InvalidOperationException, and many others.
The throw
statement throws an
exception, either of a built-in or
user-defined exception class. For example:
class OpException : Exception { char op; public OpException(char op) { this.op = op; } } int compute(char op, int a, int b) => op switch { '+' => a + b, '-' => a - b, '*' => a * b, _ => throw new OpException(op) };
The try
statement attempts to execute
a block of code. It may have one or more catch
clauses,
each of which will execute if a certain type of exception is thrown.
For example:
static void Main() { StreamReader reader; try { reader = new StreamReader("numbers"); } catch (FileNotFoundException e) { WriteLine("can't find input file: " + e.FileName); return; } catch (DirectoryNotFoundException) { WriteLine("invalid path"); return; } …
When an exception is caught, the catch block (called an exception handler) executes. As you can see in the code above, a catch block may optionally specify a variable to receive the exception object.
A catch block may rethrow the exception it caught,
or even a different exception. If the catch block does not throw an
exception, execution resumes below the try
statement.
Certain classes in the standard C# library implement the interface IDisposable, which has a single method:
void Dispose ();
This method frees any external resources associated with an object. You should call it when you are finished using an object.
Fort example, the StreamReader and StreamWriter classes (which are commonly used to read from and write to files) implement IDisposable. The Dispose() method in these classes performs the same task as the Close() method: it closes a file or other stream. It is especially important to call Close() or Dispose() when writing to a file - if you do not, some output may not be written!
C#
includes a feature that makes it easy
to dispose resources
automatically.
Specifically, when you declare any variable you may precede it with
the using
keyword.
Then the object in that variable will automatically be freed using
Dispose() as soon as code exits the block of code containing the
variable declaration
(and even if it exits via an exception that was thrown).
For example:
using StreamWriter r = new StreamWriter(filename);
This feature may remind you of Python's with
statement,
which has a similar purpose.
A local method is a method defined inside another method. In some situations these may be quite useful. For example, consider a method dfs() that performs a depth-first search on a graph in adjacency-list representation. Our method will contain a nested method visit() that performs the recursive search:
void dfs(int[][] g) { var visited = new bool[g.Length]; void visit(int v) { WriteLine($"visiting {v}"); visited[v] = true; foreach (int w in g[v]) if (!visited[w]) visit(w); } visit(0); }
A local method may
access parameters and local variables in its containing method. For
example, the local method
visit() above accesses the parameter g
and the
local variable visited
. We could write visit() outside
the dfs() method, but then visit() would need to take both g
and visited
as parameters, which would be less
convenient.
So far we have used generics only to write entire classes. C# also allows us to write generic methods that take one or more type parameters, even outside a generic class. Occasionally this is useful. For example, suppose that we want to write a method that swaps two values of any type T. We can write this as as a generic method:
void swap<T>(ref T a, ref T b) { T t = a; a = b; b = t; }
In C# a class may be nested inside another class. You may want to use a nested class when you write a helper class that's useful only inside another class. For example:
class LinkedList { class Node { public int i; public Node next; public Node(int i, Node next) { this.i = i; this.next = next; } } Node head; public void prepend(int i) { head = new Node(i, head); } }
The Node
class is nested inside LinkedList
.
It's visible inside that class, but since the Node
class
is not explicitly marked as public, it is not accessible from outside
LinkedList
.
A class that's nested inside a generic class may use all the type variables of the containing class. This can be quite convenient. For example, let's make the previous example generic:
class LinkedList<T> { class Node { public T val; public Node next; public Node(T val, Node next) { this.val = val; this.next = next; } } Node head; public void prepend(T val) { head = new Node(val, head); } }
Notice that we do not need to declare Node
as a generic class Node<T>
. It can use the
type variable T found in the containing class LinkedList<T>.
If Node
were not nested in this
example, we'd have to declare it as a generic class Node<T>
since it would be outside the scope of the type variable T. And then
inside the LinkedList
class we would have to write
Node<T>
whenever we referred to that class, which
would be less convenient.
A delegate is a value that represents a function or method. It's similar to to a function object in Python, or a function pointer in languages such as C.
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 refer to a method of corresponding type:
static bool isOdd(int i) => i % 2 == 1; static void Main() { IntCondition c = isOdd; …
We can invoke the delegate using function 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) { return 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. Notice that the method itself must also be generic (indicated by the "<T>" after the method name).
static int count<T>(T[] a, Condition<T> cond) { int n = 0; foreach (T val in a) if (cond(val)) ++n; return n; }
The standard library contains several useful generic delegate types. The built-in type Predicate<T> is exactly equivalent to the type Condition<T> that we just defined:
delegate bool Predicate<T>(T arg);
Additionally, the built-in type Func<T, U> represents an arbitrary function from type T to type U:
delegate U Func<T, U>(T arg);
A lambda expression is an anonymous function that can appear inside another expression. (It's similar to an lambda expression in Python, which we saw last semester).
For example, here's a generic method map() that applies a Func<T, U> to every element of an array of type T[], returning a new array of type U[]:
U[] map<T, U>(T[] a, Func<T, U> f) { U[] b = new U[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():
int plus2(int i) => i + 2; int[] add2(int[] a) { return map(a, plus2); }
Alternatively, we can invoke map() using a lambda expression:
int[] add2(int[] a) {
return map(a, i => i + 2);
}
Here, i => i + 2
is a lambda expression. It's an
anonymous function that takes an integer parameter i and returns the
value i + 2.
Like a local method, a lambda expression may refer to parameters or local variables in its containing method. For example, suppose we want to write a method that adds a given value k to each element in an array. We could write a local method and pass it to map():
int[] add_k(int[] a, int k) { int f(int i) { return i + k; } return map(a, f); }
Or we can use a lambda expression that adds k directly:
int[] add_k(int[] a, int k) {
return map(a, i => i + k);
}
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's 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; if (changed != null) changed(i, prev, a[i]); // fire the event to notify observers } } }
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 Main() { 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
Be warned: if you attempt to raise an event that has no registered handlers, you will get a NullPointerException. In my opinion this is a weakness in the C# event system: if would be nicer if raising such an event did nothing. However, this is how it works. So in the example above, we need to write
if (changed != null) changed(i, prev, a[i]); // fire the event to notify observers
to guard against this condition.