I recommend that you write the following at the top of every C# source file:
using static System.Console;
That will allow you to conveniently invoke methods as WriteLine() and ReadLine() without the "Console." prefix:
WriteLine("hello"); string? line = ReadLine();
More generally, a using
static
declaration
imports all
static
methods from
a given class, so that you can use these methods without prefixing
with them with the class name. A static method such as WriteLine() is
invoked on a class itself, not on an instance of the class. We'll
talk more about static methods in the next lecture.
A do
/while
loop is similar to a while
loop, but
checks the loop condition at the bottom of the loop body. For
example:
string? s; do { s = ReadLine(); } while (s != "yes" && s != "no");
Note that the body of an ordinary while
loop might not execute at all, but the body of a do
/while
loop will always execute at least once.
In most programs do
/while
loops are less common than ordinary while
loops, but they are still occasionally useful.
A switch
statement is
a more compact (and possibly more efficient) alternative to a series
of if statements. It looks like this:
switch (i) { case 11: WriteLine("jack"); break; case 12: WriteLine("queen"); break; case 13: WriteLine("king"); break; case 1: case 14: WriteLine("ace"); break; default: WriteLine(i); break; }
In this example, the group of statements that matches the value of (i) will run. For example, if i is 12, the code will print "queen". If it's either 1 or 14, it will print "ace".
Each section in a switch
statement must end with a break
or
return
statement. The default
section is optional, and runs if no other case is matched.
C# also includes switch
expressions. Here's an example:
string name = i switch { 11 => "jack", 12 => "queen", 13 => "king", 1 or 14 => "ace", _ => i.ToString() };
Notice that a switch
expression is quite
a bit more compact than a switch
statement. In it, an underscore (_) represents a default case.
Actually switch
statements and expressions can do a lot more than this – for
example, they can choose a case based on the type of a value,
and can match patterns. We'll discuss these capabilities later in
this course.
The conditional operator (?
:
) is similar to the if
statement, but it is an expression, not a statement. It takes
a Boolean value plus 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; // set k to the maximum of i and j
This operator is also sometimes called the ternary operator since it takes three operands. We also saw this operator in Python, which uses a completely different syntax for it:
k = i if i > j else j; # Python code: set k to the maximum of i and j
A tuple holds two or more values. Unlike arrays, tuples have fixed length and can hold values of varying types.
A tuple type is written as a list of element types in parentheses. For example:
(int, string) p = (3, "hello");
We can access a tuple's elements using the names Item1, Item2 and so on:
WriteLine(p.Item1); // writes 3 WriteLine(p.Item2); // writes "hello"
You can use multiple assignment to unpack a tuple into a set of variables:
(int x, string s) = p; // now x is 3, s = "hello"
As another example, you can unpack a tuple into variables using pattern matching in a foreach loop:
(int, int)[] points = { (2, 3), (5, 7), (8, 9) }; foreach ((int x, int y) in points) WriteLine($"{x}, {y}");
The generic element names Item1
and Item2
are not very informative.
Sometimes it is clearer to use a named tuple,
which specifies names for the elements. For example:
(int x, int y) p = (10, 20);
Now we can access the tuple elements by name:
WriteLine($"x = {p.x}, y = {p.y}");
Be sure to note the difference between the syntax for multiple assignment and the syntax for creating a named tuple – these may look similar at first. The declaration above creates a single variable p with fields p.x and p.y. By contrast, the line
(int x, int y) = (10, 20);
is a multiple assigment that creates local variables x and y.
You may also create a named tuple using pattern matching in a foreach loop:
(int, int)[] points = { (2, 3), (5, 7), (8, 9) }; foreach ((int x, int y) p in points) WriteLine($"{p.x}, {p.y}");
This is equivalent to the foreach loop in the previous section. The choice between these is a matter of style.
In the last lecture we saw basic syntax for defining functions in C#:
int mul(int x, int y) { return x * y; }
This function just returns an expression. C# lets us define functions such as this using a compact syntax:
int mul(int x, int y) => x * y;
This is called an expression-bodied function. I recommend using this syntax when possible.
You can make a function 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.
When you invoke any function, 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 parameters are optional, you may specify
any subset of them you like using named arguments. For example, we
may invoke the fill
function from the
previous section as
fill(a, val: 4) // same as fill(a, 0, 4)
If the last parameter to a function is marked with
the keyword params
and has an array type
T[], then in its place the function 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 function computes the sum of any number of integers:
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);
A
parameter marked with out
returns a value to the caller. For example, this function takes two
integers and returns their quotient as an ordinary return value. It
also returns the remainder via an out parameter:
int divide(int a, int b, out int remainder) { remainder = a % b; return a / b; }
You must include the keyword out
when
passing a variable to an out
parameter:
int q, r; q = divide(3, 4, out r);
You can declare a new variable as you invoke a method with an out
parameter. The following is equivalent to the two preceding lines:
int q = divide(3, 4, out int r);
A
parameter marked with ref
is passed by reference: the method can modify the variable that is
passed. For example, here's a function that swaps two integer
variables:
void swap(ref int a, ref int b) { (a, b) = (b, a); }
You must include the keyword ref
when
passing an argument by reference:
int i = 3, j = 4; swap(ref i, ref j); WriteLine(i); // writes 4
A class is a user-defined data type that can contain fields, constructors, methods, and other kinds of members.
As a first example, here's a simple class implementing a point in three dimensions:
class Point { public double x, y, z; // fields // constructor public Point(double x, double y, double z) { this.x = x; this.y = y; this.z = z; } public bool is_zero() => x == 0.0 && y == 0.0 && z == 0.0; }
We may create a Point like this:
Point p = new Point(3.0, 4.0, 6.0); WriteLine(p.is_zero()); // will write "false"
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
).
Every member in a class can
be either public
or private
.
public
members are accessible everywhere, and private
members are accessible only within the class.
The default access level is private
.
A constructor makes a new instance of a class. It always has the same name as its containing class.
A constructor will often intialize fields, as in the Point class above.
If a class definition includes no constructors, then C# provides a default constructor that is public and takes no arguments.
this
is a special value that refers to the object on which a constructor
or method was invoked. (It's like the self
variable in Python.) In code, you may access a member x of the
containing class by writing either "this.x" or simply "x".
For example, in the method is_zero() in the Point class above we
could have written
this.x
== 0.0 && this.y == 0.0 && this.z == 0.0;
but instead we just referred to the fields x, y, and z directly. (This is a significant difference from Python, in which we must always write self.x to access the attribute x.)
On the other hand, in the Point() constructor above we had to write "this.x" to access the field x, since the constructor also has an parameter named x. (If we wrote "x = x" that would assign the parameter to itself, which would do nothing.)
As another example, here's a class implementing a fixed-size stack:
class Stack { // fields int[] a; int count; // constructor public Stack(int max_size) { a = new int[max_size]; count = 0; } // methods public bool is_empty() => count == 0; public void push(int i) { a[count] = i; count += 1; } public int pop() { count -= 1; return a[count]; } }
Notice that in this class the fields are private. Usually it's good programming practice to make fields private, except in classes whose only purpose is to hold data (such as the Point class above). Then the caller can only modify the class's state by calling its public interface.
In a class you may declare multiple methods or constructors that have the same name but have different numbers and/or types of parameters. This is called overloading.
For example, here's a class that implements a vector with any number of dimensions:
class Vec { double[] a; public Vec(params double[] a) { this.a = a; } public Vec(int dims) { this.a = new double[dims]; } // Compute the sum of this vector and the vector w. public Vec add(Vec w) { double[] b = new double[a.Length]; for (int i = 0 ; i < a.Length ; ++i) b[i] = this.a[i] + w.a[i]; return new Vec(b); } }
Notice that there are two overloaded constructors: one takes any number of parameters representing coordinates, and the other creates a zero vector of any number of dimensions. Let's create two vectors using these constructors:
Vec v = new Vec(2.0, 5.0, 10.0); Vec w = new Vec(5); // 5-dimensional zero vector