22

Could someone please be kind enough to explain why calling ToString() on an empty reference type causes an exception (which in my mind makes perfect sense, you cant invoke a method on nothing!) but calling ToString() on an empty Nullable(Of T) returns String.Empty? This was quite a surprise to me as I assumed the behaviour would be consistent across types.

Nullable<Guid> value = null;
Stock stock = null;
string result = value.ToString(); //Returns empty string
string result1 = stock.ToString(); //Causes a NullReferenceException
Maxim Gershkovich
  • 45,951
  • 44
  • 147
  • 243

6 Answers6

20

Nullable<T> is actually a struct that has some compiler support and implementation support to behave like a null without actually being null.

What you are seeing is the collision between the implementation allowing you to treat it naturally as a null as you would any other reference type, but allowing the method call to happen because the Nullable<T> isn't actually null, the value inside it is null.

Visually it looks like it shouldn't work, this is simply because you cannot see what is done in the background for you.

Other such visual trickery can be seen when you call an extension method on a null reference type... the call works (against visual expectation) because under the hood it is resolved into a static method call passing your null instance as a parameter.

How does a Nullable<T> type work behind the scenes?

Community
  • 1
  • 1
Adam Houldsworth
  • 63,413
  • 11
  • 150
  • 187
  • Depends on your visual expectations. Some would say that it's the fact that calling an non-virtual method that doesn't reference any fields or virtual methods *does* throw an exception that's against expectation, since there's no real reason for it except that C# forces it (.NET however doesn't force this). – Jon Hanna Aug 03 '12 at 11:46
  • @JonHanna Not really, I would expect that to fail based on the fact it *looks* like a instance-method call, regardless of the fact it can be redefined as a static without any issue. But I see your point that expectation will be tainted based on experience. In the OPs case I think it stands though. – Adam Houldsworth Aug 03 '12 at 11:48
  • Yes, but I wouldn't expect it to fail, because I wouldn't expect `class A{public void DoNothing(){}};/*...*/(A(null)).DoNothing();` to fail. There's no reason for it to fail, but C# made an extra effort to make it fail that other languages don't. Since I wouldn't expect it to fail as a non-extension method, I wouldn't expect it to fail as an extension method either, until I learn about C# having a special case of throwing on safe calls to methods of null objects. – Jon Hanna Aug 03 '12 at 12:06
  • 1
    @JonHanna You mean unlearn from other languages? To me it seems natural that trying to call an instance member on a null item regardless of its implementation will cause issues. I'm in the boat where my main language experience comes from C#, and based on language used by the OP in the question I still think my terminology stands. I'd also initially think they didn't make *extra* effort to make it fail, they just didn't make extra effort to make it work based on what `DoSomething` actually does. – Adam Houldsworth Aug 03 '12 at 12:08
  • One has to unlearn in all cases, and C#'s special-casing of this is one of those things (and isn't explicitly stated anywhere official that I've seen, though I could have just missed it). It doesn't help that there's no logical reason for it, it's "just because, that's why". – Jon Hanna Aug 03 '12 at 12:10
  • @JonHanna Lots of design decisions that people don't explain fall under the banner of "just because, that's why", just that this one is more obvious when you boil it down to what you are trying to do and what the compiler says in response. The confusion comes in when the code itself *looks* consistent yet is not consistent in behaviour - the OPs original source of confusion, expectations based on what it looks like will happen, but the implementation / compiler under the covers does something else. Your point falls into this category too. – Adam Houldsworth Aug 03 '12 at 12:12
  • It's hardly obvious that I run code that doesn't dereference anything and it throws a `NullReferenceException`. That's like eating too much and consequently breaking a finger. And they did make an extra effort to fail, they call non-virtual methods with `callvirt` rather than `call` precisely to make it fail (analogous to the metaphor, they by-passed my stomach through my finger-bone, just so it would break if I ate too much). – Jon Hanna Aug 03 '12 at 12:16
  • @JonHanna Fair enough, but I feel that extra effort is warranted as it is entirely based on the implementation as to whether the call succeeds or not. And when this is considered as the regular behaviour on instance members, what look like the same type of calls in C# code (extension methods or in the OPs case calling a method on what appears to be null) then the change in behaviour from what *appears* to be happening is the source of confusion because you cannot take appearance alone. I wouldn't like method calls to start working on nulls *just because* I don't reference any instance members. – Adam Houldsworth Aug 03 '12 at 12:23
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/14847/discussion-between-jon-hanna-and-adam-houldsworth) – Jon Hanna Aug 03 '12 at 12:27
  • @AdamHouldsworth what compiler support? Looking at the code behind Nullable, it appears to be easily implemented anywhere? – M Afifi Aug 03 '12 at 14:04
  • 1
    @MAfifi Null assignment doesn't come from the implementation (`Nullable b = null;`). Other than that I'm not really sure, all I know is the compiler chips in to allow it to do what it does. – Adam Houldsworth Aug 03 '12 at 14:11
  • @AdamHouldsworth: I consider the fact that there's no way to specify that an instance method should be legal on null instances to be a deficiency in .net language design; if .net is going to have the default value of `String` behave like an empty string in some contexts (probably trying to improve COM interoperation), it should be consistent. Of course, that might require that `Nullable` lose the silly `struct` constraint, but that would be a good thing anyway. – supercat Aug 27 '12 at 22:17
  • @supercat Yeah I see what you're saying. But for it to be safe the compiler would have to guard against member variable access, so it may as well be static. That said, static method calls on instances (null or otherwise) could be a good halfway house. – Adam Houldsworth Aug 28 '12 at 06:06
  • @AdamHouldsworth: I'm not quite sure what you mean. Attempting to access a member variable of a null reference will trigger a null-reference exception regardless of whether the compiler allows calls to non-virtual instance members of null references. The difference would be that something like `String.Length` could check `if (this==null) return 0;` without throwing a null reference exception (thus allowing `String` to behave consistently like a value type which defaults to an empty string). – supercat Aug 28 '12 at 15:23
  • @AdamHouldsworth: On a related note, I'd probably have `Object.Equals` be a non-virtual wrapper around a virtual `Equals` function which would check for reference equality or nullity before calling the virtual method; such a method should be short enough to be inlined; the improvement in the reference-equals case should likely offset the cost of doing the extra testing before invoking the virtual method. – supercat Aug 28 '12 at 15:26
6

Nullable is a value type and the assignment to null causes it to be initialized with Value=null and HasValue=false.

Further, Nullable.ToString() is implement as follows:

public override string ToString()
{
    if (!this.HasValue)
    {
        return "";
    }
    return this.value.ToString();
}

So what you are seeing is expected.

logicnp
  • 5,796
  • 1
  • 28
  • 32
  • I think the question is more "how is that ToString called at all, since value is null?". –  Aug 03 '12 at 07:56
  • Value doesn't get initialized to null, it gets initialized to default(T). Value is a value type, it can't be null. Actually Value is a property so it doesn't get initialized to anything, but its backing field gets initialized to default(T), although you will never see that value because the property will throw an exception. – Mike Marynowski Dec 01 '12 at 18:23
4

It is a bit tricky with nullable types. When you set it to null it is actualy not null cause it is not reference type (it is value type). When you initialize such variable with null it creates new sctructure instance where HasValue property is false and it's Value is null, so when you call ToString method it works well on structure instance.

user854301
  • 5,383
  • 3
  • 28
  • 37
  • It is actually null. The concept of null and nothingness predates the concept of references in computing. That we may represent this concept by a reference that doesn't refer, it's not the only possible way. Nullable value-types are another. – Jon Hanna Aug 03 '12 at 11:32
  • @JonHanna: You're half right. True, the concept of nullity in general predates the concept of references. But user854301 here is saying that when you set a nullable type **in C#** to `null` it is not actually `null` -- which is 100% correct. The word `null` (especially when written in a code font) has a specific meaning in the context of C# which is separate from (although related to) the concept of nullity in general. It's like saying that `42.0` is an integer -- it certainly *is*, in a general existential way, but from a language point of view it's a `double` instead. – Daniel Pryden Aug 03 '12 at 17:19
  • @DanielPryden if we do `double? d = null;` we've assigned null to `d`. If we test `double == null` then we get `true`. As far as the C# meaning of `null` goes, `d` is `null`. It's not however a *null reference* because while it's null, it's not a reference. – Jon Hanna Aug 03 '12 at 22:36
  • Equals method is override for nullable types. All of this stuff were design to support communication with db (point of view) not to simply add 'another type'. – user854301 Aug 04 '12 at 21:19
  • @user854301 DB work is far from their only use (DBs have nullable for a reason, and that reason applies elsewhere). – Jon Hanna Aug 07 '12 at 14:42
1

The exception raised by calling default(object).ToString() is called NullReferenceException for a reason, it's calling a method on a null reference. default(int?) on the other hand, is not a null reference, because it's not a reference; it is a value type with a value that is equivalent to null.

The big practical point, is that if this was done, then the following would fail:

default(int?).HasValue // should return false, or throw an exception?

It would also screw-up the way we have some ability to mix nullables and non-nullables:

((int?)null).Equals(1) // should return false, or throw an exception?

And the following becomes completely useless:

default(int?).GetValueOrDefault(-1);

We could get rid of HasValue and force comparison with null, but then what if the equality override of the value-type that is made nullable can return true when compared to null in some cases. That may not be a great idea, but it can be done and the language has to cope.

Let's think back to why nullable types are introduced. The possibility that a reference type can be null, is inherent in the concept of reference types unless effort is taken to enforce non-nullability: Reference types are types that refer to something, and that implies the possibility of one not referring to anything, which we call null.

While a nuisance in many cases, we can make use of this in a variety of cases, such as representing "unknown value", "no valid value" and so on (we can use it for what null means in databases, for example).

At this point, we've given null a meaning in a given context, beyond the simple fact that a given reference doesn't refer to any object.

Since this is useful, we could therefore want to set an int or DateTime to null, but we can't because they aren't types that refer to something else, and hence can't be in a state of not referring to anything any more than I as a mammal can lose my feathers.

The nullable types introduced with 2.0 give us a form of value types that can have the semantic null, through a different mechanism than that of reference types. Most of this you could code yourself if it didn't exist, but special boxing and promotion rules allow for more sensible boxing and operator use.

Okay. Now let's consider why NullReferenceExceptions happen in the first place. Two are inevitable, and one was a design decision in C# (and doesn't apply to all of .NET).

  1. You try to call a virtual method or property, or access a field on a null reference. This has to fail, because there's no way to look up what override should be called, and no such field.
  2. You call a non-virtual method or property on a null reference which in turn calls a virtual method or property, or accesses a field. This is obviously a variant on point one, but the design decision we're coming to next has the advantage of guaranteeing this fails at the start, rather than part-way through (which could be confusing and have long-term side-effects).
  3. You call a non-virtual method or property on a null reference which does not call a virtual method or property, or access a field. There's no inherent reason why this should not be allowed, and some languages allow it, but in C# they decided to use callvirt rather than call to force a NullReferenceException for the sake of consistency (can't say I agree, but there you go).

None of these cases apply in any way to a nullable value type. It is impossible to put a nullable value type into a condition in which there is no way to know which field or method override to access. The whole concept of NullReferenceException just doesn't make sense here.

In all, not throwing a NullReferenceException is consistent with the other types - types through it if and only if a null reference is used.

Note that there is a case where calling on a null nullable-type throws, it does so with GetType(), because GetType() is not virtual, and when called on a value-type there is always an implied boxing. This is true of other value types so:

(1).GetType()

is treated as:

((object)1).GetType()

But in the case of nullable types, boxing turns those with a false HasValue into null, and hence:

default(int?).GetType()

being treated as:

((object)default(int?)).GetType()

which results in GetType() being called on a null object, and hence throwing.

This incidentally brings us to why not faking NullReferenceType was the more sensible design decision - people who need that behaviour can always box. If you want it to through then use ((object)myNullableValue).GetString() so there's no need for the language to treat it as a special case to force the exception.

EDIT

Oh, I forgot to mention the mechanics behind NullReferenceException.

The test for NullReferenceException is very cheap, because it mostly just ignores the problem, and then catches the exception from the OS if it happens. In other words, there is no test.

See What is the CLR implementation behind raising/generating a null reference exception? and note how none of that would work with nullable value types.

Community
  • 1
  • 1
Jon Hanna
  • 110,372
  • 10
  • 146
  • 251
  • IMHO, .net should have provided a means by which instance methods could be explicitly tagged so as to be callable on null instances; while that would not be expected behavior for mutable reference types, such a design would have allowed immutable reference types like `String` to behave like value types with a meaningful default value. – supercat Oct 30 '12 at 19:02
0

If you investigate Nullable<> definition, there is an override ToString definition. In this function, ToString is overriden to return String.Empty.

    // Summary:
    //     Returns the text representation of the value of the current System.Nullable<T>
    //     object.
    //
    // Returns:
    //     The text representation of the value of the current System.Nullable<T> object
    //     if the System.Nullable<T>.HasValue property is true, or an empty string ("")
    //     if the System.Nullable<T>.HasValue property is false.
    public override string ToString();

On the otherhand, Stock is a custom class, which I assume ToString is not overriden. Thus it returns NullReferenceException since it uses default behaviour.

daryal
  • 14,643
  • 4
  • 38
  • 54
  • 1
    This answer misses the key fact that Nullable is a value type. Calling ToString on any reference type will throw a NullReferenceException, whether the type has a ToString override or not. – phoog Aug 03 '12 at 08:20
  • @phoog This answer directly or indirectly does not imply anything related with reference types. Sorry if I can not get the point. I also stated that if it is "a class" and the value is "null"; then the default behaviour returns NullReferenceException. – daryal Aug 03 '12 at 10:59
  • Your answer implies that adding a `ToString()` override to the `Stock` class would change the program's behavior, but that's not correct. The NullReferenceException occurs with `Stock` because `Stock` is a reference type, and it does not occur with `Nullable` because `Nullable<>` is a value type. The presence or absence of a `ToString()` override has no bearing on whether a NullReferenceException is thrown. In other words, the problem with the answer is precisely that it does not mention the issue of value types vs. reference types. – phoog Aug 28 '12 at 14:07
0

As per MSDN Remarks

Guid.ToSTring() method Returns a string representation of the value of this Guid instance, according to the provided format specifier.

As per MSDN Remarks on Nullable

A type is said to be nullable if it can be assigned a value or can be assigned null, which means the type has no value whatsoever. Consequently, a nullable type can express a value, or that no value exists. For example, a reference type such as String is nullable, whereas a value type such as Int32 is not. A value type cannot be nullable because it has enough capacity to express only the values appropriate for that type; it does not have the additional capacity required to express a value of null.

HatSoft
  • 11,077
  • 3
  • 28
  • 43