Question

I have the following code:

public struct Num<T>
{
    private readonly T _Value;

    public Num(T value)
    {
        _Value = value;
    }

    static public explicit operator Num<T>(T value)
    {
        return new Num<T>(value);
    }
}

...
double d = 2.5;
Num<byte> b = (Num<byte>)d;

This code compiles, and it surprises my. The explicit convert should only accept a byte, not a double. But the double is accepted somehow. When I place a breakpoint inside the convert, I see that value is already a byte with value 2. By casting from double to byte should be explicit.

If I decompile my EXE with ILSpy, I see the next code:

double d = 2.5;
Program.Num<byte> b = (byte)d;

My question is: Where is that extra cast to byte coming from? Why is that extra cast place there? Where did my cast to Num<byte> go to?

EDIT
The struct Num<T> is the entire struct, so no more hidden extra methods or operators.

EDIT
The IL, as requested:

IL_0000: nop
IL_0001: ldc.r8 2.5 // Load the double 2.5.
IL_000a: stloc.0
IL_000b: ldloc.0
IL_000c: conv.u1 // Once again the explicit cast to byte.
IL_000d: call valuetype GeneriCalculator.Program/Num`1<!0> valuetype GeneriCalculator.Program/Num`1<uint8>::op_Explicit(!0) 
IL_0012: stloc.1
IL_0013: ret
Was it helpful?

Solution

Let's take a step back and ask some clarifying questions:

Is this program legal?

public struct Num<T>
{
    private readonly T _Value;

    public Num(T value)
    {
        _Value = value;
    }

    static public explicit operator Num<T>(T value)
    {
        return new Num<T>(value);
    }
}

class Program
{
    static void Main()
    {
        double d = 2.5;
        Num<byte> b = (Num<byte>)d;
    }
}

Yes.

Can you explain why the cast is legal?

As Ken Kin pointed out, I explain that here:

Chained user-defined explicit conversions in C#

Briefly: a user-defined explicit conversion may have a built-in explicit conversion inserted on "both ends". That is, we can insert an explicit conversion either from the source expression to the parameter type of the user-defined conversion method, or from the return type of the user-defined conversion method to the target type of the conversion. (Or, in some rare cases, both.)

In this case we insert a built-in explicit conversion to the parameter type, byte, so your program is the same as if you'd written:

        Num<byte> b = (Num<byte>)(byte)d;

This is desirable behaviour. A double may be explicitly converted to byte, so a double may also be explicitly converted to Num<byte>.

For a complete explanation, read section 6.4.5 "User-defined explicit conversions" in the C# 4 specification.

Why does the IL generated call op_Implicit instead of op_Explicit?

It doesn't; the question is predicated on a falsehood. The above program generates:

  IL_0000:  nop
  IL_0001:  ldc.r8     2.5
  IL_000a:  stloc.0
  IL_000b:  ldloc.0
  IL_000c:  conv.u1
  IL_000d:  call       valuetype Num`1<!0> valuetype Num`1<uint8>::op_Explicit(!0)
  IL_0012:  stloc.1
  IL_0013:  ret

You're looking at an old version of your program probably. Do a clean rebuild.

Are there other situations in which the C# compiler silently inserts an explicit conversion?

Yes; in fact this is the second time that question has come up today. See

C# type conversion inconsistent?

OTHER TIPS

First off, let's have a look at Mr. Lippert's blog:

Chained user-defined explicit conversions in C#

The compiler will sometimes1 insert the explicit conversion for us:

  • Part of the blogpost:

    ...

    When a user-defined explicit cast requires an explicit conversion on either the call side or the return side, the compiler will insert the explicit conversions as needed.

    The compiler figures that if the developer put the explicit cast in the code in the first place then the developer knew what they were doing and took the risk that any of the conversions might fail.

    ...

As this question, it's just one of the time of sometimes. What explicit conversion the compiler inserted is like we writing in the following code:

  • Test generic method with explicit conversion

    public static class NumHelper {
        public static Num<T> From<T>(T value) {
            return new Num<T>(value);
        }
    }
    
    public partial class TestClass {
        public static void TestGenericMethodWithExplicitConversion() {
            double d=2.5;
            Num<byte> b=NumHelper.From((byte)d);
        }
    }
    

    and the generated IL of the test method is:

    IL_0000: nop
    IL_0001: ldc.r8 2.5
    IL_000a: stloc.0
    IL_000b: ldloc.0
    IL_000c: conv.u1
    IL_000d: call valuetype Num`1<!!0> NumHelper::From<uint8>(!!0)
    IL_0012: stloc.1
    IL_0013: ret
    

Let's go one step back, to see the test of explicit operator as your question:

  • Test explicit operator

    public partial class TestClass {
        public static void TestExplicitOperator() {
            double d=2.5;
            Num<byte> b=(Num<byte>)d;
        }
    }
    

    and you've already seen the IL before:

    IL_0000: nop
    IL_0001: ldc.r8 2.5
    IL_000a: stloc.0
    IL_000b: ldloc.0
    IL_000c: conv.u1
    IL_000d: call valuetype Num`1<!0> valuetype Num`1<uint8>::op_Explicit(!0)
    IL_0012: stloc.1
    IL_0013: ret
    

Do you notice that they are quite similar? The difference is that the parameter !0 is a generic parameter in a type definition of your original code, and !!0 in the generic method test, is generic parameter in a method definition. You might want to have a look at chapter §II.7.1 of the specification Standard ECMA-335.

However, the most point here, is they both get into the type <uint8>(which is byte) of the generic definition; and as I mentioned above, according to Mr. Lippert's blogpost told us that the compiler sometimes inserts the explicit conversion when you did specify them explicitly!

Finally, as you suppose this is strange behavior of the compiler, and let me guess what is possibly you would think the compiler should do:

  • Test generic method by specifying type parameter:

    public partial class TestClass {
        public static void TestGenericMethodBySpecifyingTypeParameter() {
            double d=2.5;
            Num<byte> b=NumHelper.From<byte>(d);
        }
    }
    

Did I guess correctly? Anyway, what we are interested here, again, the IL. And I cannot wait to see the IL, it is:

0PC4l.png

Ooooops .. seem it's not as what the compiler thought an explicit operator would behaves like.

For conclution, when we specified the conversion explicitly, it's pretty semantic to say that we are expecting to convert one thing to another, the compiler deduced that and insert the obvious necessary convertion of the involved types; and once it found the type involved is not legal to be converted, it complains, just as we specified a more simply wrong conversion, such as (String)3.1415926 ...

Wish it's now more helpful without losing the correctness.

1: It's my personal expression of sometimes, in the blogpost actually is said as needed.


The following is some test for contrasting, when one possibly expect to convert the type with a existent explicit operator; and I put some comments in the code for describing each case:

double d=2.5;
Num<byte> b=(Num<byte>)d; // explicitly
byte x=(byte)d; // explicitly, as the case above

Num<byte> y=d; // no explicit, and won't compile

// d can be `IConvertible`, compiles
Num<IConvertible> c=(Num<IConvertible>)d;

// d can be `IConvertible`; 
// but the conversion operator is explicit, requires specified explicitly
Num<IConvertible> e=d;

// d cannot be `String`, won't compile even specified explicitly
Num<String> s=(Num<String>)d;

// as the case above, won't compile even specified explicitly
String t=(String)d; 

Maybe it's easier to understand.

The relevant section of the C# standard (ECMA-334) is §13.4.4. I have emphasized in bold the parts relevant to your code above.

A user-defined explicit conversion from type S to type T is processed as follows:

[omitted]

  • Find the set of applicable conversion operators, U. This set consists of the user-defined and, if S and T are both nullable, lifted implicit or explicit conversion operators (§13.7.3) declared by the classes or structs in D that convert from a type encompassing or encompassed by S to a type encompassing or encompassed by T. If U is empty, there is no conversion, and a compile-time error occurs.

The terms encompassing and encompassed by are defined in §13.4.2.

Specifically, the conversion operator from byte to Num<byte> will be considered when casting double to Num<byte> because byte (the actual parameter type to the operator method) can be implicitly converted to double (i.e. byte is encompassed by the operand type double). User-defined operators like this are only considered for explicit conversions, even if the operator is marked implicit.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top