Home > OS >  C# - In a Generic Class, How Can I Set a Generically-Typed Function Delegate?
C# - In a Generic Class, How Can I Set a Generically-Typed Function Delegate?

Time:01-27

I have a generic class of two types, "MyClass<T,U>". Based on a parameter to the class constructor, I'd like to be able to set a "Func<T,U>" local variable in a class instance that can be called to efficiently invoke a static method with input type T and output type U. The work done on the input variable depends on the input type. Can this be done?

Here's some code I've been playing with...

namespace ConsoleApp {

    public class MyClass<T, U> {
        // First constructor.  Pass in the worker function to use.
        public MyClass(Func<T, U> doWork) {
            _doWork = doWork;
        }
        // Second constructor.  Pass in a variable indicating the worker function to use.
        public MyClass(int workType) {
            if (workType == 1) _doWork = Workers.Method1;
            else if (workType == 2) _doWork = Workers.Method2;
            else throw new Exception();
        }
        // User-callable method to do the work.
        public U DoWork(T value) => _doWork(value);
        // Private instance variable with the worker delegate.
        private Func<T, U> _doWork;
    }

    public static class Workers {
        public static ushort Method1(uint value) => (ushort)(value >> 2);
        public static uint Method1(ulong value) => (uint)(value >> 1);
        public static ushort Method2(uint value) => (ushort)(value >> 3);
        public static uint Method2(ulong value) => (uint)(value >> 4);
    }

    public class Program {
        public static void Main(string[] args) {
            var mc1 = new MyClass<uint, ushort>(Workers.Method1);
            var mc2 = new MyClass<ulong, uint>(Workers.Method1);
            var mc3 = new MyClass<uint, ushort>(Workers.Method2);
            var mc4 = new MyClass<ulong, uint>(Workers.Method2);
            var mc5 = new MyClass<uint, ushort>(1);
            var mc6 = new MyClass<ulong, uint>(1);
            var mc7 = new MyClass<uint, ushort>(2);
            var mc8 = new MyClass<ulong, uint>(2);
        }
    }

}

The first constructor works just fine: the compiler is able to infer the correct overload of the static worker method to pass as a parameter, which gets stored in the instance variable _doWork, and can be (reasonably) efficiently called.

The second constructor won't compile, however, The problem is the assignments to _doWork which fail because "No overload for 'Method_' matches delegate 'Func<T,U>'". I sort of get it but sort of don't. It seems the compiler knows what T and U are at compile time, is "substituting" them into the class definition when compiling, and, so, ought to be able to infer which worker method to use. Anyone know why not?

Anyway, for reasons not worth going into, I'd really like to make the second constructor work. The obvious thing to try is to "cast" Method1 or Method2 to Func<T,U>, but delegates aren't objects and can't be cast. I've found a couple of pretty ugly ways to do it (that are also horribly inefficient), but I can't help but feeling there is something easier I'm missing. Any other ideas?

EDIT: It sounds like I'm abusing generics. What I have are about 100 different combinations of possible T, U, Worker values (there's actually a fourth dimension, but ignore that), each that behave somewhat differently. I'm trying to avoid having to create a separate class for each combination. So this isn't "generics" in the sense of being able to plug in any types T and U. What, if any, alternatives are there?

CodePudding user response:

Have you considered using something like a factory pattern and resolving the service in a manner similar to this example

void Main()
{
    var serviceCollection = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
    serviceCollection.AddSingleton<IMessageDeliveryProcessor, InAppNotificationMessageProcessor>();
    serviceCollection.AddSingleton<IMessageDeliveryProcessor, MessageProcessor>();
    serviceCollection.AddSingleton<IMessageProcessorFactory, MessageProcessorFactory>();
    
    var serviceProvider = serviceCollection.BuildServiceProvider();
    
    var factoryItem = serviceProvider.GetService<IMessageProcessorFactory>();
    var service = factoryItem.Resolve(DeliveryType.Email);
    
    service.ProcessAsync("", "", "");
}

public enum DeliveryType
{
    Email,
    InApp,
}


public class MessageProcessorFactory : IMessageProcessorFactory
{
    private readonly IServiceProvider _serviceProvider;

    public MessageProcessorFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;

    public IMessageDeliveryProcessor? Resolve(DeliveryType deliveryType)
        => _serviceProvider
            .GetServices<IMessageDeliveryProcessor>()
            .SingleOrDefault(processor => processor.DeliveryType.Equals(deliveryType));
}

public interface IMessageProcessorFactory
{
    IMessageDeliveryProcessor? Resolve(DeliveryType deliveryType);
}


public interface IMessageDeliveryProcessor
{
    
    DeliveryType DeliveryType { get; }
    Task ProcessAsync(string applicationId, string eventType, string messageBody);
}

public class InAppNotificationMessageProcessor : IMessageDeliveryProcessor
{
    public DeliveryType DeliveryType => DeliveryType.InApp;

    public Task ProcessAsync(string applicationId, string eventType, string messageBody)
    {
        Console.Write("InAppNotificationMessageProcessor");
        return Task.CompletedTask;
    }
}

public class EmailNotificationMessageProcessor : IMessageDeliveryProcessor
{
    public DeliveryType DeliveryType => DeliveryType.Email;

    public Task ProcessAsync(string applicationId, string eventType, string messageBody)
    {
        Console.Write("MessageProcessor");
        return Task.CompletedTask;
    }
}

This doesnt address your code and your issue exactly, but based on what I see of your issue, this could help you in the direction of travel.

CodePudding user response:

In your second constructor, you are attempting to assign something not directly compatible. What you're assigning is a method group, of which nothing in the method group can match a T or a U using the compiler's type inference rules.

One thing you can do is instead of trying to assign the delegates directly in your second destructor, you can instead assign a dispatcher method that will resolve this at runtime.

Your constructor could be changed to

public MyClass(int workType)
{
    if (workType == 1) _doWork = Method1Dispatcher;
    else if (workType == 2) _doWork = Method2Dispatcher;
    else throw new Exception();
}

where you have dispatcher methods such as

public U Method1Dispatcher(T value)
{
    return value switch
    {
        uint x => (U)(object)Workers.Method1(x),
        ulong x => (U)(object)Workers.Method1(x),
        _ => throw new NotSupportedException()
    };
}

public U Method2Dispatcher(T value)
{
    return value switch
    {
        uint x => (U)(object)Workers.Method2(x),
        ulong x => (U)(object)Workers.Method2(x),
        _ => throw new NotSupportedException()
    };
}

These methods use a double cast to get around the compile-time checks that prevent you from "equating", for instance, a uint and a T. Casting to object removes that constraint, and casts to another type, at runtime, could either succeed or fail. That's not typesafe, but if implemented carefully like the above, you at least encapsulate known (to us not the compiler) safe casts.

To test that this works, you can modify your Main method to prove it

var mc5 = new MyClass<uint, ushort>(1);
var mc5Result = mc5.DoWork(5);
Console.WriteLine($"Got result {mc5Result} of type {mc5Result.GetType().Name}");
var mc6 = new MyClass<ulong, uint>(1);
var mc6Result = mc6.DoWork(6);
Console.WriteLine($"Got result {mc6Result} of type {mc6Result.GetType().Name}");
var mc7 = new MyClass<uint, ushort>(2);
var mc7Result = mc7.DoWork(7);
Console.WriteLine($"Got result {mc7Result} of type {mc7Result.GetType().Name}");
var mc8 = new MyClass<ulong, uint>(2);
var mc8Result = mc8.DoWork(8);
Console.WriteLine($"Got result {mc6Result} of type {mc8Result.GetType().Name}");

Now, while this works, it's probably not the best solution because you say there are hundreds of combinations. Perhaps you can replace the switch with a reflection based way of obtaining the correct method, and then invoking it.

  • Related