This week's topics are covered in Essential C#: see ch. 3 More with Data Types (Nullable Modifier), ch. 5 Methods and Parameters (Advanced Method Parameters, Optional Parameters), ch. 6 Classes (Properties), ch. 9 Value Types (Enums), ch. 10 Well-Formed Types (Operator Overloading), ch. 17 Building Custom Collections (Providing an Indexer).
Here is a summary of the C# elements we discussed:
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;
If you'd like to be able to access an enum's values without the type
name prefix, include a using static
declaration at the
top of your source file:
using static Suit;
Then you will be able to write, for example:
Suit s = 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
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
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)
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 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[] a; ... public int length { get { return a.Length; } set { a = 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 => a.Length; set => a = 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 a[i]; } set { a[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 Vector { double[] a; public Vector(params double[] a) { this.a = a; } public static Vector operator + (Vector v, Vector w) { double[] b = new double[v.a.Length]; for (int i = 0 ; i < v.a.Length ; ++i) b[i] = v.a[i] + w.a[i]; return new Vector(b); } }
The operator above can be invoked like this:
Vector v = new Vector(2.0, 5.0, 10.0); Vector w = new Vector(1.0, 3,0, 9.9); Vector x = v + w;
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: +
,
-
,
*
,
/
,
%
,
<
,
>
,
<=
,
>=