In many programs we may wish to access command-line arguments.
In a new-style
program with no main class or M
ain
()
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 line
and the is
operator will evaluate to
true, so the loop will continue. When there is no more input,
ReadLine()
will return null
and the is
operator 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 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); } }
We have seen basic syntax for defining functions in C#:
int mul(int x, int y) { return x * y; }
The function above 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) { 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); WriteLine(i); // writes 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 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.
It can be awkward to write named tuple types repeatedly in code:
(int x, int y) p = (10, 20); int z = p.x + p.y; (int x, int y) q = p;
As an alternative to this, you can create a type alias via a
using
statement at the top of your
source file:
using Point = (int x, int y);
With this alias, we can write the code above more concisely:
Point p = (10, 20); int z = p.x + p.y; Point q = p;
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.