Можно ли переопределить реализацию метода, наследуя интерфейс, который дает реализацию по умолчанию для этого метода? Если нет, то как мне реструктурировать свой код, чтобы избежать явных вызовов методов реализации интерфейса?
Вот обзор структуры классов в моем проекте:
interface IInputProcessor
{
public ProcessInputsResult ProcessInputs(Item[] inputs);
}
abstract class MachineComponent : IInputProcessor
{
public abstract ProcessInputsResult ProcessInputs(Item[] inputs);
}
interface IComponentContainer<T> where T : MachineComponent
{
public T[] Components { get; }
}
class MachineSystem : MachineComponent, IComponentContainer<MachineComponent>
{
public MachineComponent[] Components { get; }
}
Моя цель — предоставить реализацию ProcessInputs
внутри IComponentContainer
интерфейса, поскольку все контейнеры компонентов должны обрабатывать входные данные каждого из своих компонентов, и реализация для этого может быть общей для всех производных классов.
Чтобы добиться этого, я попытался создать реализацию метода по умолчанию для ProcessInputs
внутри IComponentContainer
, например:
interface IComponentContainer<T> : IInputProcessor where T : MachineComponent
{
public T[] Components { get; }
ProcessInputsResult IInputProcessor.ProcessInputs(Item[] inputs)
{
...
}
}
Обратите внимание, что я также сделал IComponentContainer
производным от IInputProcessor
, поскольку все контейнеры компонентов должны обрабатывать входные данные для всех своих компонентов.
Я надеялся, что это новое определение будет означать, что MachineSystem
, который является одновременно MachineComponent
и IComponentContainer<MachineComponent>
, не нужно будет указывать реализацию для MachineComponent.ProcessInputs
, поскольку она задана в интерфейсе IComponentContainer
.
Это было не так, и мне пришлось явно сослаться на реализацию метода, указанную в интерфейсе, вот так:
class MachineSystem : MachineComponent, IComponentContainer<MachineComponent>
{
...
public override ProcessInputsResult ProcessInputs(Item[] inputs) =>
((IComponentContainer<MachineComponent>)this).ProcessInputs(inputs);
}
Я думаю, что это противоречит цели предоставления реализации по умолчанию в интерфейсе, поскольку производные классы должны указывать, что они хотят ее использовать.
Есть ли хорошее решение этой проблемы или мне следует изменить структуру кода?
ОБНОВЛЕНИЕ: В ответ на ответ Ивана Петрова
interface IComponentContainer<T> : IInputProcessor where T : MachineComponent {
public T[] Components { get; }
}
class ComponentContainer<T> : MachineComponent, IComponentContainer<T>
where T : MachineComponent {
public T[] Components { get; }
public override ProcessInputsResult ProcessInputs(Item[] inputs) {
// implementation will serve for interface method too
}
}
class MachineSystem : ComponentContainer<MachineComponent> {
// we can still override ProcessInputsResult here too
}
Использование общего базового класса для реализации обработки контейнера компонентов в моей ситуации не сработало бы, поскольку классы, реализующие IComponentContainer
и все производные от абстрактного класса MachineComponent
, находятся на разных уровнях иерархии классов. Например, MachineSystem : IComponentContainer<MachineComponent>
наследует MachineComponent
напрямую, тогда как CompositeComponent : IComponentContainer<SimpleComponent>
наследует другой класс SimpleComponent
, который, в свою очередь, является производным от MachineComponent
. По этой причине использование одного базового класса, от которого все они происходят, наложит на них одну и ту же иерархию. Эту проблему можно было бы легко решить, если бы были возможны общие типы наследования, например:
class ComponentContainer<TContainer, TBase> : TBase, IComponentContainer<TContainer>
where TContainer : MachineComponent
where TBase : MachineComponent
{
public T[] Components { get; }
public override ProcessInputsResult ProcessInputs(Item[] inputs)
{
...
}
}
Но поскольку это не поддерживается, я не вижу способа заставить его работать с общим базовым классом.
Я могу только подтвердить, что я оказался в той же структуре классов с открытыми методами, явно делегирующими частные методы реализаций интерфейса по умолчанию с тем же именем и сигнатурой, и я задавался вопросом, зачем мне это двойное объявление и что было бы намного лучше, если бы реализация по умолчанию будет доступна неявно. Это делает эту функцию громоздкой.
@Ivan Petrov Да, я упростил исходное имя (MachineSystemComponent) для ясности. Я обновил вопрос и удалил старое имя.
@EliaGiaccardi, ты уверен, что это действительно работает (а не просто компилируется) public override ProcessInputsResult ProcessInputs(Item[] inputs) => ((IComponentContainer<MachineComponent>)this).ProcessInputs(inputs);
@Ivan Petrov К сожалению, у меня сейчас нет простого способа проверить это, есть какие-то конкретные проблемы?
@EliaGiaccardi да, на самом деле это не работает. Я думаю, это потому, что мы отправляем один и тот же метод, потому что он фактически обеспечивает наиболее конкретную реализацию, и в конечном итоге мы вызываем исключение stackoverflow.
Я думаю, что это противоречит цели предоставления значения по умолчанию реализация в интерфейсе, поскольку производные классы должны указывать которые они хотят использовать.
Производным (или реализующим интерфейс) классам не нужно ничего указывать, если у них нет абстрактного класса, который заставил бы их это сделать.
но в нашем случае у нас есть такой класс
abstract class MachineComponent : IInputProcessor
{
public abstract ProcessInputsResult ProcessInputs(Item[] inputs);
}
Теперь всем производным классам (если они сами не являются абстрактными) необходимо переопределить метод и тем самым предоставить реализацию, которая будет одновременно скрываться/иметь приоритет над реализацией интерфейса по умолчанию.
Ваше обходное решение иллюстрирует это
public override ProcessInputsResult ProcessInputs(Item[] inputs) =>
((IComponentContainer<MachineComponent>)this).ProcessInputs(inputs);
Этот вызов приведенного метода компилируется в следующий IL:
callvirt instance class ProcessInputsResult IInputProcessor::ProcessInputs(class Item[])
Мы совершаем виртуальный вызов IInputProcessor::ProcessInputs
из наиболее конкретной/производной реализации IInputProcessor::ProcessInputs
. Он отправит сообщение самому себе, вызывая бесконечную рекурсию.
Это приводит нас к осознанию того, что мы не можем повторно использовать реализацию интерфейса по умолчанию в методе, который должен переопределить ее, так же, как мы могли бы сделать это с вызовом base.BaseClassMethod
в виртуальном переопределяющем методе при наследовании классов.
Моя цель — предоставить реализацию ProcessInputs внутри Интерфейс IComponentContainer, поскольку все контейнеры компонентов должны обрабатывать входные данные каждого из своих компонентов и реализацию поскольку это может быть общим для всех производных классов.
Я думаю, добавив (абстрактный) класс посередине, который реализует IComponentContainer<T>
и обеспечивает реализацию, вы достигнете своей цели.
interface IComponentContainer<T> : IInputProcessor where T : MachineComponent {
public T[] Components { get; }
}
class ComponentContainer<T> : MachineComponent, IComponentContainer<T>
where T : MachineComponent {
public T[] Components { get; }
public override ProcessInputsResult ProcessInputs(Item[] inputs) {
// implementation will serve for interface method too
}
}
class MachineSystem : ComponentContainer<MachineComponent> {
// we can still override ProcessInputsResult here too
}
Спасибо за подробный ответ. В моей ситуации это невозможно применить, поскольку классы, реализующие IComponentContainer (все они являются производными от абстрактного класса MachineComponent), находятся на разных уровнях иерархии, поэтому базовый класс ComponentContainer не может быть повторно использован, поскольку он всегда является производным от MachineComponent. Я обновлю свой вопрос, включив в него это
@EliaGiaccardi Я думаю, что остальная часть моего ответа дала вам некоторую ясность в отношении того, почему то, что вы хотите делать с реализациями интерфейса по умолчанию, на самом деле невозможно с вашей текущей структурой классов. Я не буду редактировать свой ответ, не стесняйтесь принять его в какой-то момент, если не появится ничего лучшего.
Абсолютно, спасибо за вашу помощь. Я думаю, что создам где-нибудь статический метод, чтобы обеспечить базовую реализацию обработки ввода для дочерних компонентов, и вручную вызывать его в контейнерах компонентов, таких как MachineSystem. Это также прояснит, что есть место для другой реализации, поскольку классы все равно должны явно переопределять абстрактный метод.
являются ли MachineSystemComponent и MachineComponent одним и тем же, если нет, то какова связь?