Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (3.69 MB, 424 trang )
114
CHAPTER 4
Saying nothing with nullable types
you often care about having the whole range of possible bit patterns available as real
values, whereas with reference types we’re happy enough to lose one potential value
in order to gain the benefits of having a null value.
That’s the usual situation—now why would you want to be able to represent null
for a value type anyway? The most common immediate reason is simply because databases typically support NULL as a value for every type (unless you specifically make the
field non-nullable), so you can have nullable character data, nullable integers, nullable Booleans—the whole works. When you fetch data from a database, it’s generally
not a good idea to lose information, so you want to be able to represent the nullity of
whatever you read, somehow.
That just moves the question one step further on, though. Why do databases
allow null values for dates, integers and the like? Null values are typically used for
unknown or missing values such as the dispatch date in our earlier e-commerce
example. Nullity represents an absence of definite information, which can be important in many situations.
That brings us to options for representing null values in C# 1.
4.1.2
Patterns for representing null values in C# 1
There are three basic patterns commonly used to get around the lack of nullable
value types in C# 1. Each of them has its pros and cons—mostly cons—and all of them
are fairly unsatisfying. However, it’s worth knowing them, partly to more fully appreciate the benefits of the integrated solution in C# 2.
PATTERN 1: THE MAGIC VALUE
The first pattern tends to be used as the solution for DateTime, because few people
expect their databases to actually contain dates in 1AD. In other words, it goes against the
reasoning I gave earlier, expecting every possible value to be available. So, we sacrifice
one value (typically DateTime.MinValue) to mean a null value. The semantic meaning of
that will vary from application to application—it may mean that the user hasn’t entered
the value into a form yet, or that it’s inappropriate for that record, for example.
The good news is that using a magic value doesn’t waste any memory or need any
new types. However, it does rely on you picking an appropriate value that will never be
one you actually want to use for real data. Also, it’s basically inelegant. It just doesn’t
feel right. If you ever find yourself needing to go down this path, you should at least
have a constant (or static read-only value for types that can’t be expressed as constants) representing the magic value—comparisons with DateTime.MinValue everywhere, for instance, don’t express the meaning of the magic value.
ADO.NET has a variation on this pattern where the same magic value—
DBNull.Value—is used for all null values, of whatever type. In this case, an extra value
and indeed an extra type have been introduced to indicate when a database has
returned null. However, it’s only applicable where compile-time type safety isn’t
important (in other words when you’re happy to use object and cast after testing for
nullity), and again it doesn’t feel quite right. In fact, it’s a mixture of the “magic value”
pattern and the “reference type wrapper” pattern, which we’ll look at next.
System.Nullable
115
PATTERN 2: A REFERENCE TYPE WRAPPER
The second solution can take two forms. The simpler one is to just use object as the
variable type, boxing and unboxing values as necessary. The more complex (and
rather more appealing) form is to have a reference type for each value type you need
in a nullable form, containing a single instance variable of that value type, and with
implicit conversion operators to and from the value type. With generics, you could do
this in one generic type—but if you’re using C# 2 anyway, you might as well use the
nullable types described in this chapter instead. If you’re stuck in C# 1, you have to
create extra source code for each type you wish to wrap. This isn’t hard to put in the
form of a template for automatic code generation, but it’s still a burden that is best
avoided if possible.
Both of these forms have the problem that while they allow you to use null
directly, they do require objects to be created on the heap, which can lead to garbage
collection pressure if you need to use this approach very frequently, and adds memory
use due to the overheads associated with objects. For the more complex solution, you
could make the reference type mutable, which may reduce the number of instances
you need to create but could also make for some very unintuitive code.
PATTERN 3: AN EXTRA BOOLEAN FLAG
The final pattern revolves around having a normal value type value available, and
another value—a Boolean flag—indicating whether the value is “real” or whether it
should be disregarded. Again, there are two ways of implementing this solution.
Either you could maintain two separate variables in the code that uses the value, or
you could encapsulate the “value plus flag” into another value type.
This latter solution is quite similar to the more complicated reference type idea
described earlier, except that you avoid the garbage-collection issue by using a value
type, and indicate nullity within the encapsulated value rather than by virtue of a null
reference. The downside of having to create a new one of these types for every value
type you wish to handle is the same, however. Also, if the value is ever boxed for some
reason, it will be boxed in the normal way whether it’s considered to be null or not.
The last pattern (in the more encapsulated form) is effectively how nullable types
work in C# 2. We’ll see that when the new features of the framework, CLR, and language
are all combined, the solution is significantly neater than anything that was possible in
C# 1. Our next section deals with just the support provided by the framework and the
CLR: if C# 2 only supported generics, the whole of section 4.2 would still be relevant and
the feature would still work and be useful. However, C# 2 provides extra syntactic sugar
to make it even better—that’s the subject of section 4.3.
4.2
System.Nullable
The core structure at the heart of nullable types is System.Nullable
the System.Nullable static class provides utility methods that occasionally make nullable types easier to work with. (From now on I’ll leave out the namespace, to make life
simpler.) We’ll look at both of these types in turn, and for this section I’ll avoid any extra
features provided by the language, so you’ll be able to understand what’s going on in
the IL code when we do look at the C# 2 syntactic sugar.
116
4.2.1
CHAPTER 4
Saying nothing with nullable types
Introducing Nullable
As you can tell by its name, Nullable
value type constraint on it. As I mentioned in section 3.3.1, this also means you can’t
use another nullable type as the argument—so Nullable
of T for any particular nullable type is called the underlying type of that nullable type. For
example, the underlying type of Nullable
The most important parts of Nullable
Value. They do the obvious thing: Value represents the non-nullable value (the
“real” one, if you will) when there is one, and throws an InvalidOperationException if (conceptually) there is no real value. HasValue is simply a Boolean
property indicating whether there’s a real value or whether the instance should be
regarded as null. For now, I’ll talk about an “instance with a value” and an “instance
without a value,” which mean instances where the HasValue property returns true or
false, respectively.
Now that we know what we want the properties to achieve, let’s see how to create
an instance of the type. Nullable
an instance without a value) and one taking an instance of T as the value. Once an
instance has been constructed, it is immutable.
NOTE
Value types and mutability—A type is said to be immutable if it is designed so
that an instance can’t be changed after it’s been constructed. Immutable
types often make life easier when it comes to topics such as multithreading, where it helps to know that nobody can be changing values in one
thread while you’re reading them in a different one. However, immutability is also important for value types. As a general rule, value types should
almost always be immutable. If you need a way of basing one value on
another, follow the lead of DateTime and TimeSpan—provide methods
that return a new value rather than modifying an existing one. That way,
you avoid situations where you think you’re changing a variable but actually
you’re changing the value returned by a property or method, which is just
a copy of the variable’s value. The compiler is usually smart enough to
warn you about this, but it’s worth trying to avoid the situation in the first
place. Very few value types in the framework are mutable, fortunately.
Nullable
overloads. Both return the value of the instance if there is one, or a default value otherwise. One overload doesn’t have any parameters (in which case the generic default
value of the underlying type is used), and the other allows you to specify the default
value to return if necessary.
The other methods implemented by Nullable
GetHashCode, ToString, and Equals. GetHashCode returns 0 if the instance doesn’t
have a value, or the result of calling GetHashCode on the value if there is one.
ToString returns an empty string if there isn’t a value, or the result of calling
System.Nullable
117
ToString on the value if there is. Equals is slightly more complicated—we’ll come
back to it when we’ve discussed boxing.
Finally, two conversions are provided by the framework. First, there is an implicit
conversion from T to Nullable
returns true. Likewise, there is an explicit operator converting from Nullable
T, which behaves exactly the same as the Value property, including throwing an exception when there is no real value to return.
NOTE
Wrapping and unwrapping—The C# specification names the process of
converting an instance of T to an instance of Nullable
the obvious opposite process being called unwrapping. The C# specification actually defines these terms with reference to the constructor taking
a parameter and the Value property, respectively. Indeed these calls are
generated by the C# code, even when it otherwise looks as if you’re using
the conversions provided by the framework. The results are the same
either way, however. For the rest of this chapter, I won’t distinguish
between the two implementations available.
Before we go any further, let’s see all this in action. Listing 4.1 shows everything you
can do with Nullable
Listing 4.1 Using various members of Nullable
static void Display(Nullable
{
Console.WriteLine ("HasValue: {0}", x.HasValue);
if (x.HasValue)
{
Console.WriteLine ("Value: {0}", x.Value);
Console.WriteLine ("Explicit conversion: {0}", (int)x);
}
Console.WriteLine ("GetValueOrDefault(): {0}",
x.GetValueOrDefault());
Console.WriteLine ("GetValueOrDefault(10): {0}",
x.GetValueOrDefault(10));
Console.WriteLine ("ToString(): \"{0}\"", x.ToString());
Console.WriteLine ("GetHashCode(): {0}", x.GetHashCode());
Console.WriteLine ();
}
...
Nullable
x = new Nullable
Console.WriteLine("Instance with value:");
Display(x);
x = new Nullable
Console.WriteLine("Instance without value:");
Display(x);
In listing 4.1 we first show the two different ways (in terms of C# source code) of wrapping a value of the underlying type, and then we use various different members on the
118
CHAPTER 4
Saying nothing with nullable types
instance. Next, we create an instance that doesn’t have a value, and use the same members in the same order, just omitting the Value property and the explicit conversion to
int since these would throw exceptions. The output of listing 4.1 is as follows:
Instance with value:
HasValue: True
Value: 5
Explicit conversion: 5
GetValueOrDefault(): 5
GetValueOrDefault(10): 5
ToString(): "5"
GetHashCode(): 5
Instance without value:
HasValue: False
GetValueOrDefault(): 0
GetValueOrDefault(10): 10
ToString(): ""
GetHashCode(): 0
So far, you could probably have predicted all of the results just by looking at the members provided by Nullable
there’s special behavior to make nullable types behave how we’d really like them to
behave, rather than how they’d behave if we slavishly followed the normal boxing rules.
4.2.2
Boxing and unboxing
It’s important to remember that Nullable
if you want to convert it to a reference type (object is the most obvious example), you’ll
need to box it. It is only with respect to boxing and unboxing that the CLR itself has any
special behavior regarding nullable types—the rest is “standard” generics, conversions,
method calls, and so forth. In fact, the behavior was only changed shortly before the
release of .NET 2.0, as the result of community requests.
An instance of Nullable
value) or a boxed value of T (if it does). You can unbox from a boxed value either to
its normal type or to the corresponding nullable type. Unboxing a null reference will
throw a NullReferenceException if you unbox to the normal type, but will unbox to
an instance without a value if you unbox to the appropriate nullable type. This behavior is shown in listing 4.2.
Listing 4.2
Boxing and unboxing behavior of nullable types
Nullable
object boxed = nullable;
Console.WriteLine(boxed.GetType());
int normal = (int)boxed;
Console.WriteLine(normal);
Boxes a nullable
with value
Unboxes to nonnullable variable
nullable = (Nullable
Console.WriteLine(nullable);
Unboxes to
nullable variable
System.Nullable
nullable = new Nullable
boxed = nullable;
Console.WriteLine (boxed==null);
119
Boxes a nullable
without value
nullable = (Nullable
Console.WriteLine(nullable.HasValue);
Unboxes to
nullable variable
The output of listing 4.2 shows that the type of the boxed value is printed as System.
Int32 (not System.Nullable
the value by unboxing to either just int or to Nullable
successfully unbox again to another value-less nullable instance. If we’d tried unboxing
the last value of boxed to a non-nullable int, the program would have blown up with a
NullReferenceException.
Now that we understand the behavior of boxing and unboxing, we can begin to
tackle the behavior of Nullable
4.2.3
Equality of Nullable
Nullable
operators or provide an Equals(Nullable
plied the basic building blocks, languages can add extra functionality on top, including making existing operators work as we’d expect them to. We’ll see the details of
that in section 4.3.3, but the basic equality as defined by the vanilla Equals method
follows these rules for a call to first.Equals(second):
■
■
■
■
If first has no value and second is null, they are equal.
If first has no value and second isn’t null, they aren’t equal.
If first has a value and second is null, they aren’t equal.
Otherwise, they’re equal if first’s value is equal to second.
Note that we don’t have to consider the case where second is another Nullable
because the rules of boxing prohibit that situation. The type of second is object, so in
order to be a Nullable
a nullable instance creates a box of the non-nullable type or returns a null reference.
The rules are consistent with the rules of equality elsewhere in .NET, so you can use
nullable instances as keys for dictionaries and any other situations where you need
equality. Just don’t expect it to differentiate between a non-nullable instance and a
nullable instance with a value—it’s all been carefully set up so that those two cases are
treated the same way as each other.
That covers the Nullable
Nullable class.
4.2.4
Support from the nongeneric Nullable class
The System.Nullable
receives a little help from the System.Nullable class. This is a static class—it only
120
CHAPTER 4
Saying nothing with nullable types
contains static methods, and you can’t create an instance of it.2 In fact, everything it
does could have been done equally well by other types, and if Microsoft had seen
where they were going right from the beginning, it might not have even existed—
which would have saved a little confusion over what the two types are there for, aside
from anything else. However, this accident of history has three methods to its name,
and they’re still useful.
The first two are comparison methods:
public static int Compare
public static bool Equals
Compare uses Comparer
exist), and Equals uses EqualityComparer
no values, the values returned from each method comply with the .NET conventions
of nulls comparing equal to each other and less than anything else.
Both of these methods could quite happily be part of Nullable
nongeneric methods. The one small advantage of having them as generic methods in
a nongeneric type is that generic type inference can be applied, so you’ll rarely need
to explicitly specify the type parameter.
The final method of System.Nullable isn’t generic—indeed, it absolutely couldn’t
be. Its signature is as follows:
public static Type GetUnderlyingType (Type nullableType)
If the parameter is a nullable type, the method returns its underlying type; otherwise
it returns null. The reason this couldn’t be a generic method is that if you knew the
underlying type to start with, you wouldn’t have to call it!
We’ve now seen what the framework and the CLR provide to support nullable
types—but C# 2 adds language features to make life a lot more pleasant.
4.3
C# 2’s syntactic sugar for nullable types
The examples so far have shown nullable types doing their job, but they’ve not been
particularly pretty to look at. Admittedly it makes it obvious that you are using nullable
types when you have to type Nullable<> around the name of the type you’re really
interested in, but it makes the nullability more prominent than the name of the type
itself, which is surely not a good idea.
In addition, the very name “nullable” suggests that we should be able to assign
null to a variable of a nullable type, and we haven’t seen that—we’ve always used the
default constructor of the type. In this section we’ll see how C# 2 deals with these
issues and others.
Before we get into the details of what C# 2 provides as a language, there’s one definition I can finally introduce. The null value of a nullable type is the value where
HasValue returns false—or an “instance without a value,” as I’ve referred to it in section 4.2. I didn’t use it before because it’s specific to C#. The CLI specification
2
You’ll learn more about static classes in chapter 7.
C# 2’s syntactic sugar for nullable types
121
doesn’t mention it, and the documentation for Nullable
I’ve honored that difference by waiting until we’re specifically talking about C# 2
itself before introducing the term.
With that out of the way, let’s see what features C# 2 gives us, starting by reducing
the clutter in our code.
4.3.1
The ? modifier
There are some elements of syntax that may be unfamiliar at first but have an appropriate feel to them. The conditional operator (a ? b : c) is one of them for me—it asks
a question and then has two corresponding answers. In the same way, the ? operator
for nullable types just feels right to me.
It’s a shorthand way of using a nullable type, so instead of using Nullable
we can use byte? throughout our code. The two are interchangeable and compile to
exactly the same IL, so you can mix and match them if you want to, but on behalf of
whoever reads your code next, I’d urge you to pick one way or the other and use it
consistently. Listing 4.3 is exactly equivalent to listing 4.2 but uses the ? modifier.
Listing 4.3 The same code as listing 4.2 but using the ? modifier
int? nullable = 5;
object boxed = nullable;
Console.WriteLine(boxed.GetType());
int normal = (int)boxed;
Console.WriteLine(normal);
nullable = (int?)boxed;
Console.WriteLine(nullable);
nullable = new int?();
boxed = nullable;
Console.WriteLine (boxed==null);
nullable = (int?)boxed;
Console.WriteLine(nullable.HasValue);
I won’t go through what the code does or how it does it, because the result is exactly the
same as listing 4.2. The two listings compile down to the same IL—they’re just using different syntax, just as using int is interchangeable with System.Int32. The only changes
are the ones in bold. You can use the shorthand version everywhere, including in
method signatures, typeof expressions, casts, and the like.
The reason I feel the modifier is very well chosen is that it adds an air of uncertainty to the nature of the variable. Does the variable nullable in listing 4.3 have an
integer value? Well, at any particular time it might, or it might be the null value. From
now on, we’ll use the ? modifier in all the examples—it’s neater, and it’s arguably the
idiomatic way to use nullable types in C# 2. However, you may feel that it’s too easy to
miss when reading the code, in which case there’s certainly nothing to stop you from
using the longer syntax. You may wish to compare the listings in this section and the
previous one to see which you find clearer.