views:

196

answers:

5

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?

A: 

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
The `Contains` extension method is defined on `IEnumerable` that `ISet` inherits from. It is perfectly good call in this scenario.
Oded
msdn disagrees: http://msdn.microsoft.com/en-us/library/dd382213.aspx. Oded: it is in `ICollection`, not `IEnumerable`.
Femaref
@Femaref - Are you sure? http://msdn.microsoft.com/en-us/library/bb352880.aspx
Oded
A little bit of clarity: ISet<T> inherits and _instance_ method Contains(T) from ICollection<T> (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
@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
+3  A: 

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
The comment about the whole expression being dynamic is helpful, thanks.
Andrey Breslav
The explanation is a little bit strange: why can I declare somthing as ISet<dynamic> if this type is not legitimate? There must have been a compilation error.
Andrey Breslav
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
This answer is guessing, and it is guessing wrong.
Timwi
@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<dynamic> but you can't use it as a base interface.
Eric Lippert
+1  A: 

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:

http://stackoverflow.com/questions/3071634/strange-behaviour-when-using-dynamic-types-as-method-parameters/3072267#3072267

Julien Lebosquain
There is no call to `ISet<T>.Contains` in the emitted IL. The emitted IL is a dynamic invocation involving CallSiteBinder etc.
Timwi
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
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
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
@Andrey the local representing s is of the type ISet<dynamic> not HashSet<dynamic> which is the type of the object _assigned_ to s.
Rune FS
@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
@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
A: 

Timwi
Does the overload resolution mechanism look for an _exact signature match_?! Why doesn't it matter that object is assignable from Int32?
Andrey Breslav
@Timwi, was it intentional that the entire answer's content is gone?
Sander Rijken
@Timwi: Couldn't you just delete it instead of editing it out?
Mark Byers
+5  A: 

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.

Timwi
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
@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
@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
@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
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
@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
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
@Eric Lippert: My pleasure! Thanks for commenting! :)
Timwi
@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
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