33

I encountered some interesting behavior in the interaction between Nullable and implicit conversions. I found that providing an implicit conversion for a reference type from a value type it permits the Nullable type to be passed to a function requiring the reference type when I instead expect a compilation error. The below code demonstrates this:

static void Main(string[] args)
{
    PrintCatAge(new Cat(13));
    PrintCatAge(12);
    int? cat = null;
    PrintCatAge(cat);
}

private static void PrintCatAge(Cat cat)
{
    if (cat == null)
        System.Console.WriteLine("What cat?");
    else
        System.Console.WriteLine("The cat's age is {0} years", cat.Age);
}

class Cat
{
    public int Age { get; set; }
    public Cat(int age)
    {
        Age = age;
    }

    public static implicit operator Cat(int i)
    {
        System.Console.WriteLine("Implicit conversion from " + i);
        return new Cat(i);
    }
}

Output:

The cat's age is 13 years
Implicit conversion from 12
The cat's age is 12 years
What cat?

If the conversion code is removed from Cat then you get the expected errors:

Error 3 The best overloaded method match for 'ConsoleApplication2.Program.PrintCatAge(ConsoleApplication2.Program.Cat)' has some invalid arguments

Error 4 Argument 1: cannot convert from 'int?' to 'ConsoleApplication2.Program.Cat

If you open the executable with ILSpy the code that was generated is as follows

int? num = null;
Program.PrintCatAge(num.HasValue ? num.GetValueOrDefault() : null);

In a similar experiment I removed the conversion and added an overload to PrintCatAge that takes an int (not nullable) to see if the compiler would perform a similar operation, but it does not.

I understand what is happening, but I don't understand the justification for it. This behavior is unexpected to me and seems odd. I did not have any success finding any reference to this behavior on MSDN in the documentation for conversions or Nullable<T>.

The question I pose then is, is this intentional and is there a explanation why this is happening?

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
Thomas
  • 3,603
  • 1
  • 18
  • 23
  • 1
    Interesting! A similarly unexpected behavior is ((object)(int?)null) == null being true. A nullable int can get boxed to a null reference. – usr Apr 16 '12 at 23:15
  • Casting a null like so: `(int?)null` gets optimized away. However, (null as int?) causes the actual creation of an int? temporary variable. Either way, I'd expect `null == null` in these cases. – Thomas Apr 16 '12 at 23:20
  • 2
    @usr: That is expected; the CLR has special code for boxing nullables. It is actually impossible to get a boxed `Nullable` at all. – SLaks Apr 17 '12 at 00:24
  • The equivalent VB.NET code does complain at compile time on `PrintCatAge(cat)`: "Option Strict On disallows implicit conversions from 'Integer?' to 'UserQuery.Cat'." And with `Option Strict Off` at runtime it fails with "InvalidOperationException: Nullable object must have a value.". – Mark Hurd Sep 21 '12 at 04:05

1 Answers1

29

I said earlier that (1) this is a compiler bug and (2) it is a new one. The first statement was accurate; the second was me getting confused in my haste to get to the bus on time. (The bug I was thinking of that is new to me is a much more complicated bug involving lifted conversions and lifted increment operators.)

This is a known compiler bug of long standing. Jon Skeet first brought it to my attention some time ago and I believe there's a StackOverflow question about it somewhere; I do not recall where offhand. Perhaps Jon does.

So, the bug. Let's define a "lifted" operator. If an operator converts from a non-nullable value type S to a non-nullable value type T then there is also a "lifted" operator that converts from S? to T?, such that a null S? converts to a null T? and a non-null S? converts to T? by unwrapping S? to S, converting S to T, and wrapping T to T?.

The specification says that (1) the only situation in which there is a lifted operator is when S and T are both non-nullable value types, and (2) that the lifted and non-lifted conversion operators are both considered as to whether they are applicable candidates for the conversion and if both applicable, then the source and target types of the applicable conversions, lifted or unlifted, are used to determine the best source type, best target type, and ultimately, best conversion of all the applicable conversions.

Unfortunately, the implementation thoroughly violates all of these rules, and does so in a way that we cannot change without breaking many existing programs.

First off, we violate the rule about the existence of lifted operators. A lifted operator is considered by the implementation to exist if S and T are both non-nullable value types, or if S is a non-nullable value type and T is any type to which a null could be assigned: reference type, nullable value type, or pointer type. In all those cases we produce a lifted operator.

In your particular case, we lift to nullable by saying that we convert a nullable type to the reference type Cat by checking for null. If the source is not null then we convert normally; if it is, then we produce a null Cat.

Second, we violate thoroughly the rule about how to determine the best source and target types of applicable candidates when one of those candidates is a lifted operator, and we also violate the rules about determining which is the best operator.

In short, it is a big mess that cannot be fixed without breaking real customers, and so we will likely enshrine the behaviour in Roslyn. I will consider documenting the exact behaviour of the compiler in my blog at some point, but I would not hold my breath while waiting for that day if I were you.

And of course, many apologies for the errors.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 2
    I thought only Jon Skeet answered questions while commuting. – BoltClock Apr 16 '12 at 23:31
  • Big difference is that Jon can keep posting while commuting. All those video cameras have wifi hotspots too? No idea, really. – Hans Passant Apr 16 '12 at 23:35
  • Great! Well, not the fact that there is a bug, but at least I don't have to try to figure out why this behavior makes sense :) – Thomas Apr 16 '12 at 23:36
  • Thanks for the thorough explanation. It proved to be a very difficult behavior to search for, so I am not surprised I may have missed an existing explanation about this elsewhere.I stumbled across the situation in some production code while evaluating a related error report and was thoroughly perplexed. – Thomas Apr 17 '12 at 00:38
  • 2
    @Thomas: You're welcome. The keywords you want to use when searching the web or the specification are things like "C# lifted nullable user-defined conversion operator". – Eric Lippert Apr 17 '12 at 00:42
  • @EricLippert That's a mouthful! – Thomas Apr 17 '12 at 00:54
  • That's the bug first report I guess http://stackoverflow.com/questions/6256847/curious-null-coalescing-operator-custom-implicit-conversion-behaviour – Lukasz Madon Apr 17 '12 at 02:07
  • @lukas: Nope, that's yet another bug. That bug only affects conversions that occur in the coalescing operator. – Eric Lippert Apr 17 '12 at 05:09
  • Doesn't this rather make the specification 'advisory', and not really a specification- given that the Microsoft compiler is, in practice the one that almost everyone uses- to a large extent the compiler-behaviour *is* the spec.. The specification will say 'X', as that's enshrined, however, the compiler will do continue to do 'Y' as it's business issue not to fix the bug. And anyone who comes across the bug will be confused :) – nicodemus13 May 11 '12 at 14:00