11

In the following code example the call l.Add(s) and c.Add(s) is successful, but it fails when for a generic IList<string>.

    var l = new List<string>();
    dynamic s = "s";
    l.Add(s);
    var c = (ICollection<string>)l;
    c.Add(s);
    var i = (IList<string>)l;
    i.Add("s"); // works
    i.Add(s); // fails

https://dotnetfiddle.net/Xll2If

Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: No overload for method 'Add' takes '1' arguments at CallSite.Target(Closure , CallSite , IList`1 , Object ) at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid2[T0,T1](CallSite site, T0 arg0, T1 arg1) at Program.Main() in C:\Dev\PlayGround\PlayGround\Program.cs:line 13

IList<T> derives from ICollection<T>. Can someone explain why the call for IList.Add fails?

Gerd K
  • 1,134
  • 9
  • 23
  • Really strange. Good find. The type of `i` is `IList`, which is not `dynamic`, so it seems like the `i.Add` call should be bound (binding) at compile-time. And it compiles. Yet, at run-time it appears that the binding fails?! It seems like a bug to me! – Jeppe Stig Nielsen Jul 09 '18 at 08:08
  • Are you sure the error is at the third Add()? ICollection does not have an Add method, so I would expect _that_ to fail. Sorry, my bad. Incorrectly looked that up. – PMF Jul 09 '18 at 08:09
  • Run the fiddle. You'll see the exception occurs at `i.Add(s)` – Gerd K Jul 09 '18 at 08:10
  • 1
    Apparently the binding is postponed until run-time because the argument `s` is of type `dynamic`. If you change `s` into `var s = "s";`, the bug does not occur. In any case, it is interesting why `c.Add` goes well, while `i.Add` does not. – Jeppe Stig Nielsen Jul 09 '18 at 08:13
  • 1
    By the way, the `dynamic` isn't relevant here, it's the same with a `List`: _"Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'No overload for method 'Add' takes '1' arguments'"_ I guess because [`IList` is a readonly interface](https://stackoverflow.com/questions/26475362/why-does-ilisttarray-readonly-true-but-ilistarray-readonly-false). – Tim Schmelter Jul 09 '18 at 08:13
  • 1
    @JeppeStigNielsen I already figured that out. I just wanted to focus on the smallest possible example showing the weird behavior. – Gerd K Jul 09 '18 at 08:15
  • How `dynamic` is resolved is implementation-dependent isn't it? So in another implementation this might work? – Sweeper Jul 09 '18 at 08:15
  • @TimSchmelter I created a fidlle using List https://dotnetfiddle.net/Xll2If . But adding a string to the `IList` works whilst dynamic fails. – Gerd K Jul 09 '18 at 08:20
  • @TimSchmelter When the program runs, generic argument written as `dynamic` will really be `object`. So `new List` creates the same type of object as does `new List`. Once that object is implicitly cast to another type, there is no difference between the two. You can also avoid explicit casts if you are OK with not using `var`. Therefore, the following program (that someone might consider simpler) gives the same behavior: `var l = new List(); dynamic s = "s"; l.Add(s); ICollection c = l; c.Add(s); IList i = l; i.Add(s);` – Jeppe Stig Nielsen Jul 09 '18 at 08:21
  • @GerdK: right, but it doesn't matter if the list is a `List` or a `List`. – Tim Schmelter Jul 09 '18 at 08:23
  • If you change to `System.Collections.IList i = l;`, the issue goes aways. – Jeppe Stig Nielsen Jul 09 '18 at 08:24
  • @JeppeStigNielsen The non-generic `IList.Add` might work because it takes an `object` and the dynamic invocation works properly. – Gerd K Jul 09 '18 at 08:36
  • Casting `i.Add((string)s);` will also resolve the issue. – Greg Jul 09 '18 at 08:47
  • (your last comment) No, I think the difference is non-generic `IList.Add` is declared at level `IList`, while for the generic interface `IList<>`, the `Add` method is inherited from `ICollection<>.Add`. – Jeppe Stig Nielsen Jul 09 '18 at 08:49
  • "`IList` derives from `ICollection`" -- Worth noting: if `IList` were a class type, it wouldn't be called inheritance, it would be called implementation. I would guess that the runtime binder may be wrongly treating the inheritance in this case as implementation too. It may be possible to verify or disprove this guess by looking at the source code, and I will make an attempt at doing so later today if nobody else does. –  Jul 09 '18 at 08:57
  • 1
    @hvd that's precisely what did it, and precisely what was necessary to fix in in corefx. It was also necessary to explicitly add `object` to the end of the lookup. – Jon Hanna Jul 09 '18 at 09:04

2 Answers2

5

In looking up methods to bind a call a compiler (whether static or dynamic) may have to check on base types.

Generally with uses of dynamic we're dealing with a calls to methods on the dynamic object itself, so the dynamic compiler starts with the concrete type of the object being used via dynamic and need only go up its inheritance chain until it reaches object to look for them.

In the case where a dynamic object is passed as an argument to a method call on something statically referenced as an interface type though, the dynamic binder sadly does the same thing: if it doesn't find the method on the type in question it then looks at the BaseType of that type, which for an interface is null. So it behaves the same as it would if it had checked the inheritance correctly and failed to find the method in question and throws the appropriate exception for that case. (Note that this also means it would fail to find the Equals method defined on object).

What it should do for such cases is to check all of the interfaces implemented by the interface type in question.

This bug is fixed in .NET Core but the fix has not been ported to .NET Framework. If you want to file a bug report against netfx you might want to reference that fix in corefx.

It is sometimes possible to workaround this problem by accessing the interface type itself through the base interface where the method used is defined, or as itself being dynamic (so the appropriate method is found in whichever concreted type implements it).

Jon Hanna
  • 110,372
  • 10
  • 146
  • 251
  • I'm pretty sure this question is a duplicate, btw, but can't find it. I'm pretty sure I remember addressing this fix in another answer though. – Jon Hanna Jul 09 '18 at 09:12
  • Thanks for the detailed explanation. I reported a bug report https://developercommunity.visualstudio.com/content/problem/289268/base-interface-method-call-fails-for-dynamic-argum.html - I hope this is the right location – Gerd K Jul 09 '18 at 09:31
  • I've suggested at https://github.com/dotnet/corefx/issues/14752 that they might consider porting the fix, too. – Jon Hanna Jul 09 '18 at 09:33
0

This is just a (long) comment. The following also produces the error (or a closely related one):

public interface IBase
{
  void Add(string s);
}
public interface IDerived : IBase
{
}
public class Concrete : IDerived
{
  public void Add(string s)
  {
  }
}

And then:

    IDerived i = new Concrete();
    i.Add((dynamic)"s");

My bet is this is an error in the run-time binding code emitted by the C# compiler.

Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
  • 1
    The [dotnetfiddle.net/yi3KnJ](https://dotnetfiddle.net/yi3KnJ) shows the issue. Interestingly, [this tio.run code](https://tio.run/##ZY7NCsIwEITP2adYekoP@gCKB38uHgRfISarLLSpZGNFpM8eE1uK4HVmvpmxsrBiU2IfKVyNJTzujBC8QfUdO9w6pyUG9jeUeg0D/AQPFLgnh6uZGcA2RgT3nbeBIhVrSpXG@@PSsMX/YlDZHTIOINHEnBl7zqG7BdMWdtK/7Mmw1yOk5heMG/T0nLd1vqsUL8uQdi9vWrZ1JVWRy1RKHw) does not have the issue (in tio.run, the code is encoded in the URL; I removed some `public` modifiers that are not needed or important for the issue). At what version was this bug introduced? – Jeppe Stig Nielsen Jul 09 '18 at 08:53
  • It looks like the same C# compiler is used in both cases, but with `dotnetfiddle.net` the runtime operates under Windows, while tio.run is running under Linux. If anyone has Roslyn C# under Linux, can they confirm that GerdK's original issue (from the question) does not occur there? – Jeppe Stig Nielsen Jul 09 '18 at 09:02
  • I think trio.run is using .NET Core and I fixed this bug in .NET Core about a year ago :) – Jon Hanna Jul 09 '18 at 09:03
  • It seems tio.run uses Mono. I am not familiar with Mono but It seems compiler related. Both dotnetfiddle and tio use the same .net runtime it seems. `System.Console.WriteLine(System.Environment.Version);` – Gerd K Jul 09 '18 at 09:07
  • @JonHanna tio.run has many choices. I picked "C# (Visual C# Compiler)". Another one there is "C# (.NET Core)". They have several others. Since you fixed the issue, can you clarify if you fixed the IL that the C# emits to make the run-time do the binding? Ah, I see you included a link in your own answer, to the fix. – Jeppe Stig Nielsen Jul 09 '18 at 09:07
  • 1
    The bug hits before any IL is generated, or indeed any expression (`dynamic` becomes an `Expression` first and is then either compiled to IL or interpreted according to where it runs). The significant thing is what version of `Microsoft.CSharp` is being used rather than what C# compiler the static compilation is in. – Jon Hanna Jul 09 '18 at 09:11
  • @JonHanna OK, thanks. In any case, when the member is declared in `System.Object` but called from an interface, [it also works in tio.run](https://tio.run/##TY6xCsJAEETr3FcsqS6F@QDFQsVCVBQsrJfNKguXO7w9lSD59nhEEdt5b4YhnZDSMIhPHC9IDJslKpuX6Q05VIVV8BQ5MUz/kDGaMAnBxznGcI3YZlR880eQBvYo3lamyHExdkFgDp6fv1FbzTKTen27o1Nrm85jK1SVWo7k1Gnits66Bsf1OUrinXi25cI5OGxHq8@HhuEN). – Jeppe Stig Nielsen Jul 09 '18 at 09:13
  • I fixed that in the same PR :) – Jon Hanna Jul 09 '18 at 09:15
  • 1
    Just noticed above that @GerdK says it uses Mono. Mono now uses the corefx libraries, but used to have its own version of that library. So most likely it has the fix available to it, but also possible that it's using the old Mono version and that might not have ever had this bug. (I don't know the Mono version at all, but there were a few things the Mono version had right that the .NET version was buggy for, that were found when the Mono tests started being run against the corefx version). – Jon Hanna Jul 09 '18 at 09:31