As we saw in an earlier lecture, when you create an array you can specify a set of initial values:
int[] a = {10, 20, 30, 40};
You can also initialize rectangular and jagged arrays with values:
int[,] b = {{1, 4, 5}, {2, 3, 6}}; // rectangular int[][] c = { // jagged new int[] {2, 3}, new int[] {3, 4, 5} };
It's also possible to initalize a List with values, or even a List of List objects (which is like a jagged array):
List<int> d = new() {10, 20, 30, 40}; List<List<int>> e = new() { new() {2, 3}, new() {3, 4, 5}, };
Notice that the syntax for arrays and lists is not quite the same.
C# 12 includes a new feature called collection expressions that allows you to initialze arrays and lists using a nicer, more uniform syntax. Using collection expressions, we may rewrite the above examples like this:
int[] a = [10, 20, 30, 40]; int[][] c = [[2, 3], [3, 4, 5]]; List<int> d = [ 10, 20, 30, 40 ]; List<List<int>> e = [[2, 3], [3, 4, 5]];
This is convenient. However ReCodEx only has C# 11, so you cannot use this syntax in any ReCodEx program at this time.
Also note that you cannot use a collection expression to initialize a rectangular array.
Every type in C# is either a value type or a reference type.
value types: all numeric types, bool
,
char
, tuple types
reference types: string
, arrays,
classes
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 reference to an object. An assignment between variables of reference type makes them refer to the same object.
For example:
int[] a = { 4, 5, 6 }; int[] b = a; // now b and a refer to the same array a[1] = 7; WriteLine(b[1]); // writes 7
As we have noted before, every type in C# has a certain default value. For example, the default value of int is 0. When you create an array without providing initial values, every element will be initialized to the default value of the element type. So in the declaration
int[] a = new int[100];
every array element will initially be zero.
In C# the default value of every reference type is null. And so if you create an array of any reference type, all its elements will initially be null. That is true even if the type is not nullable! For example:
string[] a = new string[10]; // array of non-nullable strings string?[] b = new string[10]; // array of nullable strings
The elements in both of these arrays will initially be null. However
you cannot assign null to any element of a
:
a[3] = null; // ERROR: converting null to non-nullable type
Arguably this is an inconsistency in the language. It exists for historical reasons. In older versions of C# there were no nullable types, and a value of any reference type could be null. When nullable types were added to C# a few years ago, the default value of reference types remained null, presumably for backward compatibility.
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 parameters are optional, you may specify
any subset of them you like using named arguments. For example, we
may invoke the fill
function from the previous section
as
fill(a, val: 4) // same as fill(a, 0, 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);
In a class you may declare multiple methods or constructors that have the same name but have different numbers and/or types of parameters. This is called overloading.
For example, here's a class that implements a vector with any number of dimensions:
class Vec { double[] a; public Vec(params double[] a) { this.a = a; } public Vec(int dims) { this.a = new double[dims]; } // Compute the sum of this vector and the vector w. public Vec add(Vec w) { double[] b = new double[a.Length]; for (int i = 0 ; i < a.Length ; ++i) b[i] = this.a[i] + w.a[i]; return new Vec(b); } }
Notice that there are two overloaded constructors: one takes any number of parameters representing coordinates, and the other creates a zero vector of any number of dimensions. Let's create two vectors using these constructors:
Vec v = new Vec(2.0, 5.0, 10.0); Vec w = new Vec(5); // 5-dimensional zero vector
Notice that if the second overloaded constructor above did not exist,
then the call new
Vec(
5
)
would call the first constructor, since 5 can be implicitly
converted to a double. When both constructors exist, the call new
Vec(
5
)
is potentially ambiguous, but C# resolves the ambiguity by
choosing the constructor that does not require an implicit
conversion.
A field or method may be static. Static members are shared by all instances of a class. For example:
class Person { string name; int id; static int next_id; public Person(string name) { this.name = name; this.id = next_id++; } public static int get_next_id() { return next_id; } }
In this class the static field next_id holds the next id that will be assigned to a newly created Person. A static method such as get_next_id() is invoked on a class, not on an instance:
WriteLine(Person.get_next_id());
Sometimes we may reasonably implement a method either as an instance method or a static method. For example:
class Point { double x, y; public Point(double x, double y) { this.x = x; this.y = y; } public double dist(Point p) => Sqrt((p.x - this.x) * (p.x - this.x) + (p.y - this.y) * (p.y - this.y)); public static double dist(Point p, Point q) => Sqrt((p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y)); }
Thie class has two overloaded versions of a dist() method that computes the distance between two points. The first is an instance method, and can be invoked like this:
Point p = new Point(3, 4); Point q = new Point(5, 6); WriteLine(p.dist(q));
The second is a static method, and can be invoked like this:
WriteLine(Point.dist(p, q));
Notice that either an instance method or static method may access private fields of any instance of the class that it belongs to.
Which version of dist() is better? This is a question of style. Some people may prefer the static version because it is more symmetric.
A class may be marked static to indicate that all of its members are static. For example, we might use a static class to hold numeric utility methods:
static class Util { static int gcd(int a, int b) => a == 0 ? b : gcd(b, a % b); static bool coprime(int a, int b) => gcd(a, b) == 1; }
No instance of a static class may ever exist. If you try to create one using the new() operator, you'll receive a compile-time error.
It may be helpful to import a static class with 'using static' so that you can access its members without specifying a prefix:
using static Util;
A property is syntactically like a field, but acts like a method. Specifically, it contains a getter and/or a setter, which are blocks of code that run when the caller retrieves or updates the property's value.
As an example, here's a class that implements a stack of integers:
class IntStack { int[] a; int n; public IntStack(int limit) { a = new int[limit]; } public bool isEmpty { get { return n == 0; } } public int limit { get { return a.Length; } set { Debug.Assert(value >= n, "new limit is too small"); int[] b = new int[value]; for (int i = 0; i < n; ++i) b[i] = a[i]; a = b; } } public void push(int i) { a[n++] = i; } public int pop() { return a[--n]; } }
The class has a boolean property is
E
mpty
that's true if the stack is empty. This property has a getter but no
setter, so it can only be read. For example:
IntStack s = new IntStack(5); WriteLine(s.isEmpty); // writes true
When the caller retrieves the property, the get block runs and returns a boolean value.
The class also has a property limit
with both a getter and setter. We might write
IntStack s = new IntStack(5); WriteLine(s.limit); // writes 5 s.limit = 10; // increase the maximum size WriteLine(s.limit); // writes 10
When the caller retrieves the property, the getter runs. When the
caller sets the property, as in the line 's.limit = 10' above,
the setter runs. Inside any setter, the keyword value
refers to the value that is being set. In this specific example, it
is 10.
You can use expression syntax to define getters or setters. The isEmpty property above could be written as
public bool isEmpty { get => n == 0; }
If a property has only a getter but no setter, it can be written using an even simpler syntax:
public bool isEmpty => n == 0;
An indexer allows you to define custom
getter and/or setter blocks that run when an instance of your class
is accessed using the array-like syntax a[i]
.
For example, here is a vector class that includes
properties dims
and length
, plus an
indexer:
class Vec { double[] a; public Vec(params double[] a) { this.a = a; } public int dims => a.Length; // number of dimensions public double length { get { double t = 0.0; foreach (double d in a) t += d * d; return t; } } // indexer public double this[int i] { get { return a[i]; } set { a[i] = value; } } }
A caller can invoke this indexer as if v itself were an array:
Vec v = new(2.0, 4.0, 8.0); v[1] = 5.5; // invokes the setter v[2] = v[0] + 1.0; // invokes both the getter and setter
An indexer getter may be written using expression-valued syntax, so we could simplify the get block above as follows:
get => a[i];
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. For example, a class representing a dictionary from
strings to strings might have an indexer like this:
class StringDict { public string this[string s] { ... } }
An indexer may even take multiple arguments. For example, a class representing a matrix might have an indexer like this:
class Matrix { public double this[int r, int c] { ... } }
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, let's add an overloaded + operator to the Vec class we saw
above:
public static Vec operator + (Vec v, Vec w) { Debug.Assert(v.dims == w.dims, "incompatible vectors"); double[] b = new double[v.dims]; for (int i = 0 ; i < v.dims ; ++i) b[i] = v[i] + w[i]; return new Vec(b); }
Now we can write this:
Vec v = new(2.0, 5.0, 10.0); Vec w = new(1.0, 3,0, 9.9); Vec x = v + w;
An overloaded operator must be public
and static
.
Notice that the overloaded operator above is
invoking the class's own indexer (in the line 'b[i] = v[i] + w[i]')
and also the dims
property.
You may overload most of the built-in operators available in C#, including
unary operators: +
, -
,
!
, ++
, –
binary operators: +, -, *, /, %, <, >, <=, >=
Another kind of class member is constants. For example:
class Stack { const int LIMIT = 100; int[] a = new a[LIMIT]; ... }
Like other members, a constant may be either public or private.
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