Compartilhar via


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

  • Anonymous
    April 18, 2007
    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...

  • Anonymous
    April 18, 2007
    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.

  • Anonymous
    April 18, 2007
    The comment has been removed

  • Anonymous
    April 18, 2007
    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?

  • Anonymous
    April 18, 2007
    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.

  • Anonymous
    April 18, 2007
    The comment has been removed

  • Anonymous
    April 18, 2007
    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.

  • Anonymous
    April 18, 2007
    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.

  • Anonymous
    April 21, 2007
    Visual Studio Orcas Beta 1 is available for download . Though quite similar to the March CTP in terms