Dynamic in C# V: Indexers, Operators, and More!

Now that we're all experts in how dynamic invocations work for regular method calls, lets extrapolate from our previous discussion about phantom methods a bit and take a look at how those basic concepts apply to other dynamic operations.

Today we'll just go through a laundry list of each type of operation, and throw in a few caveats and gotchas (limitations really, but that's such a negative word) that come along with the whole package. As always, I'll try to give some insights as to why we made the decisions that we did, and if there are workarounds for certain scenarios, I'll definitely point them out.

So without further ado, lets hit the list!

Properties

Named properties take the form d.Foo, where d is some dynamic object and Foo is some name for some field or property that lives on the runtime type of d. When the compiler encounters this, it encodes the name "Foo" in the payload, and instructs the runtime binder to bind off of the runtime type of d.

Note however, that named properties are always used in context! You can do one of three things with these guys - access the value, assign a value to the member, or do  both (compound operations, such as += etc). The compiler will thus encode the intent of the usage in the payload as well, so that the runtime will allow you to bind to a get-only property only if you're trying to access it, and will throw you an error if you're trying to assign to it.

The thing to note here is that the compiler will treat any named thing the same, and allow the runtime to differentiate between properties and fields.

The return type of these guys is dynamic at compile time.

Indexers

You can think of indexers in one of two ways - properties with arguments, or method calls with a set name. The latter is a much more useful way to think of these guys when we're dealing with dynamic. The reason is that just like method calls, even if the indexer itself can be statically bound, any dynamic arguments that don't directly map to dynamic can cause the phantom overload to come into play, and cause a late binding based on the static type of the receiver, and the dynamic types of the arguments.

They still do have some similarities to properties however - they're always used in context. As such, the compiler again will encode whether or not the user is accessing the value of the indexer, setting a value, or performing a compound operation into the payload for the binder to use.

The return type of these guys is also dynamic at compile time.

Conversions

Last time we mentioned that the although dynamic is not convertible to any other type, there are certain scenarios in which we allow it to be convertible. Assignments, condition expressions, and foreach iteration variables are a few examples.

These payloads are quite simple - because the compiler already knows the type that we're attempting to convert to (ie the type of the variable you're assigning to), it simply encodes the conversion type in the payload, indicating to the runtime binder that it should attempt all implicit (or explicit if its a cast) conversions from the runtime type of the argument to the destination type.

Note that user-defined conversions will be applied as well. We worked pretty hard to make sure that the runtime semantics will behave just like the compile time ones, so argument conversions for overload resolution and the like will all happen exactly as you'd expect.

These guys return the destination type at compile time. Note that these guys are the only guys who have a non-dynamic return type at compile time.

Operators

Operators are a bit of a strange beast. At first glance, it's hard to tell that anything dynamic is going on. However, a simple statement like d+1 still needs to be dispatched at runtime, because user-defined operators can come into play.

As such, any operation that has a dynamic argument will be dispatched at runtime. This includes all of the in place operators as well (+=, -= etc).

Note that the compiler will do the magic to figure out if you've got a member assignment (ie d.Foo += 10) or a variable assignment (ie d += 10), and figures out if it needs to pass d by ref to the call site so that it can be mutated. Note also that structs will get mutated as well! So if we were to do:

 public struct S
{
    public int Foo;
}

public class C
{
    static void Main()
    {
        dynamic d = new S();
        d.Foo += 10;
    }
}

the result would be that d would point to a struct who's Foo member is set to 10.

Lastly, note that the compiler knows that if you're doing something like d.Foo += x, and at runtime d.Foo binds to a delegate type or an event type, then the correct combine/add call will be invoked for you.

Delegate invoke

The invocation syntax is very much like a method call. The only difference is that the name of the action is not explicitly stated. This means that just like calls, both of the following examples would end up causing runtime dispatches:

 public class C
{
    static void Main()
    {
        MyDel  c = new MyDel();
        dynamic d = new MyDel();
        d();
        c(d);
    }
}

The first example causes a runtime dispatch of an invoke that takes no arguments. At runtime, the binder will check to verify that the recevier is indeed a delegate type, and then perform overload resolution to make sure the arguments supplied match the delegate signature.

The second example causes a runtime dispatch because the argument specified is dynamic. The compiler determines at compile time that we have an invoke of a delegate since c's type is a delegate, but the actual resolution must be done at runtime.

 

Okay, that's enough laundry listing for today. Next time we'll look at a few small caveats of things that aren't allowed in dynamic contexts. After that, I think we'll switch gears and start looking at some other VS 2010 features - named arguments, optional parameters, and more!

kick it on DotNetKicks.com