Partager via


Errata: dynamic conversions and overload resolution

When you stand up and tell the world how your software is supposed to behave a full year or more before you’re done working on it, a funny thing can sometimes happen. You can change your mind. And therefore be wrong by the time the software ships.

And so it is with dynamic. Shortly after PDC ’08, I posted a number of things that were correct and well-thought-out. At the time. They are now incorrect, and I want to draw attention to that fact so everyone has the right idea about how this all works. Or, at least, not the wrong idea. Well, actually, if you have the wrong idea I don’t want the blame.

In my original series on dynamic, Part IV, Part V, and Part VI are wrong. I touched up Part IV at the end, but V and VI are too wrong to fix. Here’s why.

Overload resolution with dynamic

We had this big complicated scheme thought out: overload resolution would respect dynamic arguments and bind statically sometimes, but mostly it would go dynamic. There was something called the phantom method. The idea was that you could write an API with methods that would always be statically bound, and then treat your parameters as dynamic in the method body.

None of that survives. It was too complicated. The new rules are dead simple: If you have a method call with a dynamic argument, it is dispatched dynamically, period. During the runtime binding, all the static types of your arguments are known, and types are picked for the dynamic arguments based on their actual values.

See? That’s much simpler to explain, and easier to reason about. It means that in the following code, the call to M will be dispatched at runtime, and the M that gets picked will depend on the value in d. In this case, M(string) will be called.

 class C
{
    void M(dynamic d) { }
    void M(string s) { }
    void M(int i) { }

    void CallM()
    {
        dynamic d = "test";
        this.M(d);
    }
}

You might ask, what about that M(dynamic) overload? Well, when you have a parameter of type dynamic, the fact that it is dynamic is only relevant in the body of the method. Because of this new simple scheme for overload resolution, as callers see it, M(dynamic) is really no different than M(object).

Assignment conversions

I made a big fuss about something called assignment conversions. These were in the design for a while, and they were a way for us to dispatch conversions dynamically without introducing unnecessary loops in the conversion graph. Well, the idea was right, but the abstraction we settled on is different. And I have to admit it’s a little clever.

The rules for conversions involving dynamic are now as follows:

  1. There is an implicit conversion to dynamic from every type (modulo the weirdos, like pointer types). Basically, if it has an implicit conversion to object, it has an implicit conversion to dynamic too. This has not changed.
  2. There are no implicit conversions from dynamic to any type aside from itself and object.
  3. However (this is the clever part), there are implicit conversions from any dynamic expression (not type!) to any type.
  4. There are implicit conversions between types (in both directions) when they differ only by object and dynamic (as in the exception in rule 2).

Let’s consider the consequences of rules number 2 and 3. Why the distinction between conversions of expressions and conversions of types? Well, for one thing, it allows us to do what “assignment conversions” used to do. In particular, if you have an assignment from a dynamic d:

 string s = d;

Then the conversion exists because of rule 3. The conversion is from the expression d and not the type dynamic. This is true for most places where you have a dynamic value, including returns and foreaches and property sets, and everything I laid out in Part V of the dynamic series.

So how do we observe that there exists no conversion from the type dynamic to the type string? Easy, we just need a part of the language that only uses types (without values). Enter method type inference! I can use covariance to prove to you that the compiler does not allow conversions from dynamic to string:

 public class Program
{
    static void Main()
    {
        IEnumerable<dynamic> ied = null;
        IEnumerable<string> iei = null;

        var x = M(ied, iei);
        x.ThisisOnlyCompilesIfXIsDynamic();
    }

    static T M<T>(IEnumerable<T> x, IEnumerable<T> y) { return default(T); }
}

Ok, this program compiles, and therefore method type inference must have selected dynamic for T in the method type inference step while performing overload resolution in the call to M. In short, the line of reasoning here is:

  1. The first argument has type IEnumerable<dynamic>, and therefore dynamic is a candidate for T (and a lower bound).
  2. The second argument has type IEnumerable<string>, and therefore string is a candidate for T (and a lower bound).
  3. When fixing T among the set of candidates { dynamic, string }, since there is a conversion from string to dynamic, but not the other way around, the more general type (dynamic) is selected.

In step 3, if there were a conversion back again from dynamic to string, then neither type would be more general, and method type inference would have failed with an ambiguity. Thus, I’ve proven to you that that conversion must not exist.

Note: I had to use covariance and stick these types into an IEnumerable (I could have used arrays too) because if one of the arguments had been “naked” dynamic, then the whole analysis would have been deferred to runtime due to the new overload resolution rules. In this example, M is dispatched statically.

Note 2: The compiler contains a slight deviation from the language spec on rule #2 above. During method type inference, the compiler believes that the only implicit conversion from the type dynamic is to dynamic. This is a vestige of the previous implementation that was discovered too late to fix. You will only be able to observe it in scenarios such as the above example, where you replace “string” with “object”. Such code should give an ambiguity, but instead picks dynamic. I don’t expect anyone to spend much time fretting about this.

Other things

There have been a number of changes to the DLR, notably the names of many of their types, but also in the design of binders. These affected Part II, so I updated the code samples so that they now compile and run.

In conclusion

When I was posting in 2008, the design was going through a lot of change. In fact, the reason I stopped posting about dynamic is because of these changes; I knew I was going to ultimately be wrong because everything was moving around so much. Incidentally, this made my day job a bit hectic too, as I was implementing a moving target.

Now, however, it’s too late to change anything, and I can speak with confidence about what C# 4 will look like when it launches.

Comments

  • Anonymous
    April 01, 2010
    The comment has been removed
  • Anonymous
    April 01, 2010
    Are you sure you worded #3 as intended?"However (this is the clever part), there are implicit conversions from any expression (not type!) to dynamic. "The following examples are not about converting and expression to dynamic, but about converting a dynamic expression to some static type, aren't they?
  • Anonymous
    April 01, 2010
    The comment has been removed
  • Anonymous
    April 01, 2010
    The comment has been removed
  • Anonymous
    April 02, 2010
    @Jon,Yep. That's a great example. I think to get the idea from it, though, you have to be pretty clear on the fact that array type inference uses the types of the initializer elements, and not their values. Because the value is sitting right there.
  • Anonymous
    April 02, 2010
    I agree with Chris about Jon's example, I'd changedynamic d = 0;todynamic d = "dynamic";
  • Anonymous
    April 09, 2010
    Am I reading this right - dynamic overload resolution gives us double-dispatch?For example:class C{   void M(Expression e) { }   void M(MethodCallExpression mce) { }   void M(LambdaExpression le) { }   public void CallM(Expression e)   {       dynamic d = e;       this.M(d);   }}If I invoke CallM with a LambdaExpression, it will call M(LambdaExpression); with a MethodCallExpression, it will call M(MethodCallExpression); and with any other type of Expression, it will call M(Expression)?
  • Anonymous
    April 29, 2010
    Thank you for updating Chris.I have a question.Is 'assignment conversion' still exist? Or has it replaced by 'clever part'?I mean, is this term 'assignment conversion' still valid?