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.
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.
int
– 32 bits, signed (-2,147,483,648 to 2,147,483,647)
long
– 64 bits, signed (- 263 to 263 - 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
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.
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:
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 double pi = 3.14159; const int i = 1357; WriteLine($"{pi:f2} {i:n0}");
writes
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
];
Once again, in an array declaration with initial values 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];
Now you can get or set elements like this:
i[0][0] = 2; i[0][1] = 3; i[1][0] = i[0][0] + 1;
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
reference types: string
, arrays,
classes, interfaces
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, you can use an
explicit cast. This will fail with an exception at run time if the
value is null
.
int? a = xyz(); int b = (int) a;
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
These operators are defined on numbers and characters, but not on strings.
<
: 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 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 declaration declares one or more local variables and optionally gives them initial values:
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);
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.
int[] a = { 4, 5, 6 }; foreach (int i in a) Write(i);
foreach
iterates over each element of a
string or array. 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: case 3: WriteLine("two or three"); 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
or
return
statement.
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; }
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.
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);
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);
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.
A class is an abstract data type that can contain fields, constructors, methods, properties 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 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.
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; } }
A field, constructor, method, or property 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(); }
An interface may be generic, taking one or more type parameters. For example:
interface Map<K, V> { bool get(K key, out V val); void set(K key, V val); }
Types in the standard library are grouped
into namespaces.
For example, in the type System.Int32
,
System
is a namespace and Int32
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;
then you may refer the type System.Console
as just Console
, i.e. without any
namespace prefix.
You may also use using static
to
import all static methods from a class, so that you can call them
without specifying a class name. For example, if a source file
includes
using static System.Console;
then you can write just WriteLine()
or
ReadLine()
to call the static methods
Console.WriteLine()
and
Console.ReadLine()
.
using static System.Console; class Hello { static void Main() { WriteLine("hello, world"); } }