У меня есть классы B и C, унаследованные от класса SuperA. Если у меня есть список SuperA, содержащий различные реализации SuperA, как я могу вызвать метод, принимающий аргументы B и C в соответствии с фактической реализацией каждого элемента в списке, без необходимости проверять тип каждого элемента (я бы предпочел избегать Материал if (item is B) по причинам открытого/закрытого принципа).
public class Test
{
public void TestMethod()
{
var list = new List<SuperA> {new B(), new C()};
var factory = new OutputFactory();
foreach (SuperA item in list)
{
DoSomething(factory.GenerateOutput(item)); // doesn't compile as there is no GenerateOutput(SuperA foo) signature in OutputFactory.
}
}
private static void DoSomething(OutputB b)
{
Console.WriteLine(b.ToString());
}
private static void DoSomething(OutputC c)
{
Console.WriteLine(c.ToString());
}
public class SuperA
{
}
public class B : SuperA
{
}
public class C : SuperA
{
}
public class OutputB
{
public override string ToString()
{
return "B";
}
}
public class OutputC
{
public override string ToString()
{
return "C";
}
}
public class OutputFactory
{
public OutputB GenerateOutput(B foo)
{
return new OutputB();
}
public OutputC GenerateOutput(C foo)
{
return new OutputC();
}
}
}
В приведенном выше коде я хочу напечатать:
B
C
РЕДАКТИРОВАТЬ :
Рабочим решением, которое я нашел, может быть изменение типа элемента на dynamic.
foreach (dynamic item in list)
{
DoSomething(factory.GenerateOutput(item));
}
Однако я открыт для любой лучшей идеи. Как указано в ответе, риск ошибки времени выполнения после эволюции велик.
@DavidG: изменение типа item в foreach на dynamic решило проблему.
Как я постоянно говорю людям (и даже сегодня!): каждый раз, когда вы используете dynamic, умирает котенок. :)
@DavidG Разве это не законный вариант использования dynamic ?
Я не говорю, что это незаконно, но когда я вижу динамику, это почти всегда признак того, что ваш код немного запутан, это запах кода.
@Baguette Тот факт, что вы поставили себя в положение, когда это то, что вам нужно сделать, является проблемой.
@DavidG: Вернувшись к этой теме через некоторое время, я выберу решение шаблона посетителя, которое, я думаю, кто-то предложил.





Вы можете назвать это так:
DoSomething((dynamic)factory.GenerateOutput((dynamic)item));
Таким образом, используя dynamic, ваши объекты будут привязаны во время выполнения к правильным методам.
С этой реализацией вам придется принять во внимание, что вы подвергаете себя риску отправки объекта C, для которого не был реализован метод, и ваш код все равно будет компилироваться, но будет сгенерирована ошибка времени выполнения.
Я понял это и отредактировал свой первоначальный вопрос прямо перед тем, как прочитать ваш ответ =) Риск действительно велик, я тщательно обдумаю это. Спасибо ! Я могу просто использовать dynamic в foreach. Вместо двойного кастинга, как вы мне показали.
Компилятор жалуется на ваш код, потому что, как вы указали, в классе GenerateOutput(SuperA) нет OutputFactory, а разрешение вызова метода происходит в типе компиляции, а не во время выполнения, и, следовательно, основано на типе ссылки (item - это ссылка с типом SuperA), а не от типа экземпляра среды выполнения.
Вы можете попробовать разные подходы:
SuperA, добавив абстрактный метод или свойство в SuperA и по-разному реализуя его в подклассах SuperA.class SuperA {
public abstract string Content { get; }
}
class B : SuperA {
public string Content => "B";
}
class C : SuperA {
public string Content => "C";
}
class Test {
public void TestMethod() {
// ...
foreach (SuperA item in list) {
Console.WriteLine(item.Content);
}
}
Очень просто, но не очень хорошо работает с классами SuperA, B, andCclasses are out of your control or when the different desired behaviours you should provide forAandBclasses does not belong toBandC`.
TestMethod следующим образом:public void TestMethod() {
var list = new List<SuperA> {new B(), new C()};
var compositeHandler = new CompositeHandler(new Handler[] {
new BHandler(),
new CHandler()
});
foreach (SuperA item in list) {
compositeHandler.Handle(item);
}
}
Итак, вам нужно определить интерфейс Handler и его реализации следующим образом:
interface Handler {
bool CanHandle(SuperA item);
void Handle(SuperA item);
}
class BHandler : Handler {
bool CanHandle(SuperA item) => item is B;
void Handle(SuperA item) {
var b = (B)item; // cast here is safe due to previous check in `CanHandle()`
DoSomethingUsingB(b);
}
}
class CHandler : Handler {
bool CanHandle(SuperA item) => item is C;
void Handle(SuperA item) {
var c = (C)item; // cast here is safe due to previous check in `CanHandle()`
DoSomethingUsingC(c);
}
}
class CompositeHandler {
private readonly IEnumerable<handler> handlers;
public CompositeHandler(IEnumerable<handler> handlers) {
this.handlers = handlers;
}
public void Handle(SuperA item) {
handlers.FirstOrDefault(h => h.CanHandle(item))?.Handle(item);
}
}
Этот подход использует проверки типов (item is B), но скрывает их за интерфейсом (в частности, каждая реализация интерфейса должна обеспечивать проверку типов, чтобы выбрать экземпляры, которые он может обрабатывать): если вам нужно добавить третий подкласс D extends SuperA вашей иерархии корневой класс, вам нужно только добавить третью реализацию DHandler : Handler интерфейса Handler, не изменяя ни уже предоставленные реализации, ни класс CompositeHelper; единственное изменение, которое вы должны применить к существующему коду, — это Регистрация новой реализации handler в списке, который вы предоставляете конструктору CompositeHelper, но его можно легко переместить в вашу конфигурацию IoC container или во внешний файл конфигурации.
Мне нравится этот подход, потому что он позволяет превратить алгоритм проверка типа на основе в полиморфный.
Я писал об этой теме в недавней записи в моем техническом блоге: https://javapeanuts.blogspot.com/2018/10/set-of-responsibility.html.
dynamic, как это предлагается в другом ответе.Я надеюсь это тебе поможет!
Спасибо за этот исчерпывающий ответ. Я понимаю, что существует также набор решений до .NET4, отвечающих за эту проблему.
Я бы не сказал, что приведение безопасно в методе Handle, так как это будет открытый метод, и будущий программист может неправильно вызвать его без вызова CanHandle.
Конечно, можно добавить защиту в реализацию Handle: дело, ИМХО, в том, что проверка типов скрыта в конкретных реализациях интерфейса и не загрязняет основной алгоритм.
Поразмыслив, оставив тему на некоторое время и вернувшись к ней, я выберу реализацию паттерна «Посетитель». Как вы сказали, он как раз для этого и сделан, самодокументируется для других разработчиков и не полагается на непосредственное тестирование типов. Спасибо за исчерпывающий ответ.
@DavidG Признаюсь, я искал элегантный узор, а не отражение, но если я ничего не найду, мне действительно придется вернуться к этому.