Emulating delegates with free generic type parameters in C#

廉价感情. 提交于 2019-11-29 06:53:16

I think the way you emulate this in the language is by not using delegates but interfaces. A non-generic interface can contain a generic method, so you can get most of the behavior of delegates with open type arguments.

Here is your example re-worked into a valid C# program (Note that it still requires the Factory class you defined):

public interface IWorker
{
    ICollection<T> DoWork<T>(IEnumerable<T> values);
}

public class ListCreationWorker : IWorker
{
    public ICollection<T> DoWork<T>(IEnumerable<T> values)
    {
        return Factory.CreateList<T>(values);
    }
}

public class SetCreationWorker : IWorker
{
    public ICollection<T> DoWork<T>(IEnumerable<T> values)
    {
        return Factory.CreateSet<T>(values);  
    }
}

public static class Program {
    public static void Main(string[] args) {
        string[] values1 = new string[] { "a", "b", "c" };
        int[] values2 = new int[] { 1, 2, 2, 2 };

        IWorker listWorker = new ListCreationWorker();
        IWorker setWorker = new SetCreationWorker();

        ICollection<string> result1 = listWorker.DoWork(values1);
        ICollection<int> result2 = listWorker.DoWork(values2); //.Count == 4
        ICollection<int> result3 = setWorker.DoWork(values2); //.Count == 2
    }
}

public static class Factory
{
    public static ICollection<T> CreateSet<T>(IEnumerable<T> values)
    {
        return new HashSet<T>(values);
    }

    public static ICollection<T> CreateList<T>(IEnumerable<T> values)
    {
        return new List<T>(values);
    }
}

You still get the important feature of separating the decision of which method to call from the execution of said method.

One thing that you cannot do, however, is store any state in the the IWorker implementations in a generic fashion. I'm not sure how that could be useful because the DoWork method could be called with different type arguments every time.

This does actually not make sense under .Net's type system.

What you're describing is a type constructor – a "function" that takes one or more types and returns a concrete (parameterized, or closed) type.

The problem is that type constructors themselves are not types. You cannot have an object or variable of an open type; type constructors can only be used to generate concrete types.

In other words, there is no way to represent a reference to an open function within .Net's type system.


The best you can do is to use reflection; a MethodInfo can describe an open generic method.
You can get a compile-time type-safe reference to an open MethodInfo by writing a generic method that takes an expression tree with a fake generic parameter:

public MethodInfo GetMethod<TPlaceholder>(Expression<Action> method) {
    //Find the MethodInfo and remove all TPlaceholder parameters
}

GetMethod<string>(() => SomeMethod<string>(...));

The TPlaceholder parameter is necessary in case you want to reference an open generic method with a constraint on that parameter; you can pick a placeholder type that meets the constraint.

The solution is interfaces. As @mike-z wrote, interfaces support generic methods. So, we can create the non-generic interface IFactory with generic method which incapsulates a reference to a generic method in some class. To bind the generic method of a [Factory] class using such interface we usually need to create small classes implementing the IFactory interface. They act just like closures used by lambdas.

I don't see big semantic difference between this and the generic method delegates that I've asked for. The solution is very much like what compiler does for lambdas [that just call other methods] (create closure with the method that calls the).

What are we losing? Mostly syntactic sugar.

  • Anonymous functions/lambdas. We cannot create generic lambdas. Being able to create anonymous classes (like in Java) would have solved the problem. But this isn't much of a problem to begin with as lambdas are just syntactic sugar in .Net.

  • Ability to implicitly create delegate/link from the method group (C# term). We cannot use the method group in any way if it's generic. This doesn't affect the semantics too.

  • Ability to define generic delegates is impeded. We cannot make a generic IFactory<U, V> interface with method V<T> Create<T>(U<T> arg). This is not a problem too.

This is the code of the solution. The Factory class from the question is unchanged.

public interface IFactory {
    ICollection<T> Create<T>(IEnumerable<T> values);
}

public class Worker { //not generic
    IFactory _factory;

    public Worker(IFactory factory) {
        _factory = factory;
    }

    public ICollection<T> DoWork<T>(IEnumerable<T> values) { //generic method
        return _factory.Create<T>(values);
    }
}

public static class Program {
    class ListFactory : IFactory {
        public ICollection<T> Create<T>(IEnumerable<T> values) {
            return Factory.CreateList(values);
        }
    }

    class SetFactory : IFactory {
        public ICollection<T> Create<T>(IEnumerable<T> values) {
            return Factory.CreateSet(values);
        }
    }

    public static void Main() {
        string[] values1 = new string[] { "a", "b", "c" };
        int[] values2 = new int[] { 1, 2, 2, 2 };

        Worker listWorker = new Worker(new ListFactory());
        Worker setWorker = new Worker(new SetFactory());

        ICollection<string> result1 = listWorker.DoWork(values1);
        ICollection<int> result2 = listWorker.DoWork(values2); //.Count == 4
        ICollection<int> result3 = setWorker.DoWork(values2); //.Count == 2
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!