One of the most common scenarios for nullable types is to represent unknown values. This frequently occurs in database programming, where a class is mapped to a table with nullable columns. If these columns are strings (e.g., an Email-Address column on a Customer table), there is not a problem as string is a reference type in the CLR, which can be null. However, most other SQL column types map to CLR struct types, making nullable types very useful when mapping SQL to the CLR. For example:
// maps to a Customer table in a database public class Customer { ... public decimal? AccountBalance; }
A nullable type can also be used to represent the backing field of an ambient property. An ambient property, if null, returns the value of its parent. For example:
public class Row { ... Grid parent; Color? backColor;
public Color BackColor { get { return backColor ?? parent.BackColor; } set { backColor = backColor == parent.BackColor ? null : value; } } }
Alternatives to Nullable Types
Before nullable types were part of the C# language (i.e., before C# 2.0), there were many strategies to deal with nullable value types, examples of which still appear in the .NET Framework for historical reasons. One of these strategies is to designate a particular nonnull value as the “null value”; an example is in the string and array classes. String.IndexOf returns the magic value of -1 when the character is not found:
int i = "Pink".IndexOf ('b'); onsole.WriteLine(s); // outputs -1
However,Array.IndexOfreturns-1only if the index is 0-bounded. The more general formula is thatIndexOfreturns 1 less than the minimum bound of the array. In the next example,IndexOfreturns0when an element is not found:
// Create an array whose lower bound is 1 instead of 0:
Array a = Array.CreateInstance (typeof(string),
new int[] {2}, new int[] {1}); a.SetValue("a", 1); a.SetValue("b", 2); Console.WriteLine(Array.IndexOf(a, "c")); // outputs 0
Nominating a “magic value” is problematic for several reasons:
It means that each value type has a different representation of null. In contrast, nullable types provide one common pattern that works for all value types.
There may be no reasonable designated value. In the previous example, –1 could not always be used. The same is true for our earlier examples representing an unknown account balance and an unknown temperature.
Forgetting to test for the magic value results in an incorrect value that may go unnoticed until later in execution—when it pulls an unintended magic trick. Forgetting to testHasValueon a null value, however, throws anInvalidOperationExceptionon the spot.
The ability for a value to be null is not captured in the type. Types communicate the intention of a program, allow the compiler to check for correctness, and enable a consistent set of rules enforced by the compiler.
Operators can be overloaded to provide more natural syntax for custom types. Operator overloading is most appropriately used for implementing custom structs that represent fairly primitive data types. For example, a custom numeric type is an excellent candidate for operator overloading.
Implicit and explicit conversions (with theimplicitandexplicitkeywords)
The literalstrueandfalse
The following operators are indirectly overloaded:
The compound assignment operators (e.g.,+=,/=) are implicitly overridden by overriding the noncompound operators (e.g.,+,=).
The conditional operators&&and||are implicitly overridden by overriding the bitwise operators&and|.
Operator Functions
An operator is overloaded by declaring an operator function. An operator function has the following rules:
The name of the function is specified with theoperatorkeyword followed by an operator symbol.
The operator function must be markedstatic.
The parameters of the operator function represent the operands.
The return type of an operator function represents the result of an expression.
At least one of the operands must be the type in which the operator function is declared.
In the following example, we define a struct calledNoterepresenting a musical note, and then overload the+operator:
public struct Note { int value; public Note (int semitonesFromA) { value = semitonesFromA; }
public static Note operator + (Note x, int semitones) { return new Note (x.value + semitones); } }
This overload allows us to add anintto aNote:
Note B = new Note(2); Note CSharp = B + 2;
Overloading an assignment operator automatically supports the corresponding compound assignment operator. In our example, since we overrode+, we can use+=too:
Equality and comparison operators are sometimes overridden when writing structs, and in rare cases when writing classes. Special rules and obligations come with overloading the equality and comparison operators, which we explain in Chapter 6. A summary of these rules is as follows:
Pairing
The C# compiler enforces operators that are logical pairs to both be defined. These operators are (==!=), (<>), and (<= >=).
Equalsand GetHashCode
In most cases, if you overload (==) and (!=), you need to override theEquals andGetHashCode methods defined onobjectin order to get meaningful behavior. The C# compiler will give a warning if you do not do this. (See the section “Equality comparison ” in Chapter 6 for more details.)
IComparableand IComparable<T>
If you overload (<>) and (<= >=), you should implementIComparableand IComparable<T>.
Custom Implicit and Explicit Conversions
Implicit and explicit conversions are overloadable operators. These conversions are typically overloaded to make converting between strongly related types (such as numeric types) concise and natural.
To convert between weakly related types, the following strategies are more suitable:
Write a constructor that has a parameter of the type to convert from.
WriteToXXX andFromXXX methods to convert between types.
As explained in the discussion on types, the rationale behind implicit conversions is that they are guaranteed to succeed and do not lose information during the conversion. Conversely, an explicit conversion should be required either when runtime circumstances will determine whether the conversion will succeed or if information may be lost during the conversion.
In this example, we define conversions between our musicalNotetype and a double (which represents the frequency in hertz of that note):
... // Convert to hertz public static implicit operator double(Note x) { return 440 * Math.Pow (2,(double) x.value / 12 ); } // Convert from hertz (only accurate to nearest semitone) public static explicit operator Note(double x) { return new Note ((int) (0.5 + 12 * (Math.Log(x/440) / Math.Log(2)) )); } ...
Note n =(Note)554.37; // explicit conversion double x = n; // implicit conversion
Following our own guidelines, this example might be better implemented with aToFrequencymethod (and a staticFromFrequencymethod) instead of implicit and explicit operators.
The true and false operators are used in the extremely rare case of operators defining types with three-state logic to enable these types to work seamlessly with conditional statements and operators—namely, the if, do, while,for, and?:. TheSystem.Data.SqlTypes.SqlBooleanstruct provides this functionality. For example:
class Test { static void Main() { SqlBoolean a = SqlBoolean.Null; if (a) Console.WriteLine("True"); else if (! a) Console.WriteLine("False"); else Console.WriteLine("Null"); } }
OUTPUT: Null
The following code is a reimplementation of the parts ofSqlBooleannecessary to demonstrate thetrueandfalseoperators:
public struct SqlBoolean { public static bool operator true (SqlBoolean x) { return x.m_value == True.m_value; }
public static SqlBoolean operator !(SqlBoolean x) { if (x.m_value == Null.m_value) return Null; if (x.m_value == False.m_value) return True; return False; }
public static readonly SqlBoolean Null = new SqlBoolean(0); public static readonly SqlBoolean False = new SqlBoolean(1); public static readonly SqlBoolean True = new SqlBoolean(2);