Dynamic disappointment

I’ve been eagerly awaiting .NET 4 and the new dynamic feature in C#, but after taking the beta for a spin I’ve run in to a major disappointment. Let’s take dynamic for a quick spin and see what’s got me so devastated.

public class Dynamic : DynamicObject {
    Dictionary<String, object> members = new Dictionary<string, object>();
    public override bool TrySetMember(SetMemberBinder binder, object value) {            
        members[binder.Name] = value;
        return true;
    }
    public override bool TryGetMember(GetMemberBinder binder, out object result) {
        return members.TryGetValue(binder.Name, out result);
    }
}

[TestFixture]
public class Disappointment {
    [Test]
    public void CanCreateAndCallAMethod() {
        dynamic anObject = new Dynamic();
        anObject.AMethod = new Func<int>(() => 1);
        Assert.That(anObject.AMethod(), Is.EqualTo(1));
    }
}

Here we’ve created a sub-class of DynamicObject, which lets us play in the world of dynamic lookups from C#. We override TrySetMember(...) and TryGetMember(...) to use a dictionary as a backing store for members. Our test assigns a method called AMethod to a dynamic object at run time, then executes it. It passes! Awesome!

If it walks and quacks like a duck, too bad!

Let’s declare an ICanAdd interface, as well as a class that uses objects that support the ICanAdd interface to, well, add stuff.

public interface ICanAdd {
    int Add(int a, int b);
}

public class SomethingThatAdds {
    private ICanAdd adder;
    public SomethingThatAdds(ICanAdd adder) {
        this.adder = adder;
    }
    public int FirstNumber { get; set; }
    public int SecondNumber { get; set; }
    public int AddNumbers() {
        return adder.Add(FirstNumber, SecondNumber);
    }
}

We can add this method to our dynamic object so that it supports the same operations as the ICanAdd interface:

[Test]
public void CanCreateADynamicAdder() {
    dynamic adder = new Dynamic();
    adder.Add = new Func<int, int, int>((first, second) => first + second);
    Assert.That(adder.Add(1, 3), Is.EqualTo(4));
}

This works fine, but when we try to combine static and dynamic worlds we run into problems:

[Test]
public void CannotUseDynamicAdderForAnythingUseful() {
    dynamic adder = new Dynamic();
    adder.Add = new Func<int, int, int>((first, second) => first + second);
    var somethingThatCanAdd = new SomethingThatAdds(adder); /* Fails here at runtime */
    somethingThatCanAdd.FirstNumber = 10;
    somethingThatCanAdd.SecondNumber = 20;
    Assert.That(somethingThatCanAdd.AddNumbers(), Is.EqualTo(30));
}

This compiles, but at runtime we get the test failing with the following RuntimeBinderException:

DaveSquared.DynamicDisappointment.Disappointment.CannotUseDynamicAdderForAnythingUseful:
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException : 
The best overloaded method match for 'DaveSquared.DynamicDisappointment.SomethingThatAdds.SomethingThatAdds(DaveSquared.DynamicDisappointment.ICanAdd)' has some invalid arguments
  at CallSite.Target(Closure , CallSite , Type , Object )
  at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
  at DaveSquared.DynamicDisappointment.Disappointment.CannotUseDynamicAdderForAnythingUseful() ...

The exception is fairly clear – the C# RuntimeBinder is trying to call the SomethingThatAdds(ICanAdd) constructor, but we’ve given it a dynamic instance instead. Based on my fairly primitive understanding of this stuff, in order to integrate dynamic lookups into the statically typed CLR, dynamic is actually implemented as a static type. So even though we’re using dynamic member lookups at runtime, we still need to abide by the type system and pass methods the static types they expect.

If we modify our original SomethingThatAdds class to explicitly accept the dynamic type then our last test passes:

public class SomethingThatAdds {
    private dynamic adder;
    public SomethingThatAdds(dynamic adder) {
        this.adder = adder;
    }
    /* ... snip ... */
}

I’m aware I’m probably expecting too much, but having to explicitly modify our code in order to make this kind of use of the dynamic feature for duck typing is, well, disappointing. It would be great to see something like Jon Skeet’s dynamic<T> idea get into the final release so we can get the best of both worlds. :)

Comments