Апкаст c generic-классами в C#
Я пытаюсь создать небольшую архитектуру, но застрял с generic'ами и их наследованием: у меня есть следующее:
- Интерфейс секции и примеры реализации (скорее всего, будут расширения)
public interface ISection {}
public class FooSection : ISection {}
public class BarSection : ISection {}
- Абстрактный класс для билдеров секций, которые кое-что с этими секциями делают
public abstract class ISectionBuilder<T> : IComparable<ISectionBuilder<T>>
where T: ISection
{
public int Priority { get; protected set; }
public virtual bool CanBuildSectionLikeThis => false;
public ISectionBuilder(int priority) => Priority = priority;
public Foo BuildSection(T section)
{
if(!CanBuildSectionLikeThis)
throw new InvalidOperationException();
return BuildSectionUnchecked(section);
}
protected abstract Foo BuildSectionUnchecked(T section);
public int CompareTo(ISectionBuilder<T>? other) => Priority - other.Priority;
}
public class FooBuilder : ISectionBuilder<FooSection>
{
public override bool CanBuildSectionLikeThis => true;
public FooBuilder(int priority = 0) : base(priority) { }
protected Foo BuildSectionUnchecked(FooSection section)
{
var result = new Foo();
//что-то делаю внутри
return result;
}
}
//название Mega, а не Bar, чтобы показать. что они не напрямую зависят друг от друга
public class MegaBuilder : ISectionBuilder<BarSection>
{
public override bool CanBuildSectionLikeThis => true;
public FooBuilder(int priority = 3) : base(priority) { }
protected Foo BuildSectionUnchecked(BarSection section)
{
var result = new Foo();
//что-то делаю внутри
return result;
}
}
- Далее я пытаюсь в общем методе создать несколько экземпляров и вызвать их через список:
public void Execute(List<ISection> sections)
{
var list = new List<(ISectionBuilder<ISection>, ISection)>();
foreach(var s in sections)
{
//определяю какой тип билдера создать, в данном случае, пусть, например, порядок будет такой, что
// list[0] = (new FooBuilder(), sections[0]);
// list[1] = (new MegaBuilder(), sections[1]);
}
}
И собственно, именно на этих попытках добавить экземпляры в список у меня всё и накрывается. Я уже выводил отдельно интерфейс с контравариантом (то есть ISectionBuilder становился интерфейсом, а чтобы сохранить логику стандартных реализаций методов и реализованных свойств, я наследовал AbstractSectionBuilder : ISectionBuilder и далее от него уже все остальные реализации), однако и там проблема каста остается, а при попытке явного каста выдает исключение. Можете помочь с тем, чтобы оставить абстракцию и дженерик, при этом иметь возможность и доступ к стандартным реализациям методов и функций?
Ответы (2 шт):
Если вы пытаетесь (условно) в List<ISectionBuilder<ISection>> добавить ISectionBuilder<FooSection>, то в вашей архитектуре это никак не получится.
Посудите сами. Допустим у меня есть такой список. Это значит что я могу написать код
foreach (ISectionBuilder<ISection> builder in list)
{
builder.BuildSection(new TestSection());
}
private class TestSection: ISection { ... }
При этом метод ISectionBuilder<ISection>.BuildSection получит в качестве параметра типа ISection некую третью реализацию (TestSection).
Но у вас же там по вашему замыслу лежит FooBuilder, его конкретная реализация метода BuildSection ожидает FooSection, не меньше. Его не устроят ни ISection, ни TestSection.
Поэтому даже если ISectionBuilder<in T> описать с контр-вариантностью, вы не сможете апкастить ISectionBuilder<FooSection> до ISectionBuilder<ISection>.
В обратном направлении сможете (но вам это не нужно). А с вариантностью (out T) ваш интерфейс не совместим - его методы принимают параметры типа T, а не возвращают их.
Я вижу три возможных пути решения:
- Отказаться от использования базового
ISectionBuilder<ISection>. Билдеры всегда конкретные. Каталоги билдеров будут содержать object, скорее всего без rtti-магии вродеtypeof(ISectionBuilder<>).MakeGenericType()не обойтись. Вряд ли хороший выбор. - Наоборот, отказаться от конкретных
ISectionBuilder<FooSection>. Интерфейс билдера всегда абстрактен (принимает толькоISection!), а уже реализация конкретная и содержит тайп-кастISectionв конкретный тип. - Сделать отдельный абстрактный
ISectionBuilder(не-generic, именно отдельный интерфейс, копия, вы не сможете наследовать его отISectionBuilder<ISection>). И этот абстрактныйISectionBuilderреализовать во всех ваших билдерах.
Смотрите, в чём дело.
Ваш ISectionBuilder<T> по сути контравариантен по T, т. к. не может принимать «более общий» тип вместо T. Это важно.
Ваш list, который вы пытаетесь построить, содержит в том виде, как он вами объявлен, произвольный билдер и произвольную секцию. Но ваши билдер и секция должны подходить друг к другу! То есть конкретный T и в ISectionBuilder<T>, и в ISection<T> в каждом отдельном элементе списка должны быть одинаковы.
Как это закодировать? Например, мы можем пару из ISectionBuilder<T> и ISection<T> объявить отдельным типом, и определить не зависимый от T интерфейс:
interface IWorkUnit
{
void Process();
}
record WorkUnit<T>(ISectionBuilder<T> Builder, T Section) : IWorkUnit
where T : ISection
{
public void Process() => Builder.BuildSection(Section);
}
Теперь ваш цикл можно записать как-то так:
var list = sections.Select(ComposeWorkUnit);
foreach (var workUnit in list)
{
workUnit.Process();
}
Нам нужна лишь вспомогательная функция, которая находит подходящий билдер для секции, и упаковывает их в WorkUnit. Например, такая:
IWorkUnit ComposeWorkUnit(ISection section) => section switch
{
FooSection fooSection =>
new WorkUnit<FooSection>(new FooBuilder(), fooSection),
BarSection barSection =>
new WorkUnit<BarSection>(new MegaBuilder(), barSection),
_ => throw new InvalidOperationException(
$"Неизвестный тип секции: {section?.GetType().Name}")
};
Типы IWorkUnit и WorkUnit<T> нужны лишь для вашей инициализации, поэтому, вероятно, нет смысла делать их публичными.
Надеюсь, сумел угадать, что вам нужно.