Notice in the code that C# allows you to use fairly simple syntax to initialize the two Nullable<Int32> variables, x and y. In fact, the C# team wants to integrate nullable value types into the C# language, making them first-class citizens. To that end, C# offers a cleaner syntax for working with nullable value types. C# allows the code to declare and initialize the x and y variables to be written using question mark notation.
Int32? x = 5; Int32? y = null;
In C#, Int32? is a synonym notation for Nullable<Int32>. But C# takes this further. C# allows you to perform conversions and casts on nullable instances. And C# also supports applying operators to nullable instances. The following code shows examples of these.
private static void ConversionsAndCasting() {
// Implicit conversion from nonnullable Int32 to Nullable<Int32> Int32? a = 5;
// Implicit conversion from 'null' to Nullable<Int32>
Int32? b = null; // Same as "Int32? b = new Int32?();" which sets HasValue to false
// Explicit conversion from Nullable<Int32> to nonnullable Int32 Int32 c = (Int32) a;
// Casting between nullable primitive types
Double? d = 5; // Int32>Double? (d is 5.0 as a double) Double? e = b; // Int32?>Double? (e is null)
}
C# also allows you to apply operators to nullable instances. The following code shows examples of this.
private static void Operators() { Int32? a = 5;
Int32? b = null;
// Unary operators (+ ++ ! ~) a++; // a = 6
b = b; // b = null
// Binary operators (+ * / % & | ^ << >>) a = a + 3; // a = 9
b = b * 3; // b = null;
// Equality operators (== !=)
if (a == null) { /* no */ } else { /* yes */ } if (b == null) { /* yes */ } else { /* no */ } if (a != b) { /* yes */ } else { /* no */ }
// Comparison operators (<> <= >=)
if (a < b) { /* no */ } else { /* yes */ }
}
Here is how C# interprets the operators:
■ Unary operators (+, ++, -, --, ! , ~)If the operand is null, the result is null.
■ Binary operators (+, -, *, /, %, &, |, ^, <<, >>)If either operand is null, the result is null. However, an exception is made when the & and | operators are operating on Boolean? op- erands, so that the behavior of these two operators gives the same behavior as demonstrated by SQL’s three-valued logic. For these two operators, if neither operand is null, the operator performs as expected, and if both operands are null, the result is null. The special behavior comes into play when just one of the operands is null. The following table lists the results produced by these two operators for all combinations of true, false, and null.
Operand1→Operand2↓
true
false
null
True
& = true
| = true
& = false
| = true
& = null
| = true
False
& = false
| = true
& = false
| = false
& = false
| = null
Null
& = null
| = true
& = false
| = null
& = null
| = null
■ Equality operators (==, !=)If both operands are null, they are equal. If one operand is null, they are not equal. If neither operand is null, compare the values to determine if they are equal.
■ Relational operators (<, >, <=, >=)If either operand is null, the result is false. If neither operand is null, compare the values.
You should be aware that manipulating nullable instances does generate a lot of code. For example, see the following method.
private static Int32? NullableCodeSize(Int32? a, Int32? b) { return a + b;
}
When I compile this method, there is quite a bit of resulting Intermediate Language (IL) code, which also makes performing operations on nullable types slower than performing the same opera- tion on non-nullable types. Here is the C# equivalent of the compiler-produced IL code.
private static Nullable<Int32> NullableCodeSize(Nullable<Int32> a, Nullable<Int32> b) {
if (!(nullable1.HasValue & nullable2.HasValue)) { return new Nullable<Int32>();
}
return new Nullable<Int32>(nullable1.GetValueOrDefault() + nullable2.GetValueOrDefault());
}
Finally, let me point out that you can define your own value types that overload the various op- erators previously mentioned. I discuss how to do this in the “Operator Overload Methods” section in Chapter 8, “Methods.” If you then use a nullable instance of your own value type, the compiler does the right thing and invokes your overloaded operator. For example, suppose that you have a Point value type that defines overloads for the == and != operators as follows.