Я пытаюсь написать простую библиотеку, которая может конвертировать разные типы. Использование .Net Core 3.1.
Целью этого будет преобразование между двумя классами, которые наследуются от одного и того же базового класса.
public class SharedBaseClass {}
public class DestinationClass : SharedBaseClass {
public DestinationClass(){}
}
public class InputClass : SharedBaseClass {
public InputClass(){}
}
Таким образом, я представил интерфейс, который определяет такой преобразователь
public interface IConverter<out TU> where TU : SharedBaseClass
{
public TU Convert(SharedBaseClass input);
}
Затем этот интерфейс используется классом ниже для выполнения преобразований.
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;
}
}
Затем клиенты просто создавали реализации интерфейса IConverter для инкапсуляции логики преобразования. Примером конвертера из InputClass
в DestinationClass
будет:
public class DestinationConverter: IConverter<DestinationClass> {
public DestinationClass Convert(SharedBaseClass input) {
return new DestinationClass();
}
}
Чтобы завершить пример, я добавил краткий основной метод их настройки.
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());
}
}
Все это работает, однако меня беспокоит тот факт, что входной параметр метода convert для реализации IConverter зависит от базового класса.
public DestinationClass Convert(SharedBaseClass input)
Чтобы исправить это, я попытался переопределить интерфейс как таковой:
public interface IConverter<out TU, in T> where TU : SharedBaseClass where T: SharedBaseClass
{
public TU Convert(T input);
}
Этот рефакторинг отлично работает для задействованных классов и дает мне правильный тип в каждой реализации, однако я получаю ошибку компиляции в основном методе, поскольку сигнатура класса DestinationConverter не подходит для добавления в словарь. Я подозреваю, что это происходит из-за того, что параметр T: SharedBaseClass
был добавлен как параметр in
(контравариантный) в интерфейсе IConverter
, однако оставить его как инвариантный точно так же не получится. Я подозреваю, что если бы это было ковариантно (невозможно с входным параметром), компилятор разрешил бы это. Я думаю, что где-то ошибся в своих абстракциях, так что же было бы подходящим решением в этом случае?
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());
}
}
Пример полного рефакторинга, который не работает: https://dotnetfiddle.net/vuDCiH
Некоторые мысли о «входящих» параметрах и дисперсии см. stackoverflow.com/questions/2876315/…
Корень проблемы в том, что вы можете получить любой конвертер из своего словаря и дать ему свой экземпляр SharedBaseClass
для конвертации. Поэтому преобразователи в вашем словаре должны быть объявлены как принимающие SharedBaseClass
.
Если ваш словарь принимает преобразователи, которые принимают экземпляр SharedBaseClass
, то все преобразователи, которые входят в него, также должны иметь возможность принимать экземпляр SharedBaseClass
для преобразования, поскольку технически вы можете извлечь любой из них из словаря и дать ему любой SharedBaseClass
экземпляр.
Таким образом, любой дальнейший путь зависит от того, сможем ли мы избавиться от этого словаря, содержащего IConverter<SharedBaseClass, SharedBaseClass>
экземпляры. Один из возможных подходов:
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;
}
}
Затем:
var executor = new ConverterExecutor();
executor.RegisterConverter(new DestinationConverter());
Мы заменили эти экземпляры IConverter<SharedBaseClass, SharedBaseClass>
делегатами, которые принимают SharedBaseClass
и возвращают SharedBaseClass
. Каждый делегат удерживает преобразователь и приводит экземпляр SharedBaseClass
к типу, который ожидает преобразователь.
Теперь, если вы передадите неправильный тип конкретному конвертеру, вы получите InvalidCastException
: проблема на самом деле не исчезла, но мы перенесли проверку со времени компиляции на время выполнения.
Используйте сопоставление с образцом и забудьте обо всем этом аду.