This is a summary of the subset of C# that we have learned so far in Programming II. For more details on any of the features below, see the Essential C# textbook or the C# reference pages.
types
- integers, float
,
double
,
bool
,
char
,
string
,
arrays, value and reference types, nullable types, subtypes
equality
(==
,
!=
),
relational (<
,
<=
,
>
,
>=
)
arithmetic
(+
,
-
,
*
,
/
,
%
)
assignment
(=
),
compound assignment (+=
,
-=
,
etc.)
boolean
(!
,
||
,
&&
)
conditional (?
:
)
increment/decrement
(++
,
–
)
null-coalescing
(??
),
null-conditional (?.
)
lifted operators
is
,
as
conversions – implicit, explicit
statements
– variable declarations,
if
,
while
,
do
/while
,
for
,
foreach
,
break
,
continue
,
return
,
switch
,
throw
,
try,
yield return
methods
– expression-bodied methods, overloading, ref
and
out
parameters,
parameter arrays
classes – access levels, fields, constructors, this, properties, indexers, overloaded operators, inheritance, static members
A single-line comment begins with //
and extends to
the end of the line:
x += 1; // increment x
Comments delimited with /*
and */
can
extend over multiple lines:
/* this is a comment with multiple lines */
Each short type name in C# abbreviates a longer name. For example,
int
is the same as System.Int32
. You can see the full
names in the Library Quick
Reference. You can use the shorter and longer names
interchangeably.
sbyte
– 8 bits, signed (-128 to 127)
byte
– 8 bits, unsigned (0 to 255)
short
– 16 bits, signed (-32,768 to 32,767)
ushort
– 16 bits, unsigned (0 to 65,536)
int
– 32 bits, signed (-2,147,483,648 to 2,147,483,647)
uint
– 32 bits, unsigned (0 to 4,294,967,295)
long
– 64 bits, signed (- 263 to 263 - 1)
ulong
– 64 bits, unsigned (0 to 264 – 1)
A literal integer (or floating-point value) may contain embedded underscores for readability:
int i = 1_000_000_000; // 1 billion
float
– 32 bits, 7 significant digits
double
– 64 bits, 15-16 significant digits
decimal
– 128 bits, 28-29 significant digits
The bool
type represents a Boolean value, namely either true
or false
.
A char
is a 16-bit Unicode character.
A character constant is enclosed in single quotes, e.g. 'x'
or 'ř'
.
A character constant containing a backslash is an escape
sequence.
Here are some common escape sequences:
\n
– newline
\r
– carriage return
\'
- single quote
\"
- double quote
\\
- backslash
To create a character constant representing a single quote or backslash, use one of the sequences above:
WriteLine('\\'); // writes a backslash WriteLine('\''); // writes a single quote
A string
is an immutable string of characters. A string constant is
enclosed in double quotes, e.g. "hello"
.
You may access individual characters of a string using square brackets:
string s = "spire"; char c = s[2]; // now c = 'i'
Note that characters are indexed starting from 0.
Strings may normally contain the escape sequences described above.
But you may prefix a string with @
to create a verbatim
string in which the backslash is an ordinary character. A
verbatim string may even contain newlines:
WriteLine(@"\one\ \two\ \three\" );
writes
\one\ \two\ \three\
A string beginning with $
may contain interpolated
values enclosed in braces:
int a = 3, b = 4; WriteLine($"{a} plus {b} equals {a + b}");
A interpolated value may be followed by a format specifier (preceded by a colon).
There are many predefined format specifiers. Many format specifiers can be followed by an integer called the precision, whose meaning varies among specifiers. Here are a few of the most useful:
d
– prints a value of integral type, padding
zeros to the left. The precision is the minimum number of digits to
print.
f
– prints a number in fixed-point decimal
(i.e. not using scientific notation). The precision is the number of
the digits after the decimal point.
n
– prints a number using thousands
separators. The precision is the number of digits after the decimal
point.
For example,
const int i = 1357; const double pi = 3.14159; WriteLine($"{i:d6} {pi:f2} {i:n0}");
writes
001357 3.14 1,357
You can allocate an array like this:
int[] i = new int[10];
Arrays are indexed from 0.
You can allocate and initialize an array at the same time:
i = new int[3] { 3, 4, 5 };
If you allocate and initialize an array as part of a variable
declaration, you can skip the new
operator:
int[] i = { 3, 4, 5 };
The Length
property returns the length of an array.
You can similarly allocate a multidimensional array using
the new
operator:
int
[,] i =
new
int
[
3
,
4
];
You can initialize a multidimensional array as you allocate it:
i = new int[2, 2] { {1, 4}, {2, 3} };
Once again, in a variable declaration you can skip the new
operator:
int[,] i = { {1, 4}, {2, 3} };
For a multidimensional array, the Length
property
returns the total number of elements in the array. This is the
product of the lengths of all dimensions. Also see Rank
and GetLength
in the Library
Quick Reference.
A jagged array is an array of arrays. This is not the same thing as a multidimensional array. A multidimensional array has a rectangular shape, whereas each array in a jagged array can have a different length.
You can allocate a jagged array like this:
int[][] i = new int[3][]; i[0] = new int[2]; i[1] = new int[4]; i[2] = new int[5];
If you like, you can initialize a jagged array as you allocate it:
int[][] i = { new int[] { 2, 3}, new int[] { 3, 4, 5}, new int[] { 1, 3, 5, 7, 9} };
An enum type holds one of a fixed set of constant values:
enum Suit { Club, Diamond, Heart, Spade }
To refer to one of these values, prefix it with the type name:
Suit s = Suit.Diamond;
Internally, an enumerated value is stored as an integer. Each constant in an enum is assigned an integer value, starting from 0. For example, in the enumeration above, Diamond is assigned the value 1.
Explicit conversions exist between each enum type and int
in both directions:
Suit s = Suit.Diamond; int i = (int) s; // convert a Suit to an int Suit t = (Suit) i; // convert an int to a Suit
Every type in C# is either a value type or a reference type.
value types: all numeric types, bool
, char
,
enum types, structs
reference types: string
, arrays, classes,
interfaces, delegates
When a variable's type is a value type, the variable holds a value. An assignment between variables of value type copies the value from one variable to another.
When a variable's type is a reference type, the variable holds a pointer to an object. An assignment between variables of reference type makes them point to the same object.
For example:
int[] a = { 4, 5, 6 }; int[] b = a; // now b and a point to the same array a[1] = 7; WriteLine(b[1]); // writes 7
Any variable of reference type may hold the special value null
.
Note especially that null
is not the same as the
empty string.
Value types such as int
and bool
cannot
hold null
. However, each such type can be made nullable
by appending a ? character to its name. A nullable type holds either
an instance of its base type, or the value null
. For
example:
int? x = 7; if (x > 3) x = null;
A value of the base type can be converted to a nullable type implicitly:
int y = abc(); int? z = y;
To convert from a nullable type to its base type, use an explicit
cast or access the Value
property. These are equivalent,
and will fail with an exception at run time if the value is null
.
int? a = xyz(); int b = (int) a; int c = a.Value; // equivalent
In some situations a class may be a subtype of another type:
when a class implements an interface
when an interface inherits from an interface
when a class inherits from a class
For example:
interface Collection { … } interface Stack : Collection { … } class SimpleStack : Stack { … } class HyperStack : SimpleStack { … }
Here, HyperStack
is a subtype of SimpleStack
,
which is a subtype of Stack
, which is a subtype of
Collection
.
A type may be implicitly converted to any supertype:
Collection c = new HyperStack(); // implicit conversion from HyperStack to Collection
A type may be explicitly converted to any subtype:
HyperStack h = (HyperStack) c; // explicit conversion from Collection to HyperStack
This explicit conversion will fail at runtime if the object in question does not actually belong to the subtype.
==
: equals
!=
: does not equal
Note that when comparing values of reference type, by default these operators use reference equality: they return true only if two objects are actually the same object, and do not consider objects' fields. For example:
class Foo { int i; public Foo(int i) { this.i = i; } } WriteLine(new Foo(3) == new Foo(3)); // writes False
(A particular class may overload these operators to test equality in some different way.)
<
: less than
<=
: less than or equal to
>
: greater than
>=
: greater than or equal to
+
: addition
The +
operator can also be used to concatenate
strings:
WriteLine("good" + " " + "bread");
It can also concatenate strings and other kinds of values:
int i = 4; WriteLine("i = " + i);
-
: subtraction
*
: multiplication
/
: division
The / operator performs integer division if both its arguments have integer types:
WriteLine(7 / 3); // will write 2
Otherwise, floating-point division is performed:
WriteLine(7 / 3.0); // will write 2.3333
%
: remainder
=
: assignment
x = 4; // assign 4 to x
An assignment is actually an expression in C#:
y = (x = 4) + 2; // assign 4 to x and 6 to y
+=
: addition
-=
: subtraction
*=
: multiplication
/=
: division
%=
: remainder
!
: not
||
: or
&&
: and
The conditional operator (? :
) is similar to
the if
statement, but it is an expression, not a
statement. It takes a Boolean value and 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);
The increment operator ++
increases a variable
or field by 1. It comes in two forms. With the pre-increment
operator, the expression's value is the value of the variable
after it is incremented:
int i = 4; int j = ++i; // now i = 5 and j = 5
With the post-increment operator, the expression's value is the value of the variable before it is incremented:
int i = 4; int j = i++; // now i = 5 and j = 4
The decrement operator --
also has pre-decrement
and post-decrement forms, and works similarly.
The null-coalescing operator ?? operates on a value of any nullable type, including reference types. If the value is null, the operator returns its second operand; otherwise the value itself is returned. For example:
int? x = abc(); int y = x ?? 0; int z = (x == null) ? 0 : x.Value; // equivalent
The null-conditional operator ?. operates on a value of any nullable type, including reference types. It invokes a method if the value is non-null. If the value is null, the operator returns null. For example:
string s = abc(); string t = s?.Trim();
It is often convenient to chain method calls using this operator:
string t = s?.Trim()?.ToLower();
Many operators have lifted versions which operate on nullable types.
The unary operators +
, ++
, -
,
--
and !
can act on nullable types. If
their operand is null, they do nothing and return null.
The binary operators +
, -
, *
,
/
and &
can act on nullable types. If
either or both of their operands is null, they return null.
The relational operators <
, <=
,
>
and >=
can act on nullable types.
If either or both of their operands is null, they return false.
The is
operator returns true if a value belongs to a
type. It works with both nullable types and reference types:
int? i = abc(); if (i is int) // true if i != null WriteLine(i.Value); Stack s = getStack(); if (s is LinkedStack) WriteLine("linked");
The is operator can optionally bind a variable. The first example above can be rewritten as
if (abc() is int i) WriteLine(i);
Here is a loop using is
:
while (ReadLine() is string s) WriteLine(s);
In this loop, when ReadLine()
returns a non-null value,
the string variable s receives that value and the loop continues.
When ReadLine()
returns null, the loop terminates.
The as
operator checks whether a value belongs to a
type. If so, it returns the value; otherwise it returns null:
Stack s = getStack(); LinkedStack ls = s as LinkedStack; // if s was not a LinkedStack, ls will be null
The default
operator returns the default value for a
type:
WriteLine(default(int)); // writes 0
default
is most useful
inside a generic class, where it can act on a type parameter. For
example, a dynamic array class could have this code:
class DynArray<T> { T[] a; int count; … public T this[int index] { // return default value if out of bounds get => index < count ? a[index] : default(T); set => … } }
C# will perform an implicit conversion between two numeric types if every possible value of the source type is valid in the destination type. For example:
short s; ... int i = s; // implicit conversion
You can also implicitly convert a char
to any numeric
type that can hold a 16-bit unsigned value:
char c = 'ř'; int i = c; // now i holds the Unicode value for 'ř', i.e. 345
You can use an explicit conversion to force a conversion between any two numeric types. This is accomplished with a type cast:
int i = 1000; short s = (short) i;
If the destination type cannot hold the source value, it will wrap around or be truncated.
You can explicitly convert any numeric type to a char
:
int i = 100; char c = (char) i; // now c is 'd', which is ASCII/Unicode character 100
A local variable may optionally have an initial value:
int i = 3, j = 4;
When you declare a local variable, you can use the var
keyword to tell the compiler to infer its type. For example:
var list = new List<int>(5);
This is equivalent to
List<int> list = new List<int>(5);
A local constant is like a local variable, but is fixed at compile time:
const int Million = 1_000_000;
if (i > 0) WriteLine("positive"); else if (i == 0) WriteLine("zero"); else WriteLine("negative");
An if
statement executes a statement (or block) if the
given value is true. If the statement has an else
clause, it is executed if the given value is false.
while (i < 10) { sum = sum + i; i += 1; }
A while
loop loops as long as the given condition is
true.
do { s = ReadLine(); } while (s != "yes" && s != "no");
A do
/while
loop is like a while
loop, but checks the loop condition at the bottom of the loop body.
for (int i = 0, j = 0 ; i < 10 ; i += 1, j += 2) WriteLine(i + j);
A for
statement contains three clauses, separated by
semicolons, plus a loop body.
The initializer (e.g. int
i =
0
,
j =
0
)
executes once at the beginning of the loop. It contains either a
variable declaration, or one or more statements separated by commas.
The condition (e.g.
i
<
10
)
is evaluated before every loop iteration. If it is false, the loop
terminates.
The iterator (e.g. i +=
1
,
j +=
2
)
executes at the end of every loop iteration. It contains one or more
statements, separated by commas.
foreach (char c in "hello") Write(c + " ");
foreach
iterates over each element of an object that
implements IEnumerable<T>
. (See the class library
reference for details about this interface.) Such objects include
arrays
strings
all built-in collection classes, e.g. List
or
Dictionary
A foreach
statement always declares an iteration
variable whose scope is the body of the statement.
for (int i = 1 ; i <= 10 ; ++i) { if (n % i == 0) break; WriteLine(i); }
A break
statement breaks out of the nearest enclosing
while
, do
/while
, for
or foreach
loop.
while (i < 100) { if (n % i == 0) continue; WriteLine(i); n += 1; }
A continue statement continues with the next iteration of the nearest
enclosing while
, do
/while
, for
or foreach
loop.
static int add(int x, int y) { return x + y; }
The return
statement returns a value immediately from
the enclosing method.
switch (i) { case 1: WriteLine("one"); break; case 2: WriteLine("two"); break; default: WriteLine("other"); break; }
The switch
statement is a more compact (and possibly
more efficient) alternative to a series of if statements. The default
section is optional. Each case in a switch
statement
must end with a break
statement.
again: WriteLine(i); i += 1; if (i < 10) goto again;
The goto
statement jumps to another point in a block of
code. You should rarely if ever use it.
The throw
statement throws an exception,
which can be any object belonging to the System.Exception class or
any of its subclasses. The exception will pass up the call stack,
aborting the execution of any methods in progress until it is caught
with a try...catch
block at some point higher on the
call stack. If the exception is not caught, the program will
terminate.
The try
statement attempts to execute a block of
code. It may have a set of catch
clauses and/or a
finally
clause.
A catch
clause catches all exceptions of a certain
type. For example:
static void Main() { StreamReader reader; try { reader = new StreamReader("numbers"); } catch (FileNotFoundException e) { WriteLine("can't find input file: " + e.FileName); return; } …
The code above will catch an exception of class
FileNotFoundException
,
or of any subclass of it. When an exception is caught, the
catch
block (called an exception handler)
executes. The catch block may itself rethrow the given exception, or
even a different exception. If the catch block does not throw an
exception, execution resumes below the try
statement.
A finally
clause will always execute, even if an
exception is thrown inside the body of the try
statement. For example:
StreamReader reader = …; StreamWriter writer = …; try { while (reader.ReadLine() is string s) writer.WriteLine(transform(s)); } finally { reader.Close(); writer.Close(); }
In this close, reader
and writer
will be
closed even if an exception occurs within the try
body
(for example, within the transform
method). Note that a
finally
clause does not itself catch an exception, which
will continue to pass up the call stack.
The preceding example is equivalent to
StreamReader reader = …; StreamWriter writer = …; try { while (reader.ReadLine() is string s) writer.WriteLine(transform(s)); } catch (Exception e) { reader.Close(); writer.Close(); throw e; }
An iterator is a special kind of method that generates a
sequence of values. Each time that the caller requests the next value
in the sequence, an iterator's code runs until it calls the yield
return
statement, which yields the next value in the sequence.
At that point the iterator is suspended until the caller requests the
next value, at which point the code continues executing until the
next yield return
statement, and so on. When execution
reaches the end of the iterator method body, the sequence is
complete.
An iterator must have return type IEnumerable<T>
(or IEnumerator<T>
) for some (concrete or generic)
type T. Here is a simple iterator:
static IEnumerable<int> range(int start, int end) { for (int i = start ; i <= end ; ++i) yield return i; }
Now, for example, we can add the squares of the numbers from 1 to 10 like this:
using System.Linq; int sum = range(1, 10).Select(i => i * i).Sum();
A method is like a function, but belongs to a class. In a method declaration, the return type precedes the method name:
static int mul(int x, int y) { return x * y; }
To call a method, specify values for each of its parameters:
int i = mul(16, 36); // now i = 576
If a method’s body is a simple expression, the method can be defined using a compact syntax:
static int mul(int x, int y) => x * y;
You may declare multiple methods that have the same name but have different numbers and/or types of parameters. This is called method overloading. For example:
static int more(int i) => i + 1 static int more(short s) => s + 2 static string more(string s) => s + " "
When calling an overloaded method, sometimes there is more than one candidate method. For example, consider this method call:
byte b = 77; WriteLine(more(b));
Both of the first two method declarations above are candidates for
this call, since byte
is implicitly convertible to both
int
and short
. In this situation, C# will
favor the overload that involves converting to a more specific
type. short
is more specific than int
, so
in the example above C# will call the method
int more(short s)
and will write the value 79.
For any types T and U, C# considers T to be more specific than U if there is an implicit conversion from T to U, but none from to U.
A parameter
marked with ref
is passed by reference: the method can modify the variable that is
passed. Here's a method that swaps two variables:
static 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);
A parameter marked with out
returns a value to the caller. This method takes two integers and
returns both their sum and product:
static void sumAndProduct(int a, int b, out int sum, out int product) { sum = a + b; product = a * b; }
You must include the keyword out
when passing a variable
to an out
parameter:
int s, p; sumAndProduct(3, 4, out s, out p);
You can declare a new variable as you invoke a method with an out
parameter. The following is equivalent to the two preceding lines:
sumAndProduct(3, 4, out int s, out int p);
You can make a method 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 method, 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 method parameters are optional, you may specify any subset
of them you like using named arguments. For example, we may invoke
the fill
method from the previous section as
fill(a, val: 4) // same as fill(a, 0, 4)
If the last parameter to a method is marked with params
and has type T[], then in its place the method 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 method receives a parameter array of integers and returns their sum:
static 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);
Methods in C# may be nested. A nested method appears inside the body of another method. For example:
static void Main1(string[] args) { WriteLine("hello"); double arg(int n) => double.Parse(args[n]); double d = arg(0); double e = arg(1); WriteLine(d + e); }
A nested method may access parameters and local variables in its
containing method. For example, the nested method arg
above accesses the args
parameter.
A method may be generic: it may take one or more type parameters. For example:
public static void swap<T>(ref T a, ref T b) { T t = a; a = b; b = t; }
A method's type parameters may have constraints. For example:
public static void copy<T, U>(T[] a, U[] b) where T : U { for (int i = 0 ; i < a.Length ; ++i) b[i] = a[i]; }
For details about the types of constraints that are allowed, see the section "Generic class constraints", below.
You can add extension methods to an existing class. An extension method can be used syntactically as if belonged to a class, even though it is written outside the class.
For example, suppose that we are using a C# library that provides this type:
class Vector { public double dx, dy; public Vector(double dx, double dy) { this.dx = dx; this.dy = dy; } }
We wish that the author of the class had provided a length
method that calculates the length
of a Vector. Since we cannot modify the class, we can write an
extension method:
static class Util { public static double length(this Vector v) => Sqrt(v.dx * v.dx + v.dy * v.dy); }
Now we can call the method as if it had been defined inside the
Vector
class itself:
Vector v = new Vector(3.0, 4.0); WriteLine(v.length()); // writes 5.0
Note that an extension method must
be static
and must be contained in a static
class.
A class is an abstract data type that can contain fields, constructors, methods, properties, indexers, overloaded operators and other kinds of members.
Here is a definition for a simple class representing a point in two dimensions. It has two fields, one constructor and two methods:
using static System.Math; class Point { double x, y; public Point(double x, double y) { this.x = x; this.y = y; } public void move(double dx, double dy) { x += dx; y += dy; } public double distanceFromOrigin() { return Sqrt(x * x + y * y); } }
Every member in a class can be either
public,
protected
or private
.
public
members are accessible everywhere
protected
members are accessible only within the class and to subclasses
private
members are accessible within the class.
The default access level is private
.
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
).
Code in any method, constructor or other member may refer to fields by name and may modify their values.
A field marked as readonly
can be modified only in a
constructor or field initializer. This attribute can be used to make
a class immutable:
struct Point { public readonly double x, y; public Point(double x, double y) { this.x = x; this.y = y; } }
A constructor makes a new instance of a class. It always has the same name as its containing class.
To call a constructor, use the new
operator and pass any required arguments:
Point p = new Point(3.0, 4.0);
If a class definition includes no constructors, then C# provides a default constructor that takes no arguments.
Constructors may be overloaded. A constructor may call another constructor; this is called constructor chaining. For example, we could add a second constructor to the Point class that chains to the constructor with two parameters:
public Point(double x) : this(x, 0.0) { }
An instance method is invoked on an instance of a class. Any additional arguments appear between parentheses:
Point p = new Point(3.0, 4.0); // a constructor call p.move(1.0, 2.0); // a method call
For more information about methods, see the Methods section above.
this
is a special value that refers to the object on
which a method, constructor or other member was invoked.
A property is syntactically like a field, but contains a
getter and/or a setter, which are methods that run when
the caller retrieves or updates the property's value. Inside the
setter, the keyword value
refers to the value that is
being set.
Here is a partial listing of a class Vector
that
includes a property length
:
class Vector { double[] v; ... public int length { get { return v.Length; } set { v = new double[value]; } } }
You can use expression syntax to define getters or setters. The
length
property above could be written as
public int length { get => v.Length; set => v = new double[value]; }
An indexer allows you to define custom getter and/or setter
methods that run when an instance of your class is accessed using the
array-like syntax a[i]
. For example, we can extend the
Vector class above with an indexer that retrieves and sets elements
of the underlying array v:
public double this[int i] { get { return v[i]; } set { v[i] = value; } }
A caller can now invoke this indexer as if v itself were an array:
Vector v = new Vector(...); v[3] = 77.2; v[4] = v[5] + 1.0;
The indexer defined above has return type double
and
uses an index parameter of type int.
In general, an
indexer may have any return type and any index parameter type.
You may defined overloaded operators for a class, which
redefine the meaning of built-in operators such as +
and
*
when invoked on instances of the class. For example:
class Foo { int i; public Foo(int i) { this.i = i; } public static Foo operator + (Foo a, Foo b) => new Foo(a.i + b.i); }
The operator above can be invoked like this:
Foo f = new Foo(3); Foo g = new Foo(4); Foo h = f + g;
An overloaded operator must be public
and static
.
You may overload most of the built-in operators available in C#, including
unary operators: +
, -
, !
,
++
, –
binary operators: +
, -
, *
,
/
, %
, <
, >
,
<=
, >=
A class may inherit from another class. For example:
class LinkedStack : Stack { Node head; public virtual void push(int i) { ... } public virtual int pop() { ... } public bool isEmpty { ... } } class LimitStack : LinkedStack { protected int count; protected int limit; public LimitStack(int limit) { this.limit = limit; } public override void push(int i) { if (count == limit) { WriteLine("stack is full"); return; } ++count; base.push(i); } public override int pop() { --count; return base.pop(); } } class TwoLimitStack : LimitStack { int threshold; public TwoLimitStack(int threshold, int limit) : base(limit) { this.threshold = threshold; } public override void push(int i) { if (count == limit - threshold) WriteLine("warning: stack is getting full"); base.push(i); } }
A child class constructor can invoke a base class constructor using
the base
keyword. In the example above, the
TwoLimitStack
constructor calls the LimitStack
constructor in this way.
An child class may override methods, properties or indexers
in its base class. Only a member marked as virtual
can
be overridden. Any overriding member must include the override
keyword.
A method in a child class can invoke a base class method using the
base
keyword as in the example above.
Note that an object's behavior is determined entirely by its actual class, not by the type of any containing variable. For example:
LinkedStack s = new LimitStack(5); s.push(4); s.push(6);
In this example, the calls to the push
method invoke the
implementation in LimitStack
, even though the containing
variable has type LinkedStack
.
Note that C# supports only single class inheritance: a class may inherit from only one other class.
If a class is marked as sealed
, no other class may
derive from it. If an overriding method is marked as sealed
,
no subclass may override it further.
A class marked abstract
cannot be instantiated using
the new
operator; only subclasses of it can be
instantiated. Abstact classes may have abstract methods,
which have a method signature but no body. Any concrete
(non-abstract) subclasses of an abstract class must provide an
implementation for every abstract method. Any abstract method is
implicitly virtual.
Here is an example of an abstract class that maintains a count of
objects in a stack. Any concrete subclass (for example, a class
implementing a stack using a linked list) must provide
implementations of the push2
and pop2
methods.
abstract class CountingStack { int _count; protected abstract void push2(int i); protected abstract int pop2(); public void push(int i) { ++_count; push2(i); } public int pop() { --_count; return pop2(); } public bool isEmpty { get => (_count == 0); } public int count { get => _count; } }
A class may be generic, taking one or more type parameters:
class Pair<T> { public T first, second; public Pair(T first, T second) { this.first = first; this.second = second; } }
A class's type parameters may include constraints. For example:
class Maximizer<T> where T : IComparable<T> { T _max; bool empty; public void add(T t) { if (empty || t.CompareTo(_max) > 0) _max = t; empty = false; } public T max { get => _max; } }
Each type constraint can have one of the following forms:
T :
type – T must be a subtype of the
given type
T : struct
– T must be a value type
T : class
– T must be a reference type
A field, constructor, method, property or indexer may be static. Static members are shared by all instances of a class.
class Foo { static int x = 1; static int y; static Foo() { y = x + 1; } static void inc() { x += 1; } }
A static constructor always takes no arguments. It cannot be invoked explicitly, and runs automatically when a class is accessed for the first time.
A static method is invoked on a class, not on an instance:
Foo.inc();
Static methods may access private members of
any instance of their class. For example, we can add a static method
to the Point
class that compares two Point
objects and returns true if they are equal:
public static bool eq(Point p, Point q) => p.x == q.x && p.y == q.y;
You may mark a class itself as static to indicate that it may only have static members; there can be no instances of such a class. For example, we could declare the class Foo above as
static class Foo { ...
An interface defines a set of methods, properties and/or indexers that can be implemented by a class. For example:
interface Stack { bool isEmpty { get; } void push(int i); int pop(); }
A class may implement one or more interfaces:
class LinkedStack : Stack { Node head; public bool isEmpty { get => (head == null); } public void push(int i) { head = new Node(i, head); } public int pop() { ... } }
An interface may inherit from one or more other interfaces.
For example, we can refactor the Stack
interface above
as follows:
interface Collection { bool isEmpty { get; } } interface Stack : Collection { void push(int i); int pop(); } interface Queue : Collection { void enqueue(int i); int dequeue(); }
Usually a class implements interface methods (and other members)
implicitly, as in the LinkedStack example above. An implicit
member implementation includes the keyword public
and is
visible through variables of either the class type or the interface
type:
LinkedStack ls = new LinkedStack(); ls.push(3); Stack s = ls; s.push(4);
Alternatively, a class may implement a member explicitly. An
explicit member implementation is prefixed with the interface name
and may not be public
:
class AlterStack : Stack { void Stack.push(int i) { … } … }
An explicit member implementation is visbile only through a variable of the interface type, not the class type:
AlterStack a = new AlterStack(); a.push(3); // invalid - compile error Stack b = new AlterStack(); b.push(3); // works fine
Explicit member implementations are useful when a class implements two interfaces and a member with the same name occurs in both interfaces, i.e. when there is a name conflict. In this situation, a class can provide a separate explicit implementation for each interface. But we will not often (if ever) encounter this situation in this course.
An interface may be generic, taking one or more type parameters. For example:
interface Map<K, V> { V this[K key] { get; set; } }
Suppose that C is a generic class or interface that has a single type parameter, and that U is a subtype of T. By default, C's type parameter is invariant: the types C<T> and C<U> are not convertible to each other.
Classes' type parameters in C# are
always invariant. An interface's type parameters, however, may be
marked as covariant using
the out
keyword. If I is a generic
interface with a covariant type parameter and U is a subtype of T,
then I<U> is convertible to I<T>.
If an interface's type parameter T is covariant, then the interface's methods and properties may not receive any values of type T as parameters.
For example, suppose that we've written a generic class DynArray
representing a dynamic array:
class DynArray<T> { T[] a = new T[1]; int count; public int length { get { … } set { … } } public void add(T t) { … } public T this[int index] { get { … } set { … } } … }
And suppose that we have an abstract class Shape
with
subclasses Rectangle
, Circle
and Triangle
:
abstract class Shape { public abstract double area { get; } } class Rectangle : Shape { public double width, height; public Rectangle(double width, double height) { … } public override double area { get => width * height; } } class Circle : Shape { public double radius; public Circle(double radius) { this.radius = radius; } … } class Triangle : Shape { … }
Finally, suppose that we have a method areaSum
that adds
the area of all Shapes in a dynamic array:
public static double areaSum(DynArray<Shape> a) { double sum = 0; for (int i = 0 ; i < a.length ; ++i) sum += a[i].area; return sum; }
The following code will not compile:
DynArray<Rectangle> r = new DynArray<Rectangle>(); r.add(new Rectangle(10, 2)); r.add(new Rectangle(20, 4)); double a = areaSum(r); // ???
That's because the
type DynArray<Rectangle>
is not convertible to DynArray<Shape>
.
Such a conversion would be unsafe, since it would allow the
following:
public static void addCircle(DynArray<Shape> a) { a.add(new Circle(5.0)); } DynArray<Rectangle> r = new DynArray<Rectangle>(); addCircle(r); // ???
Here is an interface with an covariant type parameter:
interface ReadOnlyArray<out T> { int length { get; } T this[int index] { get; } }
Suppose that the DynArray
class above implements this
interface:
class DynArray<T> : ReadOnlyArray<T> { … }
And suppose that we modify areaSum
to take a
ReadOnlyArray<Shape>
as its argument:
public static double areaSum(ReadOnlyArray
<Shape> a) {
double sum = 0;
for (int i = 0 ; i < a.length ; ++i)
sum += a[i].area;
return sum;
}
Now we may pass a DynArray<Rectangle>
to areaSum
:
DynArray<Rectangle> r = new DynArray<Rectangle>(); r.add(new Rectangle(10, 2)); r.add(new Rectangle(20, 4)); double a = areaSum(r); // will now compile
You will get a compile-time error if you attempt to mark the following interface's type parameter T as covariant, since the interface's methods and properties receive values of type T:
interface Arr<T> { int length { get; set; } void add (T t); T this[int index] { get; set; } }
Contravariance is a complement to covariance. You can mark a
type parameter as contravariant using the in
keyword. If
I is a generic interface with a contravariant type parameter and U is
a subtype of T, then I<T> is convertible to I<U>.
Surprisingly, arrays in C# are covariant. For example, the following code will compile:
Rectangle[] a = new Rectangle[5]; Shape[] b = a; // Rectangle[] is convertible to Shape[] b[0] = new Circle(4.0);
But the last statement above will fail at run time since b is
actually a Rectangle
[] and a Circle
cannot
be added to a Rectangle
[].
I (and many other people) believe that this array covariance is a design flaw in the C# language. It has the following negative consequences:
Every assignment to an array of a reference type (such as a Shape[] above) must check at run time that the value being assigned is compatible with the destination array. This may have a significant performance cost.
Code that assigns an incompatible element to an array may fail at run time rather than at compile time. In other words, array covariance is not type safe: it allows code to compile that may yield a run-time type error.
Structs are similar to classes, but they are value types, not reference types. Here is a simple struct:
struct Point { public double x, y; public Point(double x, double y) { this.x = x; this.y = y; } public static double distance(Point p, Point q) => Sqrt((p.x - q.x) * 2 + (p.y - q.y) * 2); }
Structs have some limitations:
Fields in a struct may not have initializers.
A struct may not have a constructor with no arguments.
It is not possible to inherit from a struct.
It is sometimes more efficient to use structs than classes, since a variable of struct type does not contain an implicit pointer. On the other hand, each time a struct is passed as a method argument, it is copied, which is more expensive than passing a class reference by value. So in some situations structs are less efficient.
I generally recommend that you use structs only for data types that contain just a few values such as integers or doubles. For example, types that represent rational numbers or complex numbers would be good candidates for structs.
A delegate holds a reference to a method. It is similar to a function pointer in languages such as Pascal or C. (A delegate is also sometimes called a closure in functional programming.)
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 hold a reference to a method:
static bool isOdd(int i) => i % 2 == 1; static void Main() { IntCondition c = isOdd; …
We can invoke the delegate using method 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) => 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:
static int count1<T>(T[] a, Condition<T> cond) { int n = 0; foreach (T val in a) if (cond(val)) ++n; return n; }
A lambda expression is an anonymous function that can appear inside another expression.
For example, here is a delegate type for a function from integers to integers:
delegate int IntFun(int i);
And here is a method that applies a given function to every element of an array of integers:
static int[] map(int[] a, IntFun f) { int[] b = new int[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
:
static int plus2(int i) => i + 2; static int[] add2(int[] a) => map(a, plus2);
Alternatively, we can invoke map
using a lambda
expression:
static int[] add2(int[] a) => map(a, i => i + 2);
Here, i => i + 2
is a lambda expression. It is an
anonymous function that takes an integer parameter i and returns the
value i + 2.
Like a nested method, a lambda expression may refer to parameters
or local variables in its containing method. For example, suppose
that we want to write a method that adds a given value k to each
element in an array. We could write a nested method and pass it to
map
:
static int[] add_k(int[] a, int k) { int f(int i) => i + k; return map(a, f); }
Or we can use a lambda expression that adds k directly:
static int[] add_k(int[] a, int k) => map(a, i => i + k);
The lambda expressions in the examples above are expression
lambdas, writen using a compact syntax similar to the expression
syntax for methods. Alternatively, a lambda expression can be written
as a statement lambda, which
can include one or more statements and can use the return
statement to return a value. For
example, we can rewrite the last example above like this:
static int[] add_k(int[] a, int k) => map(a, i => { return i + k; } );
In the examples above we've seen that a method can take a delegate (i.e. a function) as an argument. A method can also return a delegate constructed using a lambda expression.
Here is a simple example:
delegate bool IntCondition(int i); static IntCondition divisibleBy(int k) => i => (i % k == 0);
Now we can invoke this method and use the delegate that it returns:
IntCondition div3 = divisibleBy(3); WriteLine(div3(6)); // writes 'True' WriteLine(div3(7)); // writes 'False'
In this example, note that the delegate returned by divisibleBy
can refer to the parameter k even after the method divisibleBy
has returned! To put it differently, the lambda expression i
=> (i % k ==
0
)
has captured the parameter k. Local variables may also be
captured by a lambda expression.
Using lambda expressions we can write functions that transform other functions. This is a powerful technique that you may explore further in more advanced courses about functional programming. Here are just a couple of examples of this nature. First, here is a function that composes two functions f and g, returning the function (f ∘ g), which is defined as (f ∘ g)(x) = f(g(x)):
static IntFun compose(IntFun f, IntFun g) => i => f(g(i));
We can call compose
as follows:
IntFun f = compose(i => i * 2, i => i + 1); // now f(x) = 2 * (x + 1) WriteLine(f(4)); // writes 10
Second, here's a function that computes the nth power of a function f, defined as
fn(x) = f(f(...(f(x))) [ f appears n times in the preceding expression ]
static IntFun power(IntFun f, int n) => i => { for (int j = 0 ; j < n ; ++j) i = f(i); return i; };
Addition to a power is multiplication:
IntFun f = power(i => i + 10, 4); WriteLine(f(2)); // writes 42
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 is 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; changed(i, prev, a[i]); } } }
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 foo() { 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
Types in the class library are grouped into namespaces. For example:
In the type System.Int32
,
System
is a namespace and Int32
is a type name.
In the type System.IO.StreamReader
,
System.IO
is a namespace and StreamReader
is a type name.
The using
statement must appear at the top of a source file. It imports types
from a namespace, allowing you to use them without specifying a
namespace every time. For example, if a source file includes
using System.IO;
then it may refer to StreamReader
without any namespace
prefix.
using static System.Console; class Hello { static void Main() { WriteLine("hello, world"); } }