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.
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.
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.
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.
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");
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.
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.
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); } }
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.
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(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
).
In the Point class above, is_zero() is a method. We write methods using the same syntax that we previously saw for functions.
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.)
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.