I have moved!

I've moved my blog
CLICK HERE

Tuesday, 6 May 2008

DynamicObject Wrapper

Charlie Calvert started a discussion way back in January about a proposal to add dynamic lookup support to C# - a convenient way to write code that makes calls into objects of a type not known at compile time.

A number of people have suggested that for the dynamic case, the member access syntax should be made deliberately different to the usual dot syntax. There is a lot to be said for this. The big danger of this whole idea is that a radically less type-safe way of working will start to creep into programs that used to be type-safe, thus undoing some of the good work done by adding generics. But as long as the syntax makes this clear, it's better to have convenient support than nothing at all, or else people will invent their own approach and it will probably be horrible.

I was thinking that maybe to underline the fact that the method name is really no more solid than a string, you could have the syntax mimic accessing items in a dictionary.

So instead of:

d.LetThereBeLight(); 

It would be:

d["LetThereBeLight"](); 

It's not a lot more typing, and it properly highlights the dynamic nature of what the code is doing. Any C# programmer would instantly get what it meant.

It also has an advantage that other syntaxes don't - it allows me to pass a string variable if I want to.

But then I realised we can already do that today. Here's a simplistic wrapper around any CLR object that provides that exact dictionary-like calling interface:

class DynamicObject
{
    private object m_target;

    public delegate object Member(params object[] args);

    public DynamicObject(object target)
    {
        m_target = target;
    }

    public Member this[object member]
    {
        get { return args =>
        {
            string name = member as string;
            if (name != null)
            {
                // Try to find a method (TODO: overloading)
                MethodInfo m = m_target.GetType().GetMethod(name);

                if (m != null)
                    return m.Invoke(m_target, args);

                // Then try for an ordinary property
                PropertyInfo p = 
                    m_target.GetType().GetProperty(name);

                if (p != null)
                {
                    // No args implies get
                    if (args.Length == 0)
                        return p.GetValue(m_target, null);

                    // Otherwise, set - first arg is new value
                    p.SetValue(m_target, args[0], null);
                    return null;
                }
            }

            // Otherwise try indexer
            PropertyInfo i = m_target.GetType().GetProperty("Item");
            if (i != null)
            {
                object[] memberArg = new object[] { member };

                if (args.Length == 0)
                    return i.GetValue(m_target, memberArg);

                i.SetValue(m_target, args[0], memberArg);
                return null;
            }

            // Give up
            throw new InvalidOperationException(
                "No such member: " + name);
        }; }
    }
}

The indexer simply returns a delegate that takes a variable number of arguments. It handles methods and properties, and gives them uniform syntax. It's good for pretty much anything that the CLR can call through reflection. If there is no method or property with the specified name, it forwards the request on to the target object's default indexer.

So suppose our mystery dynamic object has this implementation:

public class Universe
{
    public void LetThereBeLight()
    {
        Console.WriteLine("And there was light!");
    }

    public void CreateSexes(int n)
    {
        Console.WriteLine("And there were " + n 
            + " of every creature, which was plenty");
    }

    public int SpatialDimensions { get; set; }

    public string this[int day]
    {
        get
        {
            switch (day)
            {
                case 1: return "Heaven and Earth";
                case 2: return "Mammals";
                case 3: return "Fish";
                case 4: return "TV";
                case 5: return "Hi Def";
                case 6: return "Humans";
            }

            return "Rested";
        }
    }
}

So if I know the type, I would write code like this in the usual way:

Universe u = new Universe();

u.LetThereBeLight();
u.CreateSexes(2);

u.SpatialDimensions = 3;

Debug.Assert(u.SpatialDimensions == 3);

Debug.Assert(u[3] == "Fish");

But we can wrap one in a DynamicObject and still use it fairly conveniently:

DynamicObject d = new DynamicObject(new Universe());

d["LetThereBeLight"]();
d["CreateSexes"](2);

d["SpatialDimensions"](3);

Debug.Assert((int)d["SpatialDimensions"]() == 3);
Debug.Assert((string)d[3]() == "Fish");

So do we really need a new language feature?

5 comments:

paul said...

This is actually a pretty dang cool way to access any object dynamically. Thanks for posting!

Daniel Earwicker said...

Thanks for saying so!

Chris said...

Actually that's pretty similar to how Ruby works under-the-hood, if I'm not mistaken...

basically all method calls and property gets/sets are forwarded to a "call(x)" method

Steve Cooper said...

I have a feeling that this won't work with dynamic variables. Dynamic variables (objects implementing IDynamicObject) have exactly one method;

public interface IDynamicObject
{
MetaObject GetMetaObject(Expression parameter);
}

This means reflection will always fail.

For example, imagine a python list object, with methods like `append(item)` and `pop(number)`. Exposed as an IDynamicObject, calls to

pylist.GetType().GetMethod("append")

will return null because 'append' isn't in the list of methods; only 'GetMetaObject' is.

However, the syntax provided in C#4 allows for a cleverer interaction with the GetMetaObject which returns a delegate which, when called, invokes the `append` method. Allowing you to write;

// c# code.
pylist.append("banana");

int19h said...

One thing you're missing is that your solution really only covers the case of a "dynamic" receiver, while C# 4.0 will do dynamic calls for any combination of receiver and arguments that has at least one "dynamic" value. Also, it will do dynamic dispatch for operators applied to "dynamic" values, too.