Can I create a Dictionary of Funcs that vary in their first argument?

347 views Asked by At

I have a bunch of static methods that all have the same signature, except for the type of their first argument, for example:

public static class ElementCreators
{
    public static XmlElement CreateElementForString
               (string s, string name, string namespaceURI)
    {
        [...]
    }

    public static XmlElement CreateElementForDecimal
               (Decimal d, string name, string namespaceURI)
    {
        [...]
    }
}

I want to create a Dictionary (or some sort of lookup that can be modified at runtime - people are supposed to be able to add their own funcs, though once a func is added, it doesn't need to be modified/removed, and there are never two+ funcs for a given type. There might be funcs for base and derived types, but in that case, it's up to the user to register them e.g., in the right order) to dispatch based on the type, e.g.:

var funcs = new Dictionary<Type, Func<object, string, string, XmlElement>>();
funcs[typeof(string)] = ElementCreators.CreateElementForString;
funcs[typeof(Decimal)] = ElementCreators.CreateElementForDecimal;

Now, this doesn't work, as there is no contravariance between delegates, so the compiler complains that CS0123 No overload for 'CreateElementForString' matches delegate 'Func<object, string, string, XmlElement>'.

One option is to create another delegate as the middle-man:

funcs[typeof(string)] =
            (o,s1,s2) => ElementCreators.CreateElementForString((string)o, s1, s2);

That works, but is a) ugly and b) introduces a bunch of unnecessary delegates.

Generics don't seem to be an option, because the Func can not be of an open type T. Similarly, dynamic doesn't work, but I don't want to use these anyway (runtime cost).

I could introduce a level of indirection for each method, which avoids the delegate, but it isn't any less ugly:

public static XmlElement CreateElementForString(object s, string name, string namespaceURI)
    => CreateElementForString((string)s, name, namespaceURI);

And of course, I could try automating something like that (T4 templates, Pre-Build task, custom Build Action, etc.)

But before I do that, I wonder if there's a better way that I've overlooked?

Visual Studio 2017, .NET 4.7.1, and C# 7.2 are all available for this.

2

There are 2 answers

0
Kevin Montrose On BEST ANSWER

As the comments cover, not really. But you can build a class that accomplishes what you want (relatively type safe, pleasant to use) I think.

public class DelegateDictionary
{
    Dictionary<Type, Delegate> Lookup;

    public DelegateDictionary()
    {
        Lookup = new Dictionary<System.Type, Delegate>();
    }

    public void Add<T>(Func<T, string, string, XmlElement> mtd)
    {
        Lookup.Add(typeof(T), mtd);
    }

    public XmlElement Invoke<T>(T value, string name, string namespaceURI)
    {
        if (!Lookup.TryGetValue(typeof(T), out var del)) throw new InvalidOperationException($"No delegate registered for {typeof(T).Name}");

        var typedDel = (Func<T, string, string, XmlElement>)del;
        return typedDel(value, name, namespaceURI);
    }
}

You do have to type the Add(...) call, it can't be inferred), but the Invoke(...) can be inferred (and presumably there are more invocations than registrations).

// example usage
{
    var dict = new DelegateDictionary();

    dict.Add<string>(ElementCreators.CreateElementForString);
    dict.Add<Decimal>(ElementCreators.CreateElementForDecimal);

    dict.Invoke("stringValue", "myName", "what-even-is-a-namespace");
    dict.Invoke(1.0m, "myName", "what-even-is-a-namespace");
}

I don't think you'll pay for anything besides the cast on invoke, but haven't profiled to confirm.

5
Ben Adams On

If you can be whole program static/global then you can use a very fast compile time dictionary (no-lookup) and generics (no boxing):

class Program
{
    void Main()
    {
        // Set up defaults; lambda rather than Method group (allocs)
        ElementCreators<string>.CreateElement = (s, name, namespaceURI)
                => ElementCreators.CreateElement(s, name, namespaceURI);
        ElementCreators<Decimal>.CreateElement = (d, name, namespaceURI)
                => ElementCreators.CreateElement(d, name, namespaceURI);

        // Call
        XmlElement xml = ElementCreators<string>.CreateElement("hello", "name", "ns");
    }

}

public static class ElementCreators<T>
{
    // Can change property get to throw KeyNotFound if null
    public static Func<T, string, string, XmlElement> CreateElement { get; set; }
}


public static class ElementCreators
{
    public static XmlElement CreateElement(string s, string name, string namespaceURI)
    {
        return null;
    }

    public static XmlElement CreateElement(Decimal d, string name, string namespaceURI)
    {
        return null;
    }
}