Week 3: Notes

command-line arguments

In many programs we may wish to access command-line arguments.

In a new-style program with no main class or main method, the command-line arguments are accessible via a predefined variable args of type string[]. For example, here is a one-line program:

Console.WriteLine($"Hello, {args[0]}.");

Let's run it:

$ dotnet run Fred
Hello, Fred.

If a C# program has a main class, then the Main() method may take no arguments (as we have seen before), or may take an array of command-line arguments:

static void Main(string[] args) {
    ...

For example, here's a program that echoes each argument to the output:

class Top {
    static void Main(string[] args) { 
        foreach (string s in args)
            Console.WriteLine(s);
    }
}

Let's try 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.

file input/output

We may use the StreamReader and StreamWriter classes in the System.IO namespace to read from and write to files. You can create a StreamReader or StreamWriter using the new operator, passing a filename. For example:

StreamReader sr = new("myfile.txt");

new will create a new instance of any class. If you like, you may specify the class name as you call new:

StreamReader sr = new StreamReader("myfile.txt");

However if you are assigning to a variable, then new can automatically infer the class name from the variable's type, so you don't need to specify it explicitly.

reading all lines

We may commonly want to read all lines from standard input or from a file. The easiest way to do this is using a while loop and the 'is' operator. For example, from standard input:

while (Console.ReadLine() is string line) {
    ... do something with line ...
}

We'll see more uses of 'is' in a later lecture. For now, you should understand that in the code above, if ReadLine() returns a non-null string then it will be placed in the variable 'string line', and 'is' will evaluate to true, so the loop will continue. When there is no more input, ReadLine() will return null and then 'is' will evaluate to false, so the loop will exit.

Note that (unlike in Python) each line you read in this way will not include a newline character at the end.

The StreamReader class has a ReadLine() method, so you can use a similar loop to read all lines from a text file.

closing files with 'using'

When you are finished with a StreamReader or StreamWriter, you should close it. You can call the .Close() method for this purpose. It is especially important to close a StreamWriter - if you do not, some output may not be written!

C# includes a feature that makes it easy to close files (or other resources) automatically. When you declare any variable, you may precede it with the using keyword. Then the object in that variable will automatically be freed 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 sr = new StreamWriter(filename);

This feature may remind you of Python's with statement, which has a similar purpose.

A different form of this statement has a block of code attached:

using (StreamWriter sr = new StreamWriter(filename)) {
    ... use sr here ...
}

However I think it's usually easiest to use the first form.

example: wc

Using the ideas above, we can write the classic Unix utility wc, which counts the number of lines, words and characters in a file:

using static System.Console;

if (args.Length != 1) {
    WriteLine("usage: wc <file>");
    return;
}

int lines = 0, words = 0, chars = 0;

using StreamReader sr = new(args[0]);
while (sr.ReadLine() is string line) {
    lines += 1;
    words += line.Split().Length;
    chars += line.Length + 1;   // add 1 for newline character
}

WriteLine($"{lines} lines, {words} words, {chars} chars");

switch

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 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 may discuss these capabilities later in this course.

tuples

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

named tuples

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.

functions

A function can take one or more parameters, each with a specific type. It also has a return type. For example:

double abc(double d, int a) {
    return d / (2 * a);
}

// Compute the sum of all values in an array of ints.
int sum(int[] a) {
    int s = 0;
    foreach (int i in a)
        s += i;
    return s;
}

The return statement returns a value from a function, and may be called anywhere from within the function body (just like in Python). The first function above returns a double, and the second returns an int.

If a function's return type is void, then it does not return a value:

void countdown(int i) {
    while (i > 0) {
        Console.WriteLine(i);
        --i;
    }
}

Note that ReCodEx will not accept code with top-level functions, i.e. functions that are outside of any class. If you write code with top-level functions and want to submit it to ReCodEx, you will need to move the functions inside a class by making them be static methods of the class. (As we'll see in a future lecture, a static method is something like a function that belongs to a class.) So you could write e.g.

using System;

class Top {
    static void countdown(int i) {
        while (i > 0) {
            Console.WriteLine(i);
            --i;
        }
    }

    static void Main() {
        countdown(10);
    }
}

expression-bodied functions

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.

out parameters

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

ref parameters

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

classes

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(3.0, 4.0, 6.0);

WriteLine(p.is_zero());   // will write "false"

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

methods

In the Point class above, is_zero() is a method. We write methods using the same syntax that we previously saw for functions.

access levels

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.

constructors

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

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

example: stack class

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.