Multiple Dispatch with Generics

2019-07-12 18:49发布

问题:

I'm trying to abstract away my interface implementations by providing a factory/builder using generics. However, I'm running into an issue with multiple dispatch and C# generics at run time that's doing something that seems odd.

The basic scenario is I've defined several interfaces:

public interface IAddressModel
{
}

public interface IUserModel
{
}

Then I have a factory class to return the actual implementations:

public class Factory
{
    public T BuildModel<T>()
    {
        return BuildModel(default(T));
    }

    public object BuildModel(object obj)
    {
        //this is here because the compiler will complain about casting T to
        //the first inteface parameter in the first defined BuildModel method
        return null;
    }

    public IAddressModel BuildModel(IAddressModel iModel)
    {
        //where AddressModel inherits from IAddressModel
        return new AddressModel();
    }

    public IUserModel BuildModel(IUserModel iModel)
    {
        //where UserModel inherits from IUserModel
        return new UserModel(); 
    }
}

The issue is when the factory is called like this: new Factory().BuildModel<IAddressModel>() The BuildModel(...) method that is dispatched at run time from generics is always the least derived form of T, in this case always object.

However, if you call new Factory().BuildModel(default(IAddressModel)); the correct method is displatched (most likely because this is done at compile time). It seems that dynamic dispatch with generics doesn't check the methods for the most derived type even though the method called should be the same whether it's done at compile time or run time. Ideally I'd like to make the BuildModel(...) methods private and only expose the generic method. Is there another way to get dynamic displatch to call the correct method at run time? I've tried changing the BuildModel<>() implementation to return BuildModel((dynamic)default(T)) but this throws a run time error about not being able to determine which method to dispatch. Is there maybe a way to do this with contravariance and more interfaces?

回答1:

You might be able do the dispatch yourself based on the argument type T:

public class Factory
{
    private Dictionary<Type, Func<object>> builders = new Dictionary<Type, Func<object>>
    {
        { typeof(IAddressModel), BuildAddressModel },
        { typeof(IUserModel), BuildUserModel }
    };

    public T Build<T>()
    {
        Func<object> buildFunc;
        if (builders.TryGetValue(typeof(T), out buildFunc))
        {
            return (T)buildFunc();
        }
        else throw new ArgumentException("No builder for type " + typeof(T).Name);
    }

    private static IAddressModel BuildAddressModel()
    {
        return new AddressModel();
    }

    private static IUserModel BuildUserModel()
    {
        return new UserModel();
    }
}


回答2:

The current state of the code requires an explicit cast to compile.

public T BuildModel<T>()
{
    return (T)BuildModel(default(T));
}

BuildModel is treating T polymorphically as an object. BuildModel does not know that T is an IAddressModel or an IUserModel unless you define such a restriction:

public T BuildModel<T>() where T: IAddressModel
{            
    Console.WriteLine(typeof(T));
    return (T)BuildModel(default(T));
}

Now, the compiler has enough information to recognize that T is an IAddressModel. But, what you are after is for object to become a more derived parameter (covarient) which is not type safe. In other words, C# does not support covarient parameter types because it is not type safe.

You can still achieve factory like behavior via conditional logic:

    public T BuildModel<T>()
    {
        T result = default(T);

        if (typeof(T) == typeof(IAddressModel))
            result = (T)BuildModel((IAddressModel)result);
        else if (typeof(T) == typeof(IUserModel))
            result = (T)BuildModel((IUserModel)result);

        return result;
    }