22

Please, help me to explain the following behavior:

dynamic d = 1;
ISet<dynamic> s = new HashSet<dynamic>();
s.Contains(d);

The code compiles with no errors/warnings, but at the last line I get the following exception:

Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'System.Collections.Generic.ISet<object>' does not contain a definition for 'Contains'
   at CallSite.Target(Closure , CallSite , ISet`1 , Object )
   at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid2[T0,T1](CallSite site, T0 arg0, T1 arg1)
   at FormulaToSimulation.Program.Main(String[] args) in 

As far as I can tell, this is related to dynamic overload resolution, but the strange things are

(1) If the type of s is HashSet<dynamic>, no exception occurs.

(2) If I use a non-generic interface with a method accepting a dynamic argument, no exception occurs.

Thus, it looks like this problem is related particularly with generic interfaces, but I could not find out what exactly causes the problem.

Is it a bug in the compiler/typesystem, or legitimate behavior?

Oded
  • 489,969
  • 99
  • 883
  • 1,009
Andrey Breslav
  • 24,795
  • 10
  • 66
  • 61

5 Answers5

9

The answers you have received so far do not explain the behaviour you are seeing. The DLR should find the method ICollection<object>.Contains(object) and call it with the boxed integer as a parameter, even if the static type of the variable is ISet<dynamic> instead of ICollection<dynamic> (because the former derives from the latter).

Therefore, I believe this is a bug and I have reported it to Microsoft Connect. If it turns out that the behaviour is somehow desirable, they will post a comment to that effect there.

Moby Disk
  • 3,761
  • 1
  • 19
  • 38
Timwi
  • 65,159
  • 33
  • 165
  • 230
  • It's good I could convince you that it's a bug. :) Do you know what is the motivation behind this crazy decision to implement run-time overload resolution for dynamics? – Andrey Breslav Sep 12 '10 at 20:00
  • @Andrey: That decision probably derives from the fact that there is no way to resolve something at compile-time if the information necessary to do so (the argument types) is not available at compile-time. – Timwi Sep 12 '10 at 20:11
  • @Andrey: To make this clearer, imagine you have a method `void Blah(string s);`. Now suppose you write this: `dynamic d = 1; Blah(d);`. Should this simply fail to compile, or should it decide at runtime whether to throw or not? Of course you’d want it to succeed when `d` contains a string, so it has to resolve at runtime. – Timwi Sep 12 '10 at 20:16
  • @Timwi: No, of course I would want it to fail to compile, because MSDN clearly states that dynamic is a type. C# is a staically-typed language and dynamic does not coerse to anything. Additionally, I don't want my collectios to work with any overhead when putting dynamics into them. And I want my extension methods to be available on such collections. – Andrey Breslav Sep 12 '10 at 21:46
  • What I ment to ask is more like "Do you know some really critical example, that would not work without this feature?" If such an example does not exist, the presence of run-time overload resolution is a very questionable design decision, – Andrey Breslav Sep 12 '10 at 21:49
  • @Andrey: I’ve given such an example. It seems that you misunderstand the purpose of `dynamic`. If you just want a collection with things of different types, use `object` instead. Then you can use `Contains` (etc.) without overhead (except for boxing). If you want to invoke dynamic calls on the objects *in* the collection, you can still cast them to `dynamic` when you take them out. – Timwi Sep 12 '10 at 23:59
  • 1
    Your analysis seems plausible, though I am not at my desk at the moment so I cannot actually check it. Julien gives a reasonable explanation for this behaviour; however, I agree that it seems like a bug. The larger design principle here is that the dynamic call should do exactly what the C# compiler *would have done* had the compile-time type of the argument been correctly stated as the run-time type. Since clearly it would have succeeded with an int as the argument, and generated a call to the appropriate method, it's a bug that it throws. Thanks for reporting it via Connect! – Eric Lippert Sep 13 '10 at 00:31
  • @Eric Lippert: My pleasure! Thanks for commenting! :) – Timwi Sep 13 '10 at 00:32
  • @Timwi: Thanks for the doubt of my understanding of the purpose of dynamic. One of the main purposes of having generic collections is _not to cast_ things you get from the collection. Thus, if I have to cast to do what I want, it is a sacrifice made by language designers, and I want a clear reson for why that sacrifice was made. I would have had no problem here, if dynamic was not claimed to be a type. – Andrey Breslav Sep 13 '10 at 04:50
  • In my opinion, the dynamic type is needed to be able to call those things unknown at compile-time _on_ it (cause this is what you get in dynamically-typed languages, in most of which you have _no type-based overloading at all_). And I definitiely do not want it to disturb the surrounding code. – Andrey Breslav Sep 13 '10 at 04:52
3

Why it compiles: the entire expression is evaluated as dynamic (hover your mouse over it inside your IDE to confirm), which means that it is a runtime check.

Why it bombs: My (completely wrong, see below) guess is that it is because you cannot implement a dynamic interface in such a manner. For example, the compiler does not allow you to create a class that implements ISet<dynamic>, IEnumerable<dynamic>, IList<dynamic>, etc. You get a compile-time error stating "cannot implement a dynamic interface". See Chris Burrows' blog post on this subject.

http://blogs.msdn.com/b/cburrows/archive/2009/02/04/c-dynamic-part-vii.aspx

However, since it's hitting the DLR anyway, you can make s completely dynamic.

dynamic s = new HashSet<dynamic>;
s.Contains(d);

Compiles and runs.

Edit: the second part of this answer is completely wrong. Well, it is correct in that you can't implement such an interface as ISet<dynamic>, but that's not why this blows up.

See Julian's answer below. You can get the following code to compile and run:

ICollection<dynamic> s = new HashSet<dynamic>();
s.Contains(d);
Anthony Pegram
  • 123,721
  • 27
  • 225
  • 246
  • The comment about the whole expression being dynamic is helpful, thanks. – Andrey Breslav Sep 12 '10 at 19:15
  • The explanation is a little bit strange: why can I declare somthing as ISet if this type is not legitimate? There must have been a compilation error. – Andrey Breslav Sep 12 '10 at 19:15
  • Your soultion has one significant drawback: no content-assist on Ctrl+Space. I'd better write s.Contains((object) d) -- compiles and runs. – Andrey Breslav Sep 12 '10 at 19:16
  • @Andrey: The type is perfectly legitimate. You can have a value or variable of that type. An analogy: you can have an expression of type System.ValueType, System.Enum, System.Delegate, but you can't use any of those as a base class. Similarly you can have an expression of type IFoo but you can't use it as a base interface. – Eric Lippert Sep 13 '10 at 00:18
2

The Contains method is defined on ICollection<T>, not ISet<T>. The CLR doesn't allow an interface base method to be called from a derived interface. You usually doesn't see this with static resolution because the C# compiler is smart enough to emit a call to ICollection<T>.Contains, not the non-existing ISet<T>.Contains.

Edit: The DLR mimics the CLR behavior, that's why you get the exception. Your dynamic call is done on an ISet<T>, not an HashSet<T> the DLR will mimics the CLR: for an interface, only interfaces methods are searched for, not base interfaces (contrary to classes where this behavior is present).

For an in-depth explanation, see a previous response of mine to a similar question:

Strange behaviour when using dynamic types as method parameters

Community
  • 1
  • 1
Julien Lebosquain
  • 40,639
  • 8
  • 105
  • 117
  • 1
    There is no call to `ISet.Contains` in the emitted IL. The emitted IL is a dynamic invocation involving CallSiteBinder etc. – Timwi Sep 12 '10 at 19:36
  • OK, this sounds reasonable, but a rethorical quesion here is "What the hell?". DLR should not be aware of any static typing information in the first place, and the run-time object does have the Contains method, so what is the logic behind this behavior? – Andrey Breslav Sep 12 '10 at 19:41
  • Of course, I was just using analogy between the CLR and the DLR. Edited my answer to try to make that more clear. See my other answer which is much well explained. – Julien Lebosquain Sep 12 '10 at 19:42
  • I doubt if one can say that a "dynamic call is done" on something rather than an actual run-time type of the object, which is HashSet. DLR is not supposed to know anything about the static type, is it? – Andrey Breslav Sep 12 '10 at 19:50
  • @Andrey the local representing s is of the type ISet not HashSet which is the type of the object _assigned_ to s. – Rune FS Sep 12 '10 at 20:34
  • @Rune FS: as I explained above, DLR works at runtime, and must NOT rely on any compile-time information. This is the whole point of working at runtime. – Andrey Breslav Sep 12 '10 at 21:53
  • @Andrey: No, the DLR works at runtime and *must rely* on compile-time information. For example, suppose d is of compile-time type dynamic and runtime type int. Suppose s is of compile-time type object and runtime type string. Suppose you have overloads F(int, string) and F(int, object) and a call to F(d, s). The right thing to do is to choose the SECOND overload because *the second parameter is not dynamic and therefore does not participate in dynamic dispatch resolution*. Only those expressions that are dynamic contribute their runtime type to the analysis. – Eric Lippert Sep 13 '10 at 00:25
0

Note that the type dynamic doesn’t actually exist at run-time. Variables of that type are actually compiled into variables of type object, but the compiler turns all the method calls (and properties and everything) that involve such an object (either as the this object or as a parameter) into a call that is resolved dynamically at runtime (using System.Runtime.CompilerServices.CallSiteBinder and related magic).

So what happens in your case is that the compiler:

  • turns ISet<dynamic> into ISet<object>;

  • turns HashSet<dynamic> into HashSet<object>, which becomes the actual run-time type of the instance you’re storing in s.

Now if you try to invoke, say,

s.Contains(1);

this actually succeeds without a dynamic invocation: it really just calls ISet<object>.Contains(object) on the boxed integer 1.

But if you try to invoke

s.Contains(d);

where d is dynamic, then the compiler turns the statement into one that determines, at runtime, the correct overload of Contains to call based on the runtime type of d. Perhaps now you can see the problem:

  • The compiler emits code that definitely searches the type ISet<object>.

  • That code determines that the dynamic variable has type int at runtime and tries to find a method Contains(int).

  • ISet<object> does not contain a method Contains(int), hence the exception.

Timwi
  • 65,159
  • 33
  • 165
  • 230
  • Does the overload resolution mechanism look for an _exact signature match_?! Why doesn't it matter that object is assignable from Int32? – Andrey Breslav Sep 12 '10 at 19:47
  • @Timwi, was it intentional that the entire answer's content is gone? – Sander Rijken Sep 12 '10 at 21:36
  • 1
    @Timwi: Couldn't you just delete it instead of editing it out? – Mark Byers Sep 12 '10 at 23:55
  • If you think your answer is incorrect, delete it by all means. But please don't leave a blank answer. –  Dec 28 '10 at 03:54
  • @Will: I didn’t delete this answer at the time because doing so would have cost me 20 rep. If you want to encourage users to delete incorrect answers, fix your broken incentive system so that this kind of situation doesn’t arise. – Timwi Dec 28 '10 at 18:27
  • @tim if I wrote it I would. I'm just a janitor. Take it to meta, if you care. But then, if you did, you'd delete or fix your incorrect answer instead of blanking it for a couple points. –  Dec 29 '10 at 04:32
  • @Will: I care tremendously, but Jeff Atwood obviously doesn’t. He just marks everything as “declined” without explanation and doesn’t care what his users think. – Timwi Dec 29 '10 at 17:34
-1

ISet interface does not have a method 'Contains', HashSet does however?

EDIT What i meant to say was the binder resolves 'Contains' when given the HashSet concreate type, but doesnt find the inherited 'Contains' method in the interface...

almog.ori
  • 7,839
  • 1
  • 35
  • 49
  • The `Contains` extension method is defined on `IEnumerable` that `ISet` inherits from. It is perfectly good call in this scenario. – Oded Sep 12 '10 at 18:57
  • 1
    msdn disagrees: http://msdn.microsoft.com/en-us/library/dd382213.aspx. Oded: it is in `ICollection`, not `IEnumerable`. – Femaref Sep 12 '10 at 18:57
  • 2
    A little bit of clarity: ISet inherits and _instance_ method Contains(T) from ICollection (see http://msdn.microsoft.com/en-us/library/dd382213.aspx, thanks, @Femaref). Important: it is an instance method. If it was an extension method, the call would not compile (C# refuses to compile extension method calls with dynamic arguments). – Andrey Breslav Sep 12 '10 at 19:09
  • @oded you're linking to an _extension_ method (notice that the namespace says System.Linq and the _class_ name is Enumerable and the method is static) – Rune FS Sep 13 '10 at 17:51