ross.bradbury (9) [Avatar] Offline
#1
Today at work we encountered an interesting situation. It was not expected and we couldn't find it documented anywhere until I stumbled upon 6.1.7 Boxing conversions in the C# Language Specification Version 3.0. Was this discussed in C# In Depth anywhere? If it was I overlooked it.

Since none of these sections seemed to apply:
6.1.4 Implicit nullable conversions
6.1.5 Null literal conversions
6.1.6 Implicit reference conversions

Is it the boxing conversion that allows this code example to work? If so, this means that any code defined in the implicit conversion will simply be bypassed if the Nullable<int> being converted doesn't have a value?


------------------------------------------------
BEGIN EXAMPLE
------------------------------------------------


class ReferenceType
{
readonly object _data;

private ReferenceType(object data)
{
this._data = data;
}

public override string ToString()
{
if(ReferenceEquals(_data, null))
return "Data is <null>";
else
return "Data is " + _data.ToString();
}

static public implicit operator ReferenceType(int data)
{
return new ReferenceType(data);
}
}

static class Program
{
public static void Main()
{
ReferenceType ofCourse = 2;
int? x = 3;
ReferenceType makesSense = x;

int? y = null;
ReferenceType unexpected = y;

System.Console.WriteLine(ofCourse);
System.Console.WriteLine(makesSense);
System.Console.WriteLine(unexpected == null ? "Wow, the variable is null" : unexpected.ToString());
}
}
jon.skeet (448) [Avatar] Offline
#2
Re: Implicit conversion of T? to R where R is a reference type
Ross: that's some crazy stuff going on there...

I don't understand why you're allowed to use the conversion *implicitly* from int? at all. I'd have expected an explicit cast to int to be required.

It looks like the generated code is effectively:

ReferenceType makesSense = (x==null) ? null : (ReferenceType)(x.Value);
ditto for "unexpected".

(I believe it actually calls GetValueOrDefault, but it's easier to understand as Value.)

That explains why the compiled code executes as it does, but not why it compiles in the first place.

I certainly don't cover this, given that I didn't think it was legal...
It's possible that this is a compiler bug, but I wouldn't like to say for sure. Would you like me to mail Eric Lippert about it?
ross.bradbury (9) [Avatar] Offline
#3
Re: Implicit conversion of T? to R where R is a reference type
It looks like it is as specified in 6.1.7 Boxing conversions in the C# Language Specification Version 3.0.


From the spec:
6.1.7 Boxing conversions
A boxing conversion permits a value-type to be implicitly converted to a reference type. A boxing conversion exists from any non-nullable-value-type to object, to System.ValueType and to any interface-type implemented by the non-nullable-value-type. Furthermore an enum-type can be converted to the type System.Enum.

A boxing conversion exists from a nullable-type to a reference type, if and only if a boxing conversion exists from the underlying non-nullable-value-type to the reference type.

Boxing a value of a non-nullable-value-type consists of allocating an object instance and copying the value-type value into that instance. A struct can be boxed to the type System.ValueType, since that is a base class for all structs (§11.3.2).

Boxing a value of a nullable-type proceeds as follows:
• If the source value is null (HasValue property is false), the result is a null reference of the target type.
• Otherwise, the result is a reference to a boxed T produced by unwrapping and boxing the source value.

Boxing conversions are described further in §4.3.1.
jon.skeet (448) [Avatar] Offline
#4
Re: Implicit conversion of T? to R where R is a reference type
But what do you think is being boxed? I can't see anything that *could* be boxed. We're never using a value type as a reference type here.
ross.bradbury (9) [Avatar] Offline
#5
Re: Implicit conversion of T? to R where R is a reference type
I still don't 100% agree with the behavior, but it does seem to be working as the specification says a boxing conversion works (in 6.1.7, not in 4.3.1 of the spec.)

In this case, an implicit conversion from int to ReferenceType is defined. The easiest way for me to think of this is that the that the the compiler also defines a lifted conversion from int? to ReferenceType, just exactly as it would if ReferenceType were a value type if you were to consider that when R is a reference type, R is a nullable type since it can be null, except for some reason instead of calling this a lifted conversion they called it a boxing conversion.

6.1.4 Implicit nullable conversions says
If the nullable conversion is from S? to T?:
o If the source value is null (HasValue property is false), the result is the null value of type T?.
o Otherwise, the conversion is evaluated as an unwrapping from S? to S, followed by the underlying conversion from S to T, followed by a wrapping (§4.1.10) from T to T?.

This seems to be happening exactly the same way when T is a reference type (except there is not really a T?).


If the int? has a value, then that value is unwrapped, and passed to the user-defined implicit conversion method. If the int? does not have a value, then it is boxed to object (which would just be null) and then cast to ReferenceType since null can be cast to any type - the user-defined implicit conversion code is bypassed.

Given my reasoning, I then expected that if I were to define an implicit conversion from String that the conversion code would be bypassed if I passed in a null, but it executed!

------------------
BEGIN EXAMPLE
------------------



class ReferenceType
{
readonly object _data;

private ReferenceType(object data)
{
this._data = data;
}

public override string ToString()
{
if(ReferenceEquals(_data, null))
return "Data is <null>";
else
return "Data is " + _data.ToString();
}

static public implicit operator ReferenceType(int data)
{
System.Console.WriteLine("Converting an int to ReferenceType");
return new ReferenceType(data);
}

static public implicit operator ReferenceType(string data)
{
System.Console.WriteLine("Converting a string to ReferenceType");
return new ReferenceType(data);
}
}

static class Program
{
public static void Main()
{
ReferenceType aString = "Hello";
Write(aString);

string n = null;
ReferenceType aNullString = n;
Write(aNullString);

ReferenceType ofCourse = 2;
Write(ofCourse);

int? x = 3;
ReferenceType makesSense = x;
Write(makesSense);

int? y = null;
ReferenceType unexpected = y;
Write(unexpected);
}

static void Write(object o)
{
System.Console.WriteLine(ReferenceEquals(o, null) ? "The variable is null" : o.ToString());
}
}



------------------
OUTPUT
------------------
Converting a string to ReferenceType
Data is Hello
Converting a string to ReferenceType
Data is <null>
Converting an int to ReferenceType
Data is 2
Converting an int to ReferenceType
Data is 3
The variable is null
jon.skeet (448) [Avatar] Offline
#6
Re: Implicit conversion of T? to R where R is a reference type
> I still don't 100% agree with the behavior, but it
> does seem to be working as the specification says a
> boxing conversion works (in 6.1.7, not in 4.3.1 of
> the spec.)

I really don't think so - because no boxing is involved anywhere.

> In this case, an implicit conversion from int to
> ReferenceType is defined.

Agreed.

> The easiest way for me to
> think of this is that the that the the compiler also
> defines a lifted conversion from int? to
> ReferenceType, just exactly as it would if
> ReferenceType were a value type if you were to
> consider that when R is a reference type, R
> is a nullable type since it can be
> null, except for some reason instead of calling this
> a lifted conversion they called it a boxing
> conversion
.

No, a boxing conversion only ever applies to value types. Look at 6.1.7:

<quote>
A boxing conversion permits a value-type to be implicitly converted to a reference type.
</quote>

ReferenceType isn't a value type, so boxing conversions are irrelevant.

> 6.1.4 Implicit nullable conversions says
> If the nullable conversion is from S? to T?:

You've missed an important bit here, which is defining S and T:

<quote>
For each of the predefined implicit identity and numeric conversions that convert from a non-nullable value type S to a non-nullable value type T
</quote>

ReferenceType is not a non-nullable value type, so it can't be used as S or T in the rest of the clause.

<snip>

> If the int? has a value, then that value is
> unwrapped, and passed to the user-defined implicit
> conversion method. If the int? does not have a
> value, then it is boxed to object (which would just
> be null) and then cast to ReferenceType since null
> can be cast to any type - the user-defined
> implicit conversion code is bypassed
.

A null reference known to the compiler as just object can't be converted implicitly to ReferenceType, however.

For instance, you can do:

int? x = null;
object y = x;
ReferenceType z = (ReferenceType) y;

but you can't do:

int? x = null;
object y = x;
ReferenceType z = y;

If there's an explanation anywhere, it should be in 6.4.4. In this case, we end up with:

S = int?,
S0 = int,
T = ReferenceType,
T0 = ReferenceType.
D = { int, ReferenceType }

Now, I can't see any types encompassing S where there's a conversion to a type encompassed by T. That would make U empty, and a compile-time error.


Basically, either my understanding is shot or you've discovered a compiler error.

I'll ask Eric/Mads - I'm intrigued now.

Jon
jon.skeet (448) [Avatar] Offline
#7
Re: Implicit conversion of T? to R where R is a reference type
Have heard back from Eric - it's a compiler bug. It's unlikely to be fixed because it would break code, but it's illegal in spec terms.

Once again, Eric rocks...

Jon
ross.bradbury (9) [Avatar] Offline
#8
Re: Implicit conversion of T? to R where R is a reference type
Thanks for finding out it is a compiler bug, but I still don't understand one thing.

<quote>
A boxing conversion permits a value-type to be implicitly converted to a reference type.
</quote>

int? is a value type and ReferenceType is a reference type and I'm going from the value-type to the reference type, so why isn't this a boxing conversion?
jon.skeet (448) [Avatar] Offline
#9
Re: Implicit conversion of T? to R where R is a reference type
> Thanks for finding out it is a compiler bug, but I
> still don't understand one thing.
>
> <quote>
> A boxing conversion permits a value-type to be
> implicitly converted to a reference type.
> </quote>
>
> int? is a value type and ReferenceType is a reference
> type and I'm going from the value-type to the
> reference type, so why isn't this a boxing conversion?

Because boxing conversions only happen in certain cases. Not *every* conversion of a value type into a reference type is a boxing conversion - although cases like yours are pretty rare.

Jon
ross.bradbury (9) [Avatar] Offline
#10
Re: Implicit conversion of T? to R where R is a reference type
Thanks for your time on this. Perhaps this is something worthy of noting on the book's website as a quirk of nullables. I turned to your book as my first resource to figure out what was going on, and then tried to find it in the spec (second time I've really tried to use the spec to understand C#.) Your book has been a really great tool to have at my disposal, great job!

I don't imagine this is a common scenario - in all of our code we only have one class that has implicit conversions, but it has one from every primitive type. Some day I'll have to read more about this in the C# Language Specification. For now, we may have to make some modifications to that class to handle this behavior.

One additional interesting thing is that ReSharper says this code will not compile because there isn't an implicit conversion defined but the compiler happens to say otherwise, which is how this whole discussion got started in the first place.
jon.skeet (448) [Avatar] Offline
#11
Re: Implicit conversion of T? to R where R is a reference type
> Thanks for your time on this. Perhaps this is
> something worthy of noting on the book's website as a
> quirk of nullables.

Will do, good idea. (I wonder how many people will actually read the notes on the web site? Personally I love 'em more than some of the text in the book, but...)

> I turned to your book as my
> first resource to figure out what was going on, and
> then tried to find it in the spec (second time I've
> really tried to use the spec to understand C#.) Your
> book has been a really great tool to have at my
> disposal, great job!

Sounds like I really need to get on with the language spec map then. I've been putting it off as slightly less important than other things, on the grounds that no-one else reads the spec. If people are actually reading the spec, I'd better help them smilie

> I don't imagine this is a common scenario - in all of
> our code we only have one class that has implicit
> conversions, but it has one from every primitive
> type.

Shudder. Can't say I'm a big fan of implicit conversions. But hey...

> Some day I'll have to read more about this in
> the C# Language Specification. For now, we may have
> to make some modifications to that class to handle
> this behavior.

I suspect you'll find you could just add a conversion from int?. I haven't tried it though.

> One additional interesting thing is that ReSharper
> says this code will not compile because there isn't
> an implicit conversion defined but the compiler
> happens to say otherwise, which is how this whole
> discussion got started in the first place.

Interesting. ReSharper presumably has to duplicate a lot of the work of the compiler - and now it has to duplicate their bugs too! (I think ReSharper has issues with InternalsVisibleTo between signed assemblies, too. Must mail them about that some time, if it's still an issue when 4.0 comes out.)

(I should try it on a few other interesting quirks... it's amazing what you can find in the nooks and crannies if you look hard enough.)

Thanks for your posts - very interesting!
Jon