Апкаст c generic-классами в C#

Я пытаюсь создать небольшую архитектуру, но застрял с generic'ами и их наследованием: у меня есть следующее:

  1. Интерфейс секции и примеры реализации (скорее всего, будут расширения)
public interface ISection {}
public class FooSection : ISection {}
public class BarSection : ISection {}
  1. Абстрактный класс для билдеров секций, которые кое-что с этими секциями делают
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;
      }

}
  1. Далее я пытаюсь в общем методе создать несколько экземпляров и вызвать их через список:
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 шт):

Автор решения: Boris L.

Если вы пытаетесь (условно) в 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, а не возвращают их.

Я вижу три возможных пути решения:

  1. Отказаться от использования базового ISectionBuilder<ISection>. Билдеры всегда конкретные. Каталоги билдеров будут содержать object, скорее всего без rtti-магии вроде typeof(ISectionBuilder<>).MakeGenericType() не обойтись. Вряд ли хороший выбор.
  2. Наоборот, отказаться от конкретных ISectionBuilder<FooSection>. Интерфейс билдера всегда абстрактен (принимает только ISection!), а уже реализация конкретная и содержит тайп-каст ISection в конкретный тип.
  3. Сделать отдельный абстрактный ISectionBuilder (не-generic, именно отдельный интерфейс, копия, вы не сможете наследовать его от ISectionBuilder<ISection>). И этот абстрактный ISectionBuilder реализовать во всех ваших билдерах.
→ Ссылка
Автор решения: VladD

Смотрите, в чём дело.

Ваш 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> нужны лишь для вашей инициализации, поэтому, вероятно, нет смысла делать их публичными.

Надеюсь, сумел угадать, что вам нужно.

→ Ссылка