Dynamic in C# IV: The Phantom Method

Yes, this does sound like a Star Wars movie, but no, I'm not a Star Wars geek that just likes to pull lines from my favorite movies (though I rather enjoyed Star Wars). This post will deal with what we've coined "the phantom method". It's the method that the static compiler will bind to during the initial binding phase when it recognizes that the invocation its trying to bind needs to be bound dynamically and cannot be resolved statically. It uses the rules that we talked about last time to determine what types to use at runtime.

Lets consider a simple example:

public class C
{
    public void Foo(int x)
    {
    }

    static void Main()
    {
        dynamic d = 10;
        C c = new C();
        c.Foo(d);
    }
}

When we try to bind the call to Foo, the compiler's overload resolution algorithm will construct the candidate set containing the sole candidate, C.Foo(int). At that point, we consider whether or not the arguments are convertible. But wait! We haven't talked about the convertibility of dynamic yet!

Lets take a quick segue into talking about the convertibility of the dynamic type.

Dynamic conversions

The quick and easy way to think about dynamic conversions is that everything is convertible to dynamic, and dynamic is not convertible to anything. "Wait a sec!", you say. "That doesn't make any sense!" And you're absolutely right, it doesn't make any sense - not until we talk about the special handling of dynamic in situations where you would expect convertibility.

In each of these special situations that you would expect some sort of conversion, the dynamic type signifies that the conversion is to be done dynamically, and the compiler generates all the surrounding DLR code that prompts a runtime conversion.

Lets let the local variable "c" denote some static typed local, and the variable "d" denote some dynamically typed expression. The special situations in question are the following:

  1. Overload resolution - c.Foo(d)
  2. Assignment conversion - C c = d
  3. Conditionals - if (d)
  4. Using clauses - using (dynamic d = ...)
  5. Foreach - foreach (var c in d)

We'll look at overload resolution today and explore the concepts, and leave the remaining scenarios as excercises for the reader. :)

Overload resolution

Back to argument convertibility. Since dynamic is not convertible to anything else, our argument d is not convertible to int. However, since we've got a dynamically typed argument, we really want the overload resolution for this call to be bound dynamically. Enter the phantom method.

The phantom method is a method which is introduced into the candidate set that has the same number of parameters as the number of arguments given, and each of those parameters is typed dynamic.

When the phantom method is introduced into the candidate set, it is treated like any other overload. Recall that since dynamic is not convertible to any other type, but all types are convertible to dynamic. This means that though all (well, not really all, but we'll discuss that later) of the normal overloads will fail due to the dynamic arguments being present, the phantom method will succeed.

In our example, we have one argument which is typed dynamic. We also have two overloads: Foo(int) and Foo(dynamic). The first overload fails because dynamic is not convertible to int. The second, the phantom, succeeds and so we bind to it.

Once a call is bound to the phantom overload, the compiler knows to generate the correct DLR magic to signal dispatching the call at runtime.

Only one question remains: when does the phantom overload get introduced?

Introduction of the phantom overload

When the compiler performs overload resolution, it considers each overload in the initial candidate set. If the invocation has any dynamic arguments, then for each candidate in the initial set, the compiler checks to see if the phantom overload should be introduced. The phantom will be introduced if:

  1. All of the non-dynamic arguments are convertible to their respective parameters.
  2. At least one of the dynamic arguments is not convertible to its respective parameter.

Recall that earlier we had said that it would be possible for a call containing a dynamic argument to be dispatched statically instead of dynamically. This is explained by condition 2. If the overload in question contains dynamic parameters for each of the dynamic arguments, then the binding will be dispatched statically.

The following example will not yield a dynamic lookup, but will be bound statically:

public class C
{
    public void Foo(int x, dynamic y) { ... }

    static void Main()
    {
        C c = new C();
        dynamic d = 10;
        c.Foo(10, d);
    }
}

Once the compiler has gone through each of the overloads in the initial binding pass, if the phantom has not yet been introduced, then overload resolution will behave as it always has, despite the occurrence of a dynamic parameter.

How is the phantom dispatch different than dispatch from a dynamic receiver?

It is important to note that there is a subtle difference between dispatch signaled from the phantom method and dispatch signaled from a dynamic receiver.

With a dynamic receiver, the overloads that the runtime binder will consider are determined based on the runtime type of the receiver. However, with the phantom dispatch, the overloads will be determined based on the compile time type of the receiver.

This is because of intuition - one would expect that though the arguments are dynamic, the receiver is known at compile time, and so the candidate set that the call can dispatch to should be known at compile time as well.

More specifically, one would not expect some overload defined in some derived class (possibly not defined in one's own source!) to be called. This is precisely the consideration we took when designing the behavior of the dynamic dispatch.

So what's next?

Next time, we'll apply the rules and concepts that we talked about today to other invocations - operators, conversions, indexers, and properties.

kick it on DotNetKicks.com

No Comments