Motivation
There is a fundamental difference between value types and reference types in C#. For example, compare:
and
In the first case (with int
s), the value of x
will remain 11
, but
in the second (with arrays of int
s), a[0]
will now contain 11
as
well. That is because when y = x
was executed, the value of x
was
copied, but when b = a
is executed, the reference to the array was
copied.
All the built-in types are value types: numerical types, char
and
bool
contains values. On the other hand, objects, string
and
arrays, for example, are reference types.
null Value
Reference types can contain a special value, called null
, that
intuitively means that it references nothing. It can be used as follows:
Any reference type must be handled with great care, since for example
would compile but would throw a NullReferenceException
exception (a
null
reference doesn’t have any Length
property!).
Three operators allows to simplify testing whenever a variable holds
null
and behave accordingly, we detail them below.
null-Conditional Operator
The null-conditional operator ?
allows to test if a variable holds
null
and to avoid some NullReferenceException
.
For example,
will display “Length of a is: 1.” if a
holds a reference to an array
of size 1, and “Length of a is: .” if a
holds a null
. Stated
differently, a?.Length
evaluates to the size of the array referenced
by a
if it exists, to null
otherwise.
One can similarly write a?[0]
to either get a null
(if a
itself is
null
) or the value at the first index of the array referenced by a
.
null-Coalescing Operator
The null-coalescing operator ??
allows to assign a reference if it is
not null, and to assign a default value otherwise.
For example,
will display “s1 is , s2 is nothing.”: the assignment
s2 = s1 ?? "nothing"
“skipped” the value s1
since it was null
and
used "nothing"
instead.
null-Coalescing Assignment Operator
The null-coalescing assignment operator ??=
allows to re-assign a
variable if it is null
.
For example,
will assign "default"
to s1
if it is null
, leave its value
unchanged otherwise. Note that this operator is available only starting
with C# 8.0.
Nullable value types
It is also possible to make a value type nullable, so that it can
contains the null
value. For example,
is not valid since a?.Length
will evaluate to null
, and an int
variable cannot contain a reference!
It is possible, however, to make aLength
nullable, using the ?
operator:
This way, aLength
can contain either an integer value, or the null
reference.
To “convert” a nullable value type back into a “non-nullable” value type
can be done using the null-coalescing operator ??
. For example,
will assign aLength
to d
if it is not null
, and -1
otherwise:
note that either way, d
will end up containing a non-null
value.
Testing for Equality
Motivation
A great care is required when comparing references, since one need to make sure that
null
is accounted for,- the comparison is “shallow” only if we want it to.
A “shallow” comparison compares only the “surface” of reference variables, as follows:
Both tests would evaluate to true
, since a
and b
do indeed refer
to the same array, while a
and c
refer to different arrays. In
general, this is not what is intended when comparing objects or arrays:
we want to know if what they refer to is identical.
Comparing Arrays
To compare arrays while accounting for possible null
values, a great
care is needed. One can write a method as follows:
So that, if SameArray
is passed…
- … two
null
references, it will returntrue
since, indeed, the arguments refers to “the same” array, which does not exist, - … a
null
reference and a reference that is notnull
, it will returnfalse
, as a non-existent array is not the same as an existing array, - … two arrays of different size, it will return
false
, - … two arrays of the same size, where every single value is the same,
it will return
true
.
Note that
- for the first two cases, one may decide to use
throw new ArgumentNullException()
instead, because it could be argued comparingnull
references is, precisely, shallow. - it is ok to use
arP1.Length
andarP2.Length
in our code, since we know at that point that neitherarP1
norarP2
isnull
. - we cannot use
if (arP1[i] != arP2[i])
as C# doesn’t “know” by default that what we use forT
will accept this operator. Instead, we have to use the “generic”Equals
method.
Passing Arguments
Motivation
Consider the following “swapping” method and a Main
method calling it:
This program would display:
As we can see, the values held by the variables a
and b
are
correctly swapped by the Swap
method, but this change is not
“permanent”: once the Swap
method completed, a
and b
still have
their “old” values inside Main
.
Since a method cannot return two values, making that change permanent is
difficult. A solution could be designed using arrays for example, but it
would require additional manipulation in the Main
method. Instead, one
can use references to pass the reference to the variables instead of
their values.
ref
Keyword
The ref
keyword can be used to pass the reference to a variable, as
follows:
Note that the change with the previous code is minimal: only the keyword
ref
is added:
- In front of the datatype of the arguments in
Swap
’s header, - In front of the name of the variables when the
Swap
method is called.
Note that both edits are required: the first one stipulates that the
Swap
method expects references, and the second one stipulates that
the references are passed.
This program would display:
Indeed, since the reference was passed, Swap
stored the new values
in the same variables a
and b
, making the swapping “permanent”.
out
Keyword
In some cases, one may want to pass a reference to a method simply as an address where a value must be stored. The benefit is that this reference does not need to contain a value before being passed to a method.
For example, consider:
that sets the value of a reference to a random number between 0 and 9 (both included).
It cannot be called as follows:
Because C#‘s compilation will return the error message “Use of
unassigned local variable ‘c’“. Indeed, SetToRandom
expects the
argument to already holds a reference to a value, even if it has no use
for it.
A better alternative is to use the out
keyword:
Note that:
- The keyword
out
is similarly added in the header of the method and when the argument is passed, - The variable
a
is not given a value before being passed to the method.
Summing up, the difference between ref
and out
is that out
does
not require the reference to point to an actual value entering into the
method but it must hold a value by the time we exit the method.
To illustrate this last point, observe that
would not compile, as C# would give back a message “The out parameter
‘a’ must be assigned to before controls leaves the current method”: an
argument passed using the keyword out
must be initialized in the
body of the method.