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

4

16

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.

Outofdate answered 21/2, 2012 at 17:24 Comment(3)
Any that involves dynamic is, by the spec, dynamic completely until it is cast to something else. Dynamic is infectious, and expands to make anything that uses dynamic - become dynamic.Fairfield
Yes, I know. The question is why is the spec written this way, even when there is only one possible type for the expression. Here, we are looking at the example of an invocation expression.Outofdate
I don't think this is the reason, in fact this may even never have been considered during the design, but keeping the result dynamic prevents unboxing and reboxing if the result is a value type and the rest of the method continues to that result in other dynamic method calls.Chasechaser
B
23

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.

Blender answered 21/2, 2012 at 21:29 Comment(1)
Considering the best answers to this question, you may get a dynamic object when you deserialize a JSON string. In this case you may want to pass this dynamic object to a single method (no overloads) that returns another statically typed object (mapping). I find odd that the object returned by this method is considered dynamic although the method declaration states differently.Horsey
L
4

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.

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.

Letendre answered 21/2, 2012 at 19:22 Comment(0)
E
1

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?

The compiler could potentially do this, but the language team decided not to have it work this way.

The entire purpose of dynamic is to have all expressions using dynamic execute with "their resolution is deferred until the program is run" (C# spec, 4.2.3). The compiler explicitly does not perform static binding (which would be required to get the behavior you want here) for dynamic expressions.

Having a fallback to static binding if there was only a single binding option would force the compiler to check this case - which was not added in. As for why the language team didn't want to do it, I suspect Eric Lippert's response here applies:

I am asked "why doesn't C# implement feature X?" all the time. The answer is always the same: because no one ever designed, specified, implemented, tested, documented and shipped that feature.

Enchanter answered 21/2, 2012 at 17:33 Comment(9)
"but the language team decided not to have it work this way." This is effectively what I'm asking about, I'm asking why it was designed that way, so it's a cop out to say that's the answer. "The compiler explicitly does not perform static binding (which would be required to get the behavior you want here) for dynamic expressions." Again, that's exactly what I'm asking about; why was it designed that way. "I suspect Eric Lippert's response here applies." I'm sure that response always applies. I'm hoping for a little more insight.Outofdate
@Jason Maybe somebody the language team will decided to chime in, but I suspect the answer will be the same answer they always give: "because no one ever designed, specified, implemented, tested, documented and shipped that feature." I agree, it's a good idea, but so are many other language features they didn't do - I think they just either didn't think of it, or decided it wasn't worth the effort to implement it that way....Enchanter
@Joey: I'm not sure if that's the point of dynamic, and I certainly don't agree with the "regardless" assertion; that's basically assuming away the whole question. I think of the point of dynamic as deferring type checking to runtime. But in cases where there's only one possible type, such as this, I don't see why we would want the checking deferred.Outofdate
@Jason If there is only one possible type and you don't want type checking differed then don't use 'dynamic'... To expand on the quote of Eric, the question is not "why wasn't feature X implemented". The question is, "why should feature X be implemented". There doesn't appear to be any compelling reasons for why not implementing this feature has significant problems, or that there would be significant advantages to implementing it. If you want compile time checking don't use dynamic, if you don't, use it. That's the thought process in the design of that language feature.Flasket
@Servy: Nope, you're missing the point. Read the original question that I linked to for an example as to why this might come up. It's not as simple as "if you don't want dynamic, don't use dynamic." The dynamic can come up naturally, but then you try to feed into something like DateTime.Parse and you want a DateTime to come out but the compiler types it as dynamic unless you explicitly cast somewhere.Outofdate
@Jason The result will be DateTime in your example, unless you use var to store the result... The answer there is to explicitly specify your types...Enchanter
@Reed Copsey: That's a cop out too. It doesn't answer the question as to why the spec isn't written to type the expression as a static type when there is only one possible type, whether or not there are dynamic constituents. I'm not asking for workarounds. I'm not asking for cheap "because" answers. The answer to every "why" question is "because"; that doesn't mean we don't ask "why" questions because often times there are really interesting explanations lurking in the background.Outofdate
Actually in this particular case we did design, spec, implement and test the feature. We cut it anyway, because it was awful.Blender
@Jason It's often the case that one reason for omitting a given feature is the existance of an easy workaround (and, as Eric explained, the workaround in this case is even a better solution than the feature itself). If a feature is omitted because there's an easy workaround, then it's not exactly a cop out to answer the question "why doesn't the feature exist" by saying "because there's an easy workaround".Snowblink
B
1

I think the case of being able to statically determine the only possible return type of a dynamic method resolution is so narrow that it would be more confusing and inconsistent if the C# compiler did it, rather than having across the board behavior.

Even with your example, what if Foo is part of a different dll, Foo could be a newer version at runtime from a binding redirect with additional M's that have a different return type, and then the compiler would have guessed wrong because the runtime resolution would return a different type.

What if Foo is an IDynamicMetaObjectProvider d might not match any of the static arguments and thus it would fall back on it's dynamic behavior which could possibly return a different type.

Bullyboy answered 21/2, 2012 at 19:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.