Chapter 4. Saying nothing with nullable types

This chapter covers

  • Motivation for null values
  • Framework and runtime support
  • Language support in C# 2
  • Patterns using nullable types

Nullity is a concept that has provoked debate over the years. Is a null reference a value, or the absence of a value? Is “nothing” a “something”? Should languages support the concept of nullity at all, or should it be represented in other patterns? In this chapter, I’ll try to stay more practical than philosophical. First we’ll look at why there’s a problem at all—why you can’t set a value type variable to null in C# 1 and what the traditional alternatives have been. After that, I’ll introduce you to our knight in shining armor—System.Nullable<T>—before we see how C# 2 makes working with nullable types simple and compact. Like generics, nullable types sometimes have some uses beyond what you might expect, and we’ll look at a few examples of these at the end of the chapter.

So, when is a value not a value? Let’s find out.

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

The C# and .NET designers don’t add features just for kicks. There has to be a real, significant problem to be fixed before they’ll go as far as changing C# as a language or .NET at the platform level. In this case, the problem is best summed up in one of the most frequently asked questions in C# and .NET discussion groups:

I need to set my DateTime[1]variable to null, but the compiler won’t let me. What should I do?

1 It’s almost always DateTime rather than any other value type. I’m not entirely sure why—it’s as if developers inherently understand why a byte shouldn’t be null, but feel that dates are more inherently nullable.

It’s a question that comes up fairly naturally—an example might be in an e-commerce application where users are looking at their account history. If an order has been placed but not delivered, there may be a purchase date but no dispatch date—so how would you represent that in a type that’s meant to provide the order details?

Before C# 2, the answer to the question was usually in two parts: first, why you couldn’t just use null in the first place, and second, which options were available. Nowadays the answer would usually explain nullable types instead—but it’s worth looking at the C# 1 options to understand where the problem comes from.

4.1.1. Why value type variables can’t be null

As we saw in chapter 2, the value of a reference type variable is a reference, and the value of a value type variable is the real data itself. A non-null reference is a way of getting at an object, but null acts as a special value that means “I don’t refer to any object.” If you want to think of references as being like URLs, null is (very roughly speaking) the reference equivalent of about:blank. It’s represented as all zeroes in memory (which is why it’s the default value for all reference types—clearing a whole block of memory is cheap, so that’s the way objects are initialized), but it’s still basically stored in the same way as other references. There’s no extra bit hidden somewhere for each reference type variable. That means we can’t use the “all zeroes” value for a real reference, but that’s okay—our memory is going to run out long before we have that many live objects anyway.

The last sentence is the key to why null isn’t a valid value type value, though. Let’s consider the byte type as a familiar one that’s easy to think about. The value of a variable of type byte is stored in a single byte—it may be padded for alignment purposes, but the value itself is conceptually only made up of one byte. We’ve got to be able to store the values 0–255 in that variable; otherwise it’s useless for reading arbitrary binary data. So, with the 256 normal values and one null value, we’d have to cope with a total of 257 values, and there’s no way of squeezing that many values into a single byte. Now, the designers could’ve decided that every value type would have an extra flag bit somewhere determining whether a value was null or contained real data, but the memory usage implications are horrible, not to mention the fact that we’d have to check the flag every time we wanted to use the value. So in a nutshell, with value 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 making the null reference available.

That’s the usual situation—now why would you want to be able to represent null for a value type anyway? The most common reason is simply because databases typically support NULL as a value for every type (unless you specifically make the field nonnullable), 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. Indeed, there doesn’t have to be a database involved for nullable value types to be useful: that’s just the scenario where developers typically encounter the problem first.

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 has its pros and cons—mostly cons—and all of them are fairly unsatisfying. But they’re worth knowing, 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 AD1. In other words, it goes against the line of reasoning I gave earlier, which assumes that every possible value needs to be available for normal purposes. 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 not required for that record, for example.

The good news is that using a magic value doesn’t waste any memory or require any new types. But it does rely on you picking an appropriate value that you’ll never 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. Additionally, it’s easy to accidentally use the magic value as if it were a normal, meaningful one—neither the compiler nor the runtime will help you spot the error. Most of the other solutions presented here (including the one in C# 2) would result in either a compilation error or an exception at execution time, depending on the exact situation.

The magic value pattern is deeply embedded in computing in the form of IEEE-754 binary floating-point types such as float and double. These go further than the idea of a single value representing “this isn’t really a number”—there are many bit patterns that are classified as “not a number” (NaN) as well as values for positive and negative infinity. I suspect few programmers (myself included) are as cautious around these values as we should be, which is another indication of the pattern’s shortcomings.

ADO.NET has a variation on this pattern where the same magic value—DBNull.Value—is used for all null values, regardless of the type. In this case, an extra value and indeed an extra type have been introduced to indicate when a database has returned null. But 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.

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 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’s best avoided if possible.

Both of these forms have the problem that though 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 frequently, and adds memory use due to the overhead 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 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, though. Also, if the value is ever boxed for some reason, it’ll 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, most of section 4.2 would still be relevant and the feature would still work and be useful. But C# 2 provides extra syntactic sugar to make it even better—that’s the subject of section 4.3.

4.2. System.Nullable<T>and System.Nullable

The core structure at the heart of nullable types is the System.Nullable<T> struct. 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 shorthand provided by C# 2.

4.2.1. Introducing Nullable<T>

As you can tell by its name, Nullable<T> is a generic type. The type parameter T has a value type constraint, so you can’t use Nullable<Stream>, for example. As I mentioned in section 3.3.1, this also means you can’t use another nullable type as the argument—so Nullable<Nullable<int>> is forbidden, even though Nullable<T> 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<int> is int.

The most important parts of Nullable<T> are its properties, HasValue and Value. They do the obvious: Value represents the non-nullable value (the real one, if you will) when there is one, and throws an InvalidOperationException if (conceptually) there’s 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.

These properties are backed by simple fields in the obvious way. Figure 4.1 shows instances of Nullable<int> representing (from left to right) no value, 0, and 5. Remember that Nullable<T> is still a value type, so if you have a variable of type Nullable<int>, the variable’s value will directly contain a bool and an int—it won’t be a reference to a separate object.

Figure 4.1. Sample values of Nullable<int>

Now that we know what we want the properties to achieve, let’s see how to create an instance of the type. Nullable<T> 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’s immutable.

 

Value Types and Mutability

A type is said to be immutable if it’s designed so that an instance can’t be changed after it’s been constructed. Immutable types often lead to a cleaner design than having to keep track of what might be changing shared values—particularly among different threads.

Immutability is particularly important for value types: they 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 and operators that return a new value rather than modifying an existing one. This avoids all kinds of subtle bugs, including situations where you may appear to be changing something, but you’re actually just changing a copy. Just say “no” to mutable value types.[2]

2 Most value types in the framework are immutable, but there are some commonly used exceptions—in particular, the Point structures for both Windows Forms and Windows Presentation Foundation are mutable.

 

Nullable<T> 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 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<T> 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 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’s an implicit conversion from T to Nullable<T>. This always results in an instance where HasValue returns true. Likewise, there’s an explicit conversion from Nullable<T> to T, which behaves exactly the same as the Value property, including throwing an exception when there’s no real value to return.

 

Wrapping and Unwrapping

The C# specification names the process of converting an instance of T to an instance of Nullable<T> wrapping, with the obvious opposite process being called unwrapping. The 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, though. 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<T> directly, leaving Equals aside for the moment.

Listing 4.1. Using various members of Nullable<T>
static void Display(Nullable<int> 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<int> x = 5;
x = new Nullable<int>(5);
Console.WriteLine("Instance with value:");
Display(x);

x = new Nullable<int>();
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 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<T>. When it comes to boxing and unboxing, though, 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 Nullable<T>and unboxing

It’s important to remember that Nullable <T> 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’s 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<T> is boxed to either a null reference (if it doesn’t have a value) or a boxed value of T (if it does), as shown in figure 4.2. It never boxes to a “boxed nullable int”—there’s simply no such type.

Figure 4.2. Results of boxing an instance without a value (top) or one with a value(bottom)

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 the following listing.

Listing 4.2. Boxing and unboxing behavior of nullable types

The output of listing 4.2 shows that the type of the boxed value is printed as System. Int32 (not System.Nullable<System.Int32>). It then confirms that we can retrieve the value by unboxing to either just int or to Nullable<int>. Finally, the output demonstrates we can box from a nullable instance without a value to a null reference and successfully unbox again to another valueless nullable instance. If we’d tried unboxing the last value of boxed to a non-nullable int, the program would’ve blown up with a NullReferenceException.

Now that we understand the behavior of boxing and unboxing, we can begin to tackle the behavior of Nullable<T>.Equals.

4.2.3. Equality of Nullable<T> instances

Nullable<T> overrides object.Equals(object) but doesn’t introduce any equality operators or provide an Equals(Nullable<T>) method. Since the framework has supplied 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’re 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<T> because the rules of boxing prohibit that situation. The type of second is object, so in order to be a Nullable<T> it would have to be boxed, and as we’ve just seen, boxing a nullable instance creates a box of the non-nullable type or returns a null reference. Initially the first rule may appear to be breaking the contract for object.Equals (object), which insists that x.Equals(null) returns false—but that’s only when x is a non-null reference. Again, due to the boxing behavior, Nullable<T>’s implementation will never be called via a reference.

The rules are mostly 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<T> structure itself, but it has a shadowy partner: the Nullable class.

4.2.4. Support from the nongeneric Nullable class

The System.Nullable<T> struct does almost everything you want it to. But it gets help from the System.Nullable class. This is a static class—it only contains static methods, and you can’t create an instance of it.[3] In fact, everything it does could’ve been done equally well by other types, and if Microsoft had seen where it was going right from the beginning, it might not have even existed—which would’ve saved confusion over what the two types are there for, aside from anything else. But this accident of history has three methods to its name, and they’re still useful.

3 You’ll learn more about static classes in chapter 7.

The first two are comparison methods:

public static int Compare<T>(Nullable<T> n1, Nullable<T> n2)
public static bool Equals<T>(Nullable<T> n1, Nullable<T> n2)

Compare uses Comparer<T>.Default to compare the two underlying values (if they exist), and Equals uses EqualityComparer<T>.Default. In the face of instances with no values, the results 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 happily be part of Nullable<T> 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 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 haven’t 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 underlying type, which is usually 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 value type is the value where HasValue returns false—or an “instance without a value,” as I referred to it in section 4.2. I didn’t use it before because it’s specific to C#. The CLI specification doesn’t mention it, and the documentation for Nullable<T> 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. The term also applies to reference types: the null value of a reference type is simply the null reference we’re familiar with from C# 1.

 

Nullable Type Versus Nullable Value Type

In the C# language specification, nullable type is used to mean any type with a null value—so any reference type, or any Nullable<T>. You may have noticed that I’ve been using this term as if it were synonymous with nullable value type (which obviously doesn’t include reference types). Although I’m usually a huge pedant when it comes to terminology, if I’d used “nullable value type” everywhere in this chapter it would’ve been horrible to read. You should also expect “nullable type” to be used ambiguously in the real world: it’s probably more common to use it when describing Nullable<T> than in the sense described in the specification.

 

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 ? modifier for nullable types just feels right to me.

It’s a shorthand way of specifying a nullable type, so instead of using Nullable <byte> 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, which is shown in bold.

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 simply 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 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#. But you may feel that it’s too easy to miss when reading the code, in which case there’s 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 more clear.

Given that the C# 2 specification defines the null value, it would be odd if we couldn’t use the null literal we already have in the language in order to represent it. Fortunately we can...

4.3.2. Assigning and comparing with null

A concise author could cover this whole section in a single sentence: “The C# compiler allows the use of null to represent the null value of a nullable type in both comparisons and assignments.” I prefer to show you what it means in real code, as well as think about why the language has been given this feature.

You may have felt uncomfortable every time we used the default constructor of Nullable<T>. It achieves the desired behavior, but it doesn’t express the reason why we want to do it—it doesn’t leave the right impression with the reader. We want to give the same sort of feeling that using null does with reference types. If it seems odd to you that I’ve talked about feelings in both this section and the last one, just think about who writes code, and who reads it. Sure, the compiler has to understand the code, and it couldn’t care less about the subtle nuances of style—but few pieces of code used in production systems are written and then never read again. Anything you can do to get the reader into the mental process you were going through when you originally wrote the code is good—and using the familiar null literal helps to achieve that.

With that in mind, we’re going to change the example we’re using from one that just shows syntax and behavior to one that gives an impression of how nullable types might be used. We’ll consider modeling a Person class where you need to know the name, date of birth, and date of death of a person. We’ll only keep track of people who have definitely been born, but some of those people may still be alive—in which case our date of death is represented by null. Listing 4.4 shows some of the possible code. Although a real class would clearly have more operations available, we’re just looking at the calculation of age for this example.

Listing 4.4. Part of a Person class including calculation of age

Listing 4.4 doesn’t produce any output, but the fact that it compiles might have surprised you before reading this chapter. Apart from the use of the ? modifier causing confusion, you might have found it odd that you could compare a DateTime? with null, or pass null as the argument for a DateTime? parameter.

Hopefully by now the meaning is intuitive—when we compare the death variable with null, we’re asking whether its value is the null value or not. Likewise when we use null as a DateTime? instance, we’re really creating the null value for the type by calling the default constructor. Indeed, you can see in the generated IL that the code the compiler spits out for listing 4.4 really does just call the death.HasValue property , and creates a new instance of DateTime? using the default constructor (represented in IL as the initobj instruction). The date of Alan Turing’s death is created by calling the normal DateTime constructor and then passing the result into the Nullable<DateTime> constructor that takes a parameter.

I mention looking at the IL because that can be a useful way of finding out what your code is actually doing, particularly if something compiles when you don’t expect it to. You can use the ildasm tool that comes with the .NET SDK, or for a better user interface you can use Reflector (see http://mng.bz/pMXJ), which has many other features (most notably decompilation to high-level languages such as C# as well as disassembly to IL).

We’ve seen how C# provides shorthand syntax for the concept of a null value, making the code more expressive once nullable types are understood in the first place. But one part of listing 4.4 took a bit more work than we might have hoped—the subtraction at . Why did we have to unwrap the value? Why couldn’t we just return death - birth directly? What would we want that expression to mean if death had been null (excluded in our code by our earlier test, of course)? These questions—and more—are answered in our next section.

4.3.3. Nullable conversions and operators

We’ve seen that we can compare instances of nullable types with null, but there are other comparisons that can be made and other operators that can be used in some cases. Likewise we’ve seen wrapping and unwrapping, but other conversions can be used with some types. This section explains what’s available. I’m afraid it’s pretty much impossible to make this kind of topic genuinely exciting, but carefully designed features like these are what make C# a pleasant language to work with in the long run. Don’t worry if not all of it sinks in the first time: just remember that the details are here if you need to refer to them in the middle of a coding session.

The executive summary is that if there’s an operator or conversion available on a non-nullable value type, and that operator or conversion only involves other nonnullable value types, then the nullable value type also has the same operator or conversion available, usually converting the non-nullable value types into their nullable equivalents. To give a more concrete example, there’s an implicit conversion from int to long, and that means there’s also an implicit conversion from int? to long? that behaves in the obvious manner.

Unfortunately, although that broad description gives the right general idea, the exact rules are slightly more complicated. Each one is simple, but there are quite a few of them. It’s worth knowing about them because otherwise you may end up staring at a compiler error or warning for a while, wondering why it believes you’re trying to make a conversion that you never intended in the first place. We’ll start with the conversions, and then look at operators.

Conversions Involving Nullable Types

For completeness, let’s start with the conversions we already know about:

  • An implicit conversion from the null literal to T?
  • An implicit conversion from T to T?
  • An explicit conversion from T? to T

Now consider the predefined and user-defined conversions available on types. For instance, there’s a predefined conversion from int to long. For any conversion like this, from one non-nullable value type (S) to another (T), the following conversions are also available:

  • S? to T? (explicit or implicit depending on original conversion)
  • S to T? (explicit or implicit depending on original conversion)
  • S? to T (always explicit)

To carry our example forward, this means that you can convert implicitly from int? to long? and from int to long? as well as explicitly from int? to long. The conversions behave in the natural way, with null values of S? converting to null values of T?, and non-null values using the original conversion. As before, the explicit conversion from S? to T will throw an InvalidOperationException when converting from a null value of S?. For user-defined conversions, these extra conversions involving nullable types are known as lifted conversions.

So far, so relatively simple. Now let’s consider the operators, where things are slightly more tricky.

Operators Involving Nullable Types

C# allows the following operators to be overloaded:

  • Unary: + ++ - -- ! ~ true false
  • Binary: + - * / % & | ^ << >>
  • Equality:[4]== !=

    4 The equality and relational operators are binary operators too, but we’ll see that they behave slightly differently than the others; hence their separation here.

  • Relational: < > <= >=

When these operators are overloaded for a non-nullable value type T, the nullable type T? has the same operators, with slightly different operand and result types. These are called lifted operators whether they’re predefined operators such as addition on numeric types, or user-defined operators such as adding a TimeSpan to a DateTime. There are a few restrictions as to when they apply:

  • The true and false operators are never lifted. They’re incredibly rare in the first place, though, so it’s no great loss.
  • Only operators with non-nullable value types for the operands are lifted.
  • For the unary and binary operators (other than equality and relational operators), the return type has to be a non-nullable value type.
  • For the equality and relational operators, the return type has to be bool.
  • The & and | operators on bool? have separately defined behavior, which we’ll see in section 4.3.6.

For all the operators, the operand types become their nullable equivalents. For the unary and binary operators, the return type also becomes nullable, and a null value is returned if any of the operands is a null value. The equality and relational operators keep their non-nullable Boolean return types. For equality, two null values are considered equal, and a null value and any non-null value are considered different, which is consistent with the behavior we saw in section 4.2.3. The relational operators always return false if either operand is a null value. When none of the operands is a null value, the operator of the non-nullable type is invoked in the obvious way.

All these rules sound more complicated than they really are—for the most part, everything works as you probably expect it to. It’s easiest to see what happens with a few examples, and as int has so many predefined operators (and integers can be so easily expressed), it’s the natural demonstration type. Table 4.1 shows a number of expressions, the lifted operator signature, and the result. It’s assumed that there are variables four, five, and nullInt, each with type int? and with the obvious values.

Table 4.1. Examples of lifted operators applied to nullable integers

Expression

Lifted operator

Result

-nullInt
-five
five + nullInt
five + five
nullInt == nullInt
five == five
five == nullInt
five == four
four < five
nullInt < five
five < nullInt
nullInt < nullInt
nullInt <= nullInt
int? –(int? x)
int? –(int? x)
int? +(int? x, int? y)
int? +(int? x, int? y)
bool ==(int? x, int? y)
bool ==(int? x, int? y)
bool ==(int? x, int? y)
bool ==(int? x, int? y)
bool <(int? x, int? y)
bool <(int? x, int? y)
bool <(int? x, int? y)
bool <(int? x, int? y)
bool <=(int? x, int? y)
null
-5
null
10
true
true
false
false
true
false
false
false
false

Possibly the most surprising line of the table is the bottom one—that a null value isn’t deemed less than or equal to another null value, even though they are deemed to be equal to each other (as per the fifth row)! Very odd—but unlikely to cause problems in real life, in my experience.

One aspect of lifted operators and nullable conversion that has caused some confusion is unintended comparisons with null when using a non-nullable value type. The code that follows is legal, but not useful:

int i = 5;
if (i == null)
{
Console.WriteLine ("Never going to happen");
}

The C# compiler raises warnings on this code, but you may consider it surprising that it’s allowed at all. What’s happening is that the compiler sees the int expression on the left side of the ==, sees null on the right side, and knows that there’s an implicit conversion to int? from each of them. Because a comparison between two int? values is perfectly valid, the code doesn’t generate an error—just the warning. As a further complication, this isn’t allowed in the case where instead of int, we’re dealing with a generic type parameter that has been constrained to be a value type—the rules on generics prohibit the comparison with null in that situation.

Either way, there’ll be an error or a warning, so as long as you look closely at warnings, you shouldn’t end up with deficient code due to this quirk—and hopefully pointing it out to you now may save you from getting a headache trying to work out exactly what’s going on.

Now we can answer the question at the end of the previous section—why we used death.Value - birth in listing 4.4 instead of just death - birth. Applying the previous rules, we could have used the latter expression, but the result would’ve been a TimeSpan? instead of a TimeSpan. This would’ve left us with the options of casting the result to TimeSpan, using its Value property, or changing the Age property to return a TimeSpan?—which just pushes the issue onto the caller. It’s still a bit ugly, but we’ll see a nicer implementation of the Age property in section 4.3.6.

In the list of restrictions regarding operator lifting, I mentioned that bool? works slightly differently than the other types. Our next section explains this and pulls back the lens to see the bigger picture of why all these operators work the way they do.

4.3.4. Nullable logic

I vividly remember my early electronics lessons at school. They always seemed to revolve around either working out the voltage across different parts of a circuit using the V=IR formula, or applying truth tables—the reference charts for explaining the difference between NAND gates and NOR gates and so on. The idea is simple—a truth table maps out every possible combination of inputs into whatever piece of logic you’re interested in and tells you the output.

The truth tables we drew for simple, two-input logic gates always had four rows—each input had two possible values, which means there were four possible combinations. Boolean logic is simple like that—but what happens when you have a tristate logical type? Well, bool? is just such a type—the value can be true, false, or null. That means that our truth tables now need nine rows for our binary operators, as there are nine combinations. The specification only highlights the logical AND and inclusive OR operators (& and |, respectively) because the other operators—unary logical negation (!) and exclusive OR (^)—follow the same rules as other lifted operators. There are no conditional logical operators (the short-circuiting && and || operators) defined for bool?, which makes life simpler.

For the sake of completeness, table 4.2 gives the truth table for all four valid bool? logical operators.

Table 4.2. Truth table for the logical operators AND, inclusive OR, exclusive OR, and logical negation, applied to the bool? type

x

y

x & y

x | y

x ^ y

!x

true
true
true
false
false
false
null
null
null
true
false
null
true
false
null
true
false
null
true
false
null
false
false
false
null
false
null
true
true
true
true
false
null
true
null
null
false
true
null
true
false
null
null
null
null
false
false
false
true
true
true
null
null
null

For those who find reasoning about rules easier to understand than looking up values in tables, the idea is that a null bool? value is in some senses a “maybe.” If you imagine that each null entry in the input side of the table is a variable instead, then you’ll always get a null value on the output side of the table if the result depends on the value of that variable. For instance, looking at the third line of the table, the expression true & y will only be true if y is true, but the expression true | y will always be true whatever the value of y is, so the nullable results are null and true, respectively.

When considering the lifted operators and particularly how nullable logic works, the language designers had two slightly contradictory sets of existing behavior—C# 1 null references and SQL NULL values. In many cases, these don’t conflict at all—C# 1 had no concept of applying logical operators to null references, so there was no problem in using the SQL-like results given earlier. The definitions we’ve seen may surprise some SQL developers, though, when it comes to comparisons. In standard SQL, the result of comparing two values (in terms of equality or greater than/less than) is always unknown if either value is NULL. The result in C# 2 is never null, and in particular two null values are considered to be equal to each other.

 

Reminder: This is C# Specific!

It’s worth remembering that the lifted operators and conversions, along with the bool? logic described in this section, are all provided by the C# compiler and not by the CLR or the framework itself. If you use ildasm on code that evaluates any of these nullable operators, you’ll find that the compiler has created all the appropriate IL to test for null values and dealt with them accordingly. This means that different languages can behave differently on these matters—definitely something to look out for if you need to port code between different .NET-based languages.

 

Another familiar operator is now available with nullable value types, and it behaves exactly as you’d expect it to if you consider your existing knowledge of null references and just tweak it to be in terms of null values.

4.3.5. Using the as operator with nullable types

Prior to C# 2, the as operator was only available for reference types. As of C# 2, it can now be applied to nullable value types as well. The result is a value of that nullable type—either the null value if the original reference was the wrong type or null, or a meaningful value otherwise. Here’s a short example:

This allows you to safely convert from an arbitrary reference to a value in a single step—although you’d normally check. In C# 1, you’d have had to use the is operator followed by a cast, which is inelegant: it’s asking the CLR to perform the same type check twice, effectively.

 

Surprising Performance Trap

I’d always assumed that doing one check would be faster than two, but it appears that’s not the case—at least with the versions of .NET I’ve tested with. When writing a quick benchmark that summed all the integers within an array of type object[] where only a third of the values were actually boxed integers, using is and then a cast ended up being 20 times faster than using the as operator. The details are beyond the scope of this book, and as always you should test performance with your actual code and data before deciding the best course of action for your specific situation—but it’s worth being aware of.

 

We now know enough to use nullable types and predict how they’ll behave, but C# 2 has a sort of “bonus track” when it comes to syntax enhancements: the null coalescing operator.

4.3.6. The null coalescing operator

Aside from the ? modifier, all of the rest of the C# compiler’s tricks relating to nullable types so far have worked with the existing syntax. But C# 2 introduces a new operator that can occasionally make code shorter and sweeter. It’s called the null coalescing operator and appears in code as ?? between its two operands. It’s like the conditional operator but specially tweaked for nulls.

It’s a binary operator that evaluates first ?? second by going through the following steps (roughly speaking):

  1. Evaluate first.
  2. If the result is non-null, that’s the result of the whole expression.
  3. Otherwise, evaluate second; the result then becomes the result of the whole expression.

I say “roughly speaking” because the formal rules in the specification have to deal with situations involving conversions between the types of first and second. As ever, these aren’t important in most uses of the operator, and I don’t intend to go through them—consult section 7.13 of the specification if you need the details.

Importantly, if the type of the second operand is the underlying type of the first operand (and therefore non-nullable), then the overall result is that underlying type. For example, this code is perfectly valid:

int? a = 5;
int b = 10;
int c = a ?? b;

Note how we’re assigning directly to c even though its type is the non-nullable int type. We can only do this because b is non-nullable, so we know that we’ll get a non-nullable result eventually. Obviously that’s a pretty simplistic example; let’s find a more practical use for this by revisiting the Age property from listing 4.4. As a reminder, here’s how it was implemented back then, along with the relevant variable declarations:

DateTime birth;
DateTime? death;

public TimeSpan Age
{
get
{
if (death == null)
{
return DateTime.Now - birth;
}
else
{
return death.Value - birth;
}
}
}

Note how both branches of the if statement subtract the value of birth from some non-null DateTime value. The value we’re interested in is the latest time the person was alive—the time of the person’s death if he or she has already died, or now otherwise. To make progress in little steps, let’s try just using the normal conditional operator first:

DateTime lastAlive = (death == null ? DateTime.Now : death.Value);
return lastAlive - birth;

That’s progress of a sort, but arguably the conditional operator has actually made it harder to read rather than easier, even though the new code is shorter. The conditional operator is often like that—how much you use it is a matter of personal preference, although it’s worth consulting the rest of your team before using it extensively. Let’s see how the null coalescing operator improves things. We want to use the value of death if it’s non-null, and DateTime.Now otherwise. We can change the implementation to

DateTime lastAlive = death ?? DateTime.Now;
return lastAlive - birth;

Note how the type of the result is DateTime rather than DateTime? because we’ve used DateTime.Now as the second operand. We could shorten the whole thing to one expression:

return (death ?? DateTime.Now) - birth;

But this is more obscure—in particular, in the two-line version the name of the lastAlive variable helps the reader to see why we’re applying the null coalescing operator. I hope you agree that the two-line version is simpler and more readable than either the original version using the if statement or the version using the normal conditional operator from C# 1. Of course, it relies on the reader understanding what the null coalescing operator does. In my experience, this is one of the least well-known aspects of C# 2, but it’s useful enough to make it worth trying to enlighten your coworkers rather than avoiding it.

There are two further aspects that increase the operator’s usefulness. First, it doesn’t just apply to nullable value types—it works with reference types too; you just can’t use a non-nullable value type for the first operand, as that would be pointless. Also, it’s right associative, which means an expression of the form first ?? second ?? third is evaluated as first ?? (second ?? third)—and so it continues for more operands. You can have any number of expressions, and they’ll be evaluated in order, stopping with the first non-null result. If all of the expressions evaluate to null, the result will be null too.

As a concrete example of this, suppose you have an online ordering system (who doesn’t these days?) with the concepts of a billing address, contact address, and shipping address. The business rules declare that any user must have a billing address, but the contact address is optional. The shipping address for a particular order is also optional, defaulting to the billing address. These optional addresses are easily represented as null references in the code. To work out whom to contact in the case of a problem with a shipment, the code in C# 1 might look something like this:

Address contact = user.ContactAddress;
if (contact == null)
{
contact = order.ShippingAddress;
if (contact == null)
{
contact = user.BillingAddress;
}
}

Using the conditional operator in this case is even more horrible. But using the null coalescing operator makes the code very straightforward:

Address contact = user.ContactAddress ??
order.ShippingAddress ??
user.BillingAddress;

If the business rules changed to use the shipping address by default instead of the user’s contact address, the change here would be extremely obvious. It wouldn’t be particularly taxing with the if/else version, but I know I’d have to stop and think twice, and verify the code mentally. I’d also be relying on unit tests, so there’d be little chance of actually getting it wrong, but I’d prefer not to think about things like this unless I absolutely have to.

 

Everything in Moderation

Just in case you may be thinking that my code is littered with uses of the null coalescing operator, it’s really not. I tend to consider it when I see defaulting mechanisms involving nulls and possibly the conditional operator, but it doesn’t come up often. When its use is natural, though, it can be a powerful tool in the battle for readability.

 

We’ve seen how nullable types can be used for ordinary properties of objects—cases where we naturally might not have a value for some particular aspect that’s still best expressed with a value type. Those are the more obvious uses for nullable types and indeed the most common ones. A few patterns aren’t as obvious but can still be powerful when you’re used to them. We’ll explore two of these patterns in our next section. This is more for the sake of interest than as part of learning about the behavior of nullable types themselves—you now have all the tools you need to use them in your own code. If you’re interested in quirky ideas and perhaps trying something new, read on...

4.4. Novel uses of nullable types

Before nullable types became a reality, I saw lots of people effectively asking for them, usually related to database access. That’s not the only use they can be put to, though. The patterns presented in this section are unconventional but can make code simpler. If you only ever stick to normal idioms of C#, that’s fine—this section might not be for you, and I have a lot of sympathy for that point of view. I usually prefer simple code over code that’s clever—but if a whole pattern provides benefits when it’s known, that sometimes makes the pattern worth learning. Whether you use these techniques is entirely up to you—but you may find that they suggest other ideas to use elsewhere in your code. Without further ado, let’s start with an alternative to the TryXXX pattern mentioned in section 3.3.3.

4.4.1. Trying an operation without using output parameters

The pattern of using a return value to say whether an operation worked, and an output parameter to return the real result, is becoming increasingly common in the .NET Framework. I have no issues with the aims—the idea that some methods are likely to fail to perform their primary purpose in non-exceptional circumstances is common sense. My one problem with it is that I’m not a huge fan of output parameters. There’s something slightly clumsy about the syntax of declaring a variable on one line, then immediately using it as an output parameter.

Methods returning reference types have often used a pattern of returning null on failure and non-null on success. It doesn’t work so well when null is a valid return value in the success case. Hashtable is an example of both of these statements, in a slightly ambivalent way. You see, null is a theoretically valid value in a Hashtable, but in my experience most uses of Hashtable never use null values, which makes it perfectly acceptable to have code that assumes that a null value means a missing key. One common scenario is to have each value of the Hashtable as a list: the first time an item is added for a particular key, a new list is created and the item added to it. Thereafter, adding another item for the same key involves adding the item to the existing list. Here’s the code in C# 1:

ArrayList list = hash[key];
if (list == null)
{
list = new ArrayList();
hash[key] = list;
}
list.Add(newItem);

Hopefully you’d use variable names more specific to your situation, but I’m sure you get the idea and may well have used the pattern yourself.[5] With nullable types, this pattern can be extended to value types—and in fact, it’s safer with value types, because if the natural result type is a value type, then a null value could only be returned as a result of failure. Nullable types add that extra Boolean piece of information in a nice general way with language support—so why not use them?

5 Wouldn’t it be great if Hashtable and Dictionary<TKey,TValue> could take a delegate to call whenever a new value was required due to looking up a missing key? Situations like this would be a lot simpler.

To demonstrate this pattern in practice and in a context other than dictionary lookups, I’ll use the classic example of the TryXXX pattern—parsing an integer. The implementation of the TryParse method in listing 4.5 shows the version of the pattern using an output parameter, but then we see the use of the version using nullable types in the main part at the bottom.

Listing 4.5. An alternative implementation of the TryXXX pattern

You may think there’s little to distinguish the two versions here—they’re the same number of lines, after all. But I believe there’s a difference in emphasis. The nullable version encapsulates the natural return value and the success or failure into a single variable. It also separates the doing from the testing, which puts the emphasis in the right place in my opinion. Usually, if I call a method in the condition part of an if statement, that method’s primary purpose is to return a Boolean value. Here, the return value is in some ways less important than the output parameter. When you’re reading code, it’s easy to miss an output parameter in a method call and be left wondering what’s actually doing all the work and magically giving the answer. With the nullable version, this is more explicit—the result of the method has all the information we’re interested in. I’ve used this technique in a number of places (often with more method parameters, at which point output parameters become even harder to spot) and believe it has improved the general feel of the code. Of course, this only works for value types.

Another advantage of this pattern is that it can be used in conjunction with the null coalescing operator—you can try to understand several pieces of input, stopping at the first valid one. The normal TryXXX pattern allows this using the short-circuiting operators, but the meaning isn’t nearly as clear when you use the same variable for two different output parameters in the same statement.

The next pattern is an answer to a specific pain point—the irritation and fluff that can be present when writing multitiered comparisons.

4.4.2. Painless comparisons with the null coalescing operator

I suspect you dislike writing the same code over and over again as much as I do. Refactoring can often get rid of duplication, but some cases resist refactoring surprisingly effectively. Code for Equals and Compare often falls firmly into this category in my experience.

Suppose you’re writing an e-commerce site and have a list of products. You may wish to sort them by popularity (descending), then price, then name—so that the five-star-rated products come first, but the cheapest five-star products come before the more expensive ones. If there are multiple products with the same price, products beginning with A are listed before products beginning with B. This isn’t a problem specific to e-commerce sites—sorting data by multiple criteria is a fairly common requirement in computing.

Assuming we have a suitable Product type, we can write the comparison with code like this in C# 1:

public int Compare (Product first, Product second)
{
// Reverse comparison of popularity to sort descending
int ret = second.Popularity.CompareTo(first.Popularity);
if (ret != 0)
{
return ret;
}
ret = first.Price.CompareTo(second.Price);
if (ret != 0)
{
return ret;
}
return first.Name.CompareTo(second.Name);
}

This assumes that we won’t be asked to compare null references, and that all of the properties will return non-null references too. We could use some up-front null comparisons and Comparer<T>.Default to handle those cases, but that would make the code even longer and more involved. The code could be shorter (and avoid returning from the middle of the method) if we rearranged it slightly, but the fundamental “compare, check, compare, check” pattern would still be present, and it wouldn’t be as obvious that once we have a nonzero answer, we’re done.

Ah... that last sentence is reminiscent of something else: the null coalescing operator. As we saw in section 4.3, if we have a lot of expressions separated by ??, then the operator will be repeatedly applied until it hits a non-null expression. Now all we have to do is work out a way of returning null instead of zero from a comparison. This is easy to do in a separate method that can also encapsulate the use of the default comparer. We can even have an overload to use a specific comparer if we want. We’ll also deal with the case where either of the Product references we’re passed is null. First, let’s look at the class implementing our helper methods, as shown in the following listing.

Listing 4.6. Helper class for providing partial comparisons
public static class PartialComparer
{
public static int? Compare<T>(T first, T second)
{
return Compare(Comparer<T>.Default, first, second);
}

public static int? Compare<T>(IComparer<T> comparer,
T first, T second)
{
int ret = comparer.Compare(first, second);
return ret == 0 ? new int?() : ret;
}

public static int? ReferenceCompare<T>(T first, T second)
where T : class
{
return first == second ? 0
: first == null ? -1
: second == null ? 1
: new int?();
}
}

The Compare methods in listing 4.6 are almost pathetically simple—when a comparer isn’t specified, the default comparer for the type is used, and all that happens to the comparison’s return value is that zero is translated to the null value.

 

Null Values and the Conditional Operator

You may have been surprised to see me use new int?() rather than null to return the null value in the second Compare method. But the conditional operator requires that its second and third operands either be of the same type, or that there be an implicit conversion from one to the other. That wouldn’t be the case with null, because the compiler wouldn’t know what type the value was meant to be—the language rules don’t take the overall aim of the statement (returning from a method with a return type of int?) into account when examining subexpressions. Other options include casting either operand to int? explicitly or using default(int?) for the null value. Basically, the important thing is to make sure that one of the operands is known to be an int? value.

 

The ReferenceCompare method uses another conditional operator—three of them, in fact. You may find this less readable than the (rather longer) equivalent code using if/else blocks—it depends on how comfortable you are with the conditional operator. I like it in terms of making the order of the comparisons clear. Also, this could easily have been a nongeneric method with two object parameters—but this form prevents you from accidentally using the method to compare value types via boxing. The method really is only useful with reference types, which is indicated by the type parameter constraint.

Even though this class is simple, it’s remarkably useful. We can now replace our previous product comparison with a neater implementation:

public int Compare(Product first, Product second)
{
return PC.ReferenceCompare(first, second) ??
// Reverse comparison of popularity to sort descending
PC.Compare(second.Popularity, first.Popularity) ??
PC.Compare(first.Price, second.Price) ??
PC.Compare(first.Name, second.Name) ??
0;
}

As you may have noticed, I’ve used PC rather than PartialComparer—this is solely for the sake of being able to fit the lines on the printed page. In real code, I’d use the full type name and still have one comparison per line. Of course, if you wanted short lines for some reason, you could specify a using directive to make PC an alias for PartialComparer—I just wouldn’t recommend it.

The final 0 indicates that if all of the earlier comparisons have passed, the two Product instances are equal. We could have just used Comparer<string>.Default. Compare(first.Name, second.Name) as the final comparison, but that would hurt the symmetry of the method.

This comparison plays nicely with nulls, is easy to modify, forms an easy pattern to use for other comparisons, and only compares as far as it needs to: if the prices are different, the names won’t be compared.

You may be wondering whether the same technique could be applied to equality tests, which often have similar patterns. There’s much less point in the case of equality, because after the nullity and reference equality tests, you can just use && to provide the desired short-circuiting functionality for Booleans. A method returning a bool? can be used to obtain an initial definitely equal, definitely not equal, or unknown result based on the references, though. The complete code of PartialComparer on this book’s website contains the appropriate utility method and examples of its use.

4.5. Summary

When faced with a problem, developers tend to take the easiest short-term solution, even if it’s not particularly elegant. That’s often the right decision—we don’t want to be guilty of overengineering, after all. But it’s always nice when a good solution is also the easiest solution.

Nullable types solve a specific problem that only had somewhat ugly solutions before C# 2. The features provided are just a better-supported version of a solution that was feasible but time consuming in C# 1. The combination of generics (to avoid code duplication), CLR support (to provide suitable boxing and unboxing behavior), and language support (to provide concise syntax along with convenient conversions and operators) makes the solution far more compelling than it was previously.

It so happens that in providing nullable types, the C# and Framework designers have made some other patterns available that just weren’t worth the effort before. We’ve looked at some of them in this chapter, and I wouldn’t be surprised to see more of them appearing over time.

So far our two new features (generics and nullable types) have addressed areas where in C# 1 we occasionally had to hold our noses due to unpleasant code smells. This pattern continues in the next chapter, where we discuss the enhancements to delegates. These form an important part of the subtle change of direction of both the C# language and the .NET Framework, toward a slightly more functional viewpoint. This emphasis is made even clearer in C# 3, so though we’re not looking at those features quite yet, the delegate enhancements in C# 2 act as a bridge between the familiarity of C# 1 and the style of idiomatic C# 3, which can often be radically different from earlier versions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.5.201