Here are notes about the topics we covered in lecture 4. For more details, see the Essential C# textbook or the C# reference pages.
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.
Note that when comparing values of reference type, by default the
==
operator uses reference equality: it returns
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.)
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
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();
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
A static constructor always takes no arguments. It cannot be invoked explicitly, and runs automatically when a class is accessed for the first time. For example:
class Foo { static int x = 1; static int y; static Foo() { // a static constructor y = x + 1; } static void inc() { x += 1; } }
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.
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: +
, -
, *
,
/
, %
, <
, >
,
<=
, >=