0

Assume I have a method like this:

static string ToString(dynamic d)
{
    return (string)d.ToString();
}

And for example that I have var tmp = new List<dynamic> { 1, "2", 345 };

Why does

IEnumerable<string> test = tmp.Select(ToString);

compile fine, but

IEnumerable<string> test = tmp.Select(x => ToString(x));

doesn't?

Error:

CS0266  Cannot implicitly convert type 'System.Collections.Generic.IEnumerable<dynamic>'  to 'System.Collections.Generic.IEnumerable<string>'. An explicit conversion exists (are you missing a cast?)

This holds true on VS 2015 and VS 2017 against all >=4.5.0 frameworks

2 Answers2

3

You've encountered dynamic contagion. A dynamic could be anything, so any method or property on it could return anything. Any method you pass it to could be anything, because when you pass a dynamic as a parameter, overload resolution happens at runtime (this can be exploited if you hate the people who'll have to maintain your code).

If you hover the mouse over Select, it's of type Select<dynamic, string>, so it returns IEnumerable<String>. Because ToString(dynamic d) has an explicit cast to string in the return, and returns type string, the compiler can be assured that ToString really is returning a string.

IEnumerable<string> test = tmp.Select(ToString);

Incidentally, if we make these changes to ToString(), the above will still compile and the lambda version still won't:

    static string ToString(object d)
    {
        //  Remove the cast to string
        return d.ToString();
    }

The contagion is introduced when we create tmp as List<dynamic>.

In lambda version that won't compile, hovering the mouse shows that we're actually calling Select<dynamic, dynamic> which returns IEnumerable<dynamic>.

Overload resolution is done at runtime when you pass dynamic. The compiler can't guarantee at compile time what method will actually be called or what it will return. Intellisense thinks ToString is your static method, but the compiler doesn't trust that to remain true.

IEnumerable<string> test2 = tmp.Select(x => ToString(x));

This compiles, because we have a cast where overload resolution won't affect it.

IEnumerable<string> test3 = tmp.Select(x => (string)ToString(x));

Jonathon Chase kindly notes in comments that we make it compile by explicitly passing type parameters to Select:

IEnumerable<string> test4 = tmp.Select<dynamic, string>(x => ToString(x));

The important question, in my view, is why your first case compiles. My cautious guess is that because you're passing a reference to the method rather than calling it, dynamic overload resolution doesn't happen.

Consider this a placeholder until somebody with deeper understanding takes an interest in the question.

  • 1
    I think it's really interesting that `Select(ToString)` is inferred as `Select` whereas `Select(x => ToString(x))` is inferred as `Select`. I think you're correct that overload resolution is probably the culprit, given the murkiness of it's rules with odd corner cases. – Jonathon Chase May 24 '18 at 19:02
  • 1
    Which, having realized that was the case, you can get it to compile with `tmp.Select(x => ToString(x));` – Jonathon Chase May 24 '18 at 19:04
2

I will not repeat answer made by Ed about "dynamic contagion". But will try to shed some light on the reason of the first case successful compilation.

Here

IEnumerable<string> test = tmp.Select(ToString);

ToString is a method group – sugaring feature which allows you to pass a method as a delegate with appropriate signature using some kind of a shortcut. Some additional info may be found in this thread What is a method group in c#.

This actually works by the means of creating a new delegate instance under the cover. Therefore your code is pretty equivalent to

Func<dynamic, string> toString = new Func<dynamic, string>(ToString);
IEnumerable<string> test = tmp.Select(toString);

As you can see we have an explicit delegate return type here, and it's not dynamic, thus we shouldn't worry about dynamic contagion.

Uladzislaŭ
  • 1,680
  • 10
  • 13
  • In this case, _why_ does the method group conversion choose (the equivalent of, at least) `Func`? – Jonathon Chase May 25 '18 at 01:24
  • @JonathonChase this is the only overload available at compile time. Signature may be `Func` due to variance, but what's the matter if delegate target will be `ToString : dynamic -> string` in any case – Uladzislaŭ May 25 '18 at 06:18