1. Trang chủ >
  2. Công Nghệ Thông Tin >
  3. Kỹ thuật lập trình >

1 What do you do when you just don’t have a value?

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 and 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 and System.Nullable

The core structure at the heart of nullable types is System.Nullable. In addition,

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 is a generic type. The type parameter T has the

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> is forbidden, for instance, even though Nullable is a value type in every other way. The type

of T for any particular nullable type is called the underlying type of that nullable type. For

example, the underlying type of Nullable is int.

The most important parts of Nullable are its properties, HasValue and

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 has two constructors: the default one (creating

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 introduces a single new method, GetValueOrDefault, which has two



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 all override existing methods:

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 and 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. This always results in an instance where HasValue

returns true. Likewise, there is an explicit operator converting from Nullable to

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 wrapping, with

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 directly, leaving Equals aside for the moment.

Listing 4.1 Using various members of Nullable

static void Display(Nullable x)

{

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 = 5;

x = new Nullable(5);

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. When it comes to boxing and unboxing, however,

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 is a struct—a value type. This means that

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 is boxed to either a null reference (if it doesn’t have a

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 nullable = 5;

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)boxed;

Console.WriteLine(nullable);



Unboxes to

nullable variable



System.Nullable and System.Nullable

nullable = new Nullable();

boxed = nullable;

Console.WriteLine (boxed==null);



119



Boxes a nullable

without value



nullable = (Nullable)boxed;

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). It then confirms that we can retrieve

the value by unboxing to either just int or to Nullable. Finally, the output demonstrates we can box from a nullable instance without a value to a null reference and

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.Equals.



4.2.3



Equality of Nullable instances

Nullable overrides object.Equals(object) but doesn’t introduce any equality

operators or provide an Equals(Nullable) method. Since the framework has sup-



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 it would have to be boxed, and as we have just seen, boxing

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 structure itself, but it has a shadowy partner: the

Nullable class.



4.2.4



Support from the nongeneric Nullable class

The System.Nullable struct does almost everything you want it to. However, it

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(Nullable n1, Nullable n2)

public static bool Equals(Nullable n1, Nullable n2)



Compare uses Comparer.Default to compare the two underlying values (if they

exist), and Equals uses EqualityComparer.Default. In the face of instances with



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 as static but

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 itself doesn’t mention it.

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.



Xem Thêm
Tải bản đầy đủ (.pdf) (424 trang)

×