Mike
Mike

Reputation: 17

Argument not assignable due to contravariance

I'm trying to write a simple library that can convert between different types. Using .Net Core 3.1.

The purpose of this would be to convert between two classes that inherit from the same base class.

public class SharedBaseClass {}

public class DestinationClass : SharedBaseClass {
    public DestinationClass(){}
}

public class InputClass : SharedBaseClass {
    public InputClass(){}
}

As such, I have introduced an interface that defines such a converter

public interface IConverter<out TU> where TU : SharedBaseClass
{
    public TU Convert(SharedBaseClass input);
}

This interface is then used by the below class to perform the conversions

public class ConverterExecutor
{
    private readonly Dictionary<Type, IConverter<SharedBaseClass>> _converters;
    
    public ConverterExecutor(Dictionary<Type, IConverter<SharedBaseClass>> converters)
    {
        _converters = converters;
    }
    
    public IEnumerable<SharedBaseClass> ConvertMultiple(IEnumerable<SharedBaseClass> classesToConvert)
    {
        var converted = new List<SharedBaseClass>();
        
        foreach(var toConvert in classesToConvert)
        {
            _converters.TryGetValue(toConvert.GetType(), out var converter);
            
            if (converter != null) {
                converted.Add(converter.Convert(toConvert));
                continue;
            }
                              
            converted.Add(toConvert);
        }
        
        return converted;
    }
}

Clients would then simply create implementations of the IConverter interface to encapsulate conversion logic. An example for a converter from InputClass to DestinationClass would is

public class DestinationConverter: IConverter<DestinationClass> {
    public DestinationClass Convert(SharedBaseClass input) {
        return new DestinationClass();
    }
}

To complete the example, I've added a short main method of how these would be setup

public class Program
{
    public static void Main()
    {
        var executor = new ConverterExecutor(new Dictionary<Type, IConverter<SharedBaseClass>>{
            // various converters for various types added here
            {typeof(InputClass), new DestinationConverter()}
        });
        
        var result = executor.ConvertMultiple(new List<SharedBaseClass>{new InputClass()});
        Console.WriteLine(result.First());
    }
}

This all works, however I am bothered by the fact hat the input parameter of the convert method for an implementation of IConverter is dependent on the base class.

public DestinationClass Convert(SharedBaseClass input)

To fix this, I tried redefining the interface as such:

public interface IConverter<out TU, in T> where TU : SharedBaseClass where T: SharedBaseClass
{
    public TU Convert(T input);
}

This refactoring works fine across the involved classes and gives me the correct type within each implementation, however I am getting a compile error within the main method as the signature of the DestinationConverter class is not suitable to be added to the dictionary. I suspect this comes from the fact that the T: SharedBaseClass parameter has been added as an in parameter (contravariant) on the IConverter interface, however leaving it as invariant fails in the same way. I suspect if it would be covariant (not possible with an input param) the compiler would allow this. I'm thinking I got my abstractions wrong somewhere along the way, so what would be a suitable solution in this case?

public class Program
{
    public static void Main()
    {
        var executor = new ConverterExecutor(new Dictionary<Type, IConverter<SharedBaseClass, SharedBaseClass>>{
            // various converters for various types to be added here
            {typeof(InputClass), new DestinationConverter()}
        });
        
        var result = executor.ConvertMultiple(new List<SharedBaseClass>{new InputClass()});
        Console.WriteLine(result.First());
    }
}

Full refactored example that is failing: https://dotnetfiddle.net/vuDCiH

Upvotes: 0

Views: 61

Answers (1)

canton7
canton7

Reputation: 42320

The root of the problem is that you can get any converter out of your dictionary, and give it your SharedBaseClass instance to convert. Therefore the converters in your dictionary need to be declared as accepting a SharedBaseClass.

If your dictionary accepts converters which take a SharedBaseClass instance, then all converters which go into it must also be able to take a SharedBaseClass instance to convert, as you're technically able to fetch any one of them out of the dictionary, and give it any SharedBaseClass instance.

Any way forwards therefore hinges on us being able to get rid of that dictionary containing IConverter<SharedBaseClass, SharedBaseClass> instances. One possible approach is:

public class ConverterExecutor
{
    private readonly Dictionary<Type, Func<SharedBaseClass, SharedBaseClass>> _converters = new();
    
    public void RegisterConverter<TU, T>(IConverter<TU, T> converter) where TU : SharedBaseClass where T : SharedBaseClass
    {
        _converters[typeof(T)] = x => converter.Convert((T)x);  
    }
    
    public IEnumerable<SharedBaseClass> ConvertMultiple(IEnumerable<SharedBaseClass> classesToConvert)
    {
        var converted = new List<SharedBaseClass>();
        
        foreach(var toConvert in classesToConvert)
        {
            _converters.TryGetValue(toConvert.GetType(), out var converter);
            
            if (converter != null) {
                converted.Add(converter(toConvert));
                continue;
            }
                              
            converted.Add(toConvert);
        }
        
        return converted;
    }
}

Then:

var executor = new ConverterExecutor();
executor.RegisterConverter(new DestinationConverter());

Link.

We've replaced those IConverter<SharedBaseClass, SharedBaseClass> instances with delegates which take a SharedBaseClass and return a SharedBaseClass. Each delegate holds onto a converter, and casts the SharedBaseClass instance into the type which the converter is expecting.

Now if you pass the wrong type to a particular converter you get an InvalidCastException: the problem hasn't really gone away, but we've moved the check from compile-time to runtime.

Upvotes: 1

Related Questions