Gotchas in dynamic typing
The dynamic
support in C# is great, but there are some tricky aspects which are worth
knowing about, because they can trip you up easily. If you come across any yourself, please
mail me to let me know, and I'll add them to the list.
Explicit interface implementation
Remember that when the execution-time compiler handles a dynamic expression, it considers the actual type of the object, rather than anything else you might have known about it. (It's actually slightly more complicated than that, as the object may be of a type that the calling code doesn't know about. The compiler works out the "Best Accessible Type" and acts on that, but most of the time you don't need to worry about this.) It acts as if you'd declared a variable of that type, and performs the normal steps to work out which execution paths to take.
That's usually fine, but it doesn't play well with explicit interface implementation. If you recall, explicit interface implementation only allows you to call the implemented member via an expression which is the interface type. Assuming the actual implementation type is accessible, this means the execution-time compiler won't find the relevant member. Here's the example I give in the book, with a slight modification:
static void PrintCount(ICollection collection)
{
dynamic d = collection;
Console.WriteLine("Static typing: {0}", collection.Count);
Console.WriteLine("Dynamic typing: {0}", d.Count);
}
...
PrintCount(new int[10]);
We know for sure that the type has a Count
property of some kind,
because it implements ICollection
- the second line of the method shows
that we know about that property with static typing. However, when we call the method
with an array, it prints the count using the statically-typed property, but then
throws an exception:
Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'System.Array' does not contain a definition for 'Count'
This is because System.Array
implements the
ICollection.Count
property
explicitly. We'd have the same problem at compile time if we tried to use the
property via an expression of type Array
, for example by changing the type
of the collection
parameter:
Test.cs(9,60): error CS1061: 'System.Array' does not contain a definition for 'Count' and no extension method 'Count' accepting a first argument of type 'System.Array' could be found (are you missing a using directive or an assembly reference?)
The execution-time compiler is doing exactly what we should expect it to do - it's just somewhat annoying at the same time. This is a sort of impedance mismatch between the normal C# type system and dynamic typing.
Overloading ambiguity
One way of avoiding the explicit interface implementation issue is to use dynamic typing to perform execution-time overload resolution to find the most specific method - which then uses static typing instead. Again, there's an example of this in the book, trying to find the length of a sequence efficiently. However, there's a sneaky problem even in the book's example... the compiler won't always know which overload to choose. It copes with explicit interface implementation... but even the same simple attempt with an array will fail, unfortunately:
private static int CountImpl<T>(ICollection<T> collection)
{
return collection.Count;
}
private static int CountImpl(ICollection collection)
{
return collection.Count;
}
public static void PrintCount(IEnumerable collection)
{
dynamic d = collection;
int count = CountImpl(d);
Console.WriteLine(count);
}
...
PrintCount(new int[10]);
Here, int[]
implements both ICollection
and
ICollection<int>
- so we get the following error at execution time:
Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: The call is ambiguous between the following methods or properties: 'Test.CountImpl(System.Collections.Generic.ICollection )' and 'Test.CountImpl(System.Collections.ICollection)'
There are ways to overcome this, using some of the oddities of overloading - but it's not a particularly nice thing to have to do.
Where possible, use concrete classes for the overload parameters - that makes it much less likely
that you'll run into issues around ambiguous overloads. Also note that a parameter
with a type of dynamic
is equivalent to a parameter with a type of object
as far as the calling code is concerned.
Finally, remember that when the value of a dynamic expression being used for an argument
is null
, the overload resolution will be roughly equivalent to just passing in the literal
null
as the argument... there's no other type information available to
help the compiler work out what you want, so you can very easily end up with ambiguity.
Compound assignment
This issue bit me when I was writing about Summing a sequence dynamically. The C# language specification describes (in section 7.17.2) compound assignment operators as either performing the obvious "execute the operator and then assign the result back to the original variable" (where the result of the operator is implicitly convertible to the operator's return type) or (for cases where there's no implicit conversion, but there's an explicit conversion and another couple of conditions hold) it's the same, but with a cast before the assignment. That's what lets code like this compile and run:
byte b = 10;
b += 20;
Remember that there's no operator +(byte x, byte y)
operator - the above code will
promote the operands to int
, use the operator +(int x, int y)
and then
cast the result back to byte
automatically. It's a neat little convenience provided
by the compiler.
Now consider the following dynamic code:
byte b = 10;
dynamic d = b;
d += 20;
Console.WriteLine(d.GetType());
What should the result be here? Well, the value of d
before the compound assignment
is definitely byte
... so you might expect the C# compiler to do the same thing. Indeed,
it used to work that way, in some of the beta versions of .NET 4. However, apparently this caused some issues
elsewhere, so it was changed before the final release to not perform the cast - and the result is that
d
is an int
, so the above code prints System.Int32
.
I doubt that this particular gotcha will hit many developers, but it's worth being aware of... it confused the heck out of me when the behaviour changed.
Anonymous types
You can use anonymous types with dynamic typing, and indeed this is fairly common in ASP.NET MVC. However,
you need to be aware that the "real" types used to implement anonymous types (generated by the compiler) are
internal. Dynamic typing obeys the normal rules of access control, so if you want to use an instance of an
anonymous type dynamically from a different assembly, you need to use [InternalsVisibleTo]
in order
to grant access from the assembly containing the type to the assembly using it.
Generics
Dynamic typing is restricted by generics, in that even though you can convert an expression of type dynamic
to any other (non-pointer) type, that doesn't apply when dynamic
is a type argument - the type Foo<dynamic>
is statically bound, not dynamically bound. Here's an example with LINQ,
where we're squaring some numbers. We know the result will really be a list of integers, to we try assigning to a
List<int>
:
IEnumerable<dynamic> numbers = new dynamic[] { 42 };
List<int> squares = numbers.Select(d => d * d).ToList();
That doesn't work, because the result of the ToList
call is actually a List<dynamic>
, which will
be a List<object>
as far as the CLR is concerned. We have to cast each element to int
instead,
either by casting in the lambda expression or using the Cast
method:
IEnumerable<dynamic> numbers = new dynamic[] { 42 };
List<int> squares = numbers.Select(d => d * d).Cast<int>().ToList();
Tasks provide another example of this:
private static int Square(int number)
{
return number * number;
}
static void Main(string[] args)
{
dynamic number = 42;
// Invalid: no conversion from Task<dynamic> to Task<int>
Task<int> task = Task.Run(() => Square(number));
Task<int> valid = Task.Run(new Func<int>(() => Square(number)));
Task<int> alsoValid = Task.Run(() => (int)Square(number));
}
Here the expression Square(number)
is of type dynamic
, because almost all expressions using dynamic values
have a dynamic
result type. So the first call to Task.Run
has
a return type of Task<dynamic>
which in turn can't be converted to Task<int>
.
WCF, IClientChannel
and proxies
WCF uses RealProxy
for service proxies. Amazingly enough, RealProxy
does work with dynamic
, so long as a call
to GetType
returns the right type. The proxy returned by WCF implements both IClientChannel
and the API interface of the service you're proxying - but GetType
only returns the API interface. So if you try to call any of the members of IClientChannel
via a dynamic value, it fails. This is similar to explicit interface implementation - it's as if the proxy implemented IClientChannel
explicitly.
Conclusion
None of the behaviour described on this page should be described as a bug in the C# compiler (either the "normal" one or the embedded version used for dynamic execution. These are simply things you might not otherwise think about. You definitely need to be careful when using dynamic typing - it's powerful, but it doesn't always work exactly as you might expect.