Overload Resolution of dynamic/object types
February 9, 2010 by
Christoff Truter
C#
Last year sometime, I started doing some research about adding traditional overloads to PHP (you heard
me, hehe). Basically I created a mechanism that allow devs to overload methods in PHP - which
is quite interesting considering that we're talking about a loosely typed language - every
variable essentially dynamic by default, minus type hinting
Sooo I remembered that C# 4.0 added similar logic to its codebase and decided to have a look
at how C# resolves dynamic types within overloads.
Note: For intents and purposes dynamic types are equivalent to object. eg. substitute dynamic for object prior to C# 4.0 the difference however is that dynamic types get resolved at runtime, eg. late-binding (resolved by what the caller sent to it)
There are however a few things that didn't make sense to me, lets have a quick look at my confusion, consider the following snippet:
using System;
namespace ConsoleApp
{
class Program
{
static void f(Int32 x) { }
static void f(dynamic x) {}
static void f(Int32 x, dynamic y) {}
static void f(dynamic x, Int32 y) {}
static void Main(string[] args)
{
f(10); // Works
f(10, 10); // Ambiguous - obvious
}
}
}
- Passing value 10 to method f, will resolve to (Int32 x) - we assume that (Int32 x)
defines an exclusion from (dynamic x)?
- Passing values 10, 10 to method f, won't resolve - since its impossible to resolve, both
methods are equally callable.
It seems pretty straightforward and we can see some kind of pattern... or
can we?
Things get interesting as soon as we add a third parameter:
static void f(Int32 x, dynamic y, Int32 z) {}
static void f(dynamic x, Int32 y, dynamic z) {}
Building on our our previous assumptions, let's assume that x & z defines exclusions, by
these assumptions, passing values 10, 10, 10 should resolve (Int32 x, dynamic y, Int32 z).
Instead, the compiler informs us that we made an ambiguous call - nullifying my previous
assumptions.
Which brings me to what really happens
(still busy confirming this with a few
clever guys at microsoft), observe the following overloads (swapping the types of y & z of
the previous example):
static void f(Int32 x, dynamic y, Int32 z) {}
static void f(dynamic x, dynamic y, Int32 z) {}
Passing values 10, 10, 10 will resolve correctly to (Int32 x, dynamic y, Int32 z), providing
further clues to internal processes.
Resolving these overloads by hand, will look something like this:
- Cancel out matching types
- Cancel out least matching types eg if x is Int32 -> (Int32 > dynamic)
- In the end if we're not left with only one resolveable method, the methods are ambiguous
Another example:
At the end of the day, I can't fully agree with the resolution of dynamic/object types (regarding overloads) in C# - it is however a very safe design
(good language design?).
But currently I would lean towards overload resolution like the following:
- Cancel out matching types
- Cancel out least matching types eg if x is Int32 -> (Int32 > dynamic)
-
Cancel out methods containing less matching types eg. the second method only contains one definite match,
but our first method contains two matches - hence more resolveable.
- In the end if we're not left with only one resolveable method, the methods are ambiguous
What do you guys and girls think? Any thoughts?