Why does a method invocation expression have type dynamic even when there is only one possible return type?

UPDATE: This question was the subject of my blog on the 22nd of October, 2012. Thanks for the great question!


Why can’t the compiler figure out the compile-type type of M(dynamic_expression) if there is only one overload of M or all of the overloads of M have the same return type?

The compiler can figure out the compile-time type; the compile-time type is dynamic, and the compiler figures that out successfully.

I think the question you intended to ask is:

Why is the compile-time type of M(dynamic_expression) always dynamic, even in the rare and unlikely case that you’re making a completely unnecessary dynamic call to a method M that will always be chosen regardless of the argument type?

When you phrase the question like that, it kinda answers itself. 🙂

Reason one:

The cases you envision are rare; in order for the compiler to be able to make the kind of inference you describe, enough information must be known so that the compiler can do almost a full static type analysis of the expression. But if you are in that scenario then why are you using dynamic in the first place? You would do far better to simply say:

object d = whatever;
Foo foo = new Foo();
int x = (d is string) ? foo.M((string)d) : foo((int)d);

Obviously if there is only one overload of M then it is even easier: cast the object to the desired type. If it fails at runtime because the cast it bad, well, dynamic would have failed too!

There’s simply no need for dynamic in the first place in these sorts of scenarios, so why would we do a lot of expensive and difficult type inference work in the compiler to enable a scenario we don’t want you using dynamic for in the first place?

Reason two:

Suppose we did say that overload resolution has very special rules if the method group is statically known to contain one method. Great. Now we’ve just added a new kind of fragility to the language. Now adding a new overload changes the return type of a call to a completely different type — a type which not only causes dynamic semantics, but also boxes value types. But wait, it gets worse!

// Foo corporation:
class B
{
}

// Bar corporation:
class D : B
{
    public int M(int x) { return x; }
}

// Baz corporation:
dynamic dyn = whatever;
D d = new D();
var q = d.M(dyn);

Let’s suppose that we implement your feature requiest and infer that q is int, by your logic. Now Foo corporation adds:

class B
{
    public string M(string x) { return x; }
}

And suddenly when Baz corporation recompiles their code, suddenly the type of q quietly turns to dynamic, because we don’t know at compile time that dyn is not a string. That is a bizarre and unexpected change in the static analysis! Why should a third party adding a new method to a base class cause the type of a local variable to change in an entirely different method in an entirely different class that is written at a different company, a company that does not even use B directly, but only via D?

This is a new form of the Brittle Base Class problem, and we seek to minimize Brittle Base Class problems in C#.

Or, what if instead Foo corp said:

class B
{
    protected string M(string x) { return x; }
}

Now, by your logic,

var q = d.M(dyn);

gives q the type int when the code above is outside of a type that inherits from D, but

var q = this.M(dyn);

gives the type of q as dynamic when inside a type that inherits from D! As a developer I would find that quite surprising.

Reason Three:

There is too much cleverness in C# already. Our aim is not to build a logic engine that can work out all possible type restrictions on all possible values given a particular program. We prefer to have general, understandable, comprehensible rules that can be written down easily and implemented without bugs. The spec is already eight hundred pages long and writing a bug-free compiler is incredibly difficult. Let’s not make it more difficult. Not to mention the expense of testing all those crazy cases.

Reason four:

Moreover: the language affords you many opportunities to avail yourself of the static type analyzer. If you are using dynamic, you are specifically asking for that analyzer to defer its action until runtime. It should not be a surprise that using the “stop doing static type analysis at compile time” feature causes static type analysis to not work very well at compile time.

Leave a Comment