Chained user-defined explicit conversions in C#, Part Two

Reader Larry Lard asks a follow-up question regarding the subject of Monday’s blog entry. Why is it that the compiler knows that (int)(new Base()) will always fail, and therefore makes the conversion illegal, but does not know that (Derived)(new Base()) will always fail, and make that conversion illegal too?

There are two answers, a general answer and a specific answer.

The general answer is that in most cases, the C# type system operates solely on types. There are some exceptions where we peer closer at an expression when doing a conversion – lambdas are the obvious huge exception to that rule, since the convertibility of a lambda to a particular type depends on the structure of the type and the contents of the lambda parameters and body. There are smaller exceptions as well – literal zero is convertible to any enum, for example. But in general, we cleave to the principle that the type system mostly makes decisions based on the types of expressions, not the content of the expressions themselves.

Thus, in the examples above, we only see that the argument of the cast operator is something of type Base. The fact that this thing can be no more derived than Base is lost; it’s something of type Base, and therefore might be something that can be converted to Derived.

The more specific answer is that in fact, this doesn’t always fail. Betcha didn’t know that!

This is one of the most obscure, bizarre (and frankly, also one of the most poorly specified and documented) parts of the C# implementation. I myself just happened to learn about it recently. But it is true -- there is a situation where there is a non-user-defined explicit conversion from Base to Derived such that (Derived)(new Base()) succeeds at runtime.

Via a similar mechanism, there is also a situation where (Base)(new Base()) fails at runtime!

Actually, it gets even worse than that. Last time, I mentioned that there were two times when the compiler inserts an "explicit" cast on your behalf. The case I am referring to here introduces a third that I didn't know about. This means that it is possible for Base b = new Base(); to compile but fail at runtime!

So, another challenge to my readers: does anyone know the extremely obscure way that this can happen?

I’ll give you a hint: the Base will typically start with the letter I, not B...

And in related news, I've also recently learned of a fourth situation in which the compiler inserts an explicit cast. I'll blog more about that later this week probably.

Comments (9)

  1. There’s probably been a dozen comments before mine saying this, but none of them are showing up as I type this.

    It’s one of those goofy COM Interop things that the language should never have supported directly, isn’t it?

    But surely the compiler could still detect that case – since the compiler IS the one doing the magic – as distinct from the case that must always fail…

  2. I see that Stuart has beaten me to it. I was going to say the same thing: COM Interop.

    If an interface has System.Runtime.InteropServices.CoClassAttribute, the compiler will allow you to instantiate it and whenever an instance of a COM type is cast to a COM interface, the resulting cast ultimately boils down to QueryInterface, which can fail at runtime.

  3. Eric Lippert says:

    Yep, you guys are on the right track.  You can actually abuse the COM interop system to do stuff entirely in managed code which fails in bizarre ways.  I’ll post an example next time.

  4. pcooper says:

    I’m not really familiar with .NET, but since it supports multiple programming languages, would it be possible for Base and Derived to be implmented in a language that when asked for a new Base could actually return a Derived? Or am I just completely talking nonsense?

  5. Eric Lippert says:

    That is a good guess. I am not sure if it is possible to have a base object with a .ctor method which returns a more derived type, but if the .NET framework allows it then you might be right.

  6. Rob P says:

    A related question:

    How and when is the list of available conversion operators determined?

    I have a custom generic list class that I’m faking covariance on by providing an explicit cast operator.  Basically:

    class MyList<T> : List<T> where T : Base


           public static explicit operator MyList<Base>(MyList<T> l)

           { …

    That makes

    MyList<Base> bl2 = (MyList<Base>)(new MyList<Derived>());

    work just fine.

    If, however, I do

    MyList<Derived> dl = new MyList<Derived>();

    object o = dl;

    MyList<Base> bl = (MyList<Base>)o;

    I get a runtime error.

    It looks like this happens because System.Object doesn’t have an explicit cast operator to MyList<Base>.  I was hoping the available operators would be resolved by the type of the instances (o.GetType() is MyList<Derived>) rather than the type the variable is declared as.

    Does this mean cast operator resolution is done statically but attempted dynamically?   Is there a way to do dynamic cast operator resolution?

    Apologies if this seems to tech-support-ish

  7. Eric Lippert says:

    Essentially what you’re asking is "why isn’t the conversion operator to call determined by late-bound dispatch at runtime?"  yes?

    Because a conversion operator is a static method. That’s why we force you to put "static" in the declaration, to call that out.

    Late bound dispatch based on the runtime type of an argument to a function call has a name in C#. It is called "virtual" dispatch. Conversion operators are not virtual functions. They are static functions; if we can’t figure out to call it by static analysis, we don’t do it.

    In this case since we cannot find a static user-defined conversion function that converts object to your type, we generate a runtime type check to see if the object is of the desired type or a more derived type.  The runtime type check fails, hence the exception.

  8. Ah yes, that reminds me of the other feature that might be used to create weird typing situations: transparent proxies. If you write your own proxy, you can get called by the runtime whenever it encounters a type cast, so you can do weird things.

  9. Visual Studio Orcas Beta 1 is available for download . Though quite similar to the March CTP in terms

Skip to main content