Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c#

dynamic

Inspired by this question.

Short version: Why can't the compiler figure out the compile-time type of M(dynamic arg) if there is only one overload of M or all of the overloads of M have the same return type?

Per the spec, §7.6.5:

An invocation-expression is dynamically bound (§7.2.2) if at least one of the following holds:

  • The primary-expression has compile-time type dynamic.

  • At least one argument of the optional argument-list has compile-time type dynamic and the primary-expression does not have a delegate type.

It makes sense that for

class Foo {
    public int M(string s) { return 0; }
    public string M(int s) { return String.Empty; }
}

the compiler can't figure out the compile-time type of

dynamic d = // dynamic
var x = new Foo().M(d);

because it won't know until runtime which overload of M is invoked.

However, why can't the compiler figure out the compile-time type if M has only one overload or all of the overloads of M return the same type?

I'm looking to understand why the spec doesn't allow the compiler to type these expressions statically at compile time.

like image 425
jason Avatar asked Feb 21 '12 17:02

jason


2 Answers

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.

like image 125
Eric Lippert Avatar answered Oct 12 '22 09:10

Eric Lippert


An early design of the dynamic feature had support for something like this. The compiler would still do static overload resolution, and introduced a "phantom overload" that represents dynamic overload resolution only if necessary.

  • Blog post introducing phantom methods
  • Details on phantom methods

As you can see in the second post, this approach introduces a lot of complexity (the second article talks about how type inference would need to be modified to make the approach work out). I'm not surprised that the C# team decided to go with the simpler idea of always using dynamic overload resolution when dynamic is involved.

like image 31
Daniel Avatar answered Oct 12 '22 10:10

Daniel