根据子类分配协变列表元素

时间:2019-02-01 13:39:24

标签: c# dynamic dispatch contravariance

我有类BC,它们是从类SuperA继承的。如果我有一个SuperA的列表,其中包含SuperA的各种实现,那么如何根据列表中每个元素的实际实现来调用采用BC参数的方法,而不必测试每个元素的类型(出于开放/封闭的原则,我宁愿避免使用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));
}

但是我愿意接受任何更好的主意。正如在回答中指出的那样,演化后运行时错误的风险很大。

2 个答案:

答案 0 :(得分:1)

编译器抱怨您的代码,因为如您所指出的,GenerateOutput(SuperA)类中没有OutputFactory,并且方法调用解析发生在编译类型而不是运行时,因此基于引用的类型(item是类型为SuperA的引用),而不是运行时实例的类型。

您可以尝试不同的方法:

  1. 如果您认为有道理,则可以尝试将多态行为(要生成的输出文本)移至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);
    }
}

非常简单,但是在SuperAB, and C classes are out of your control or when the different desired behaviours you should provide for A and B classes does not belong to B and C时效果不佳课程。

  1. 您可以使用一种我喜欢的方法来称为责任集:这有点类似于GoF的模式责任链,但没有链;-);您可以按以下方式重写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的实现的 registration ,但这很容易已移至您的IoC container配置或外部配置文件。 我喜欢这种方法,因为它可以将基于类型检查的算法转换为多态算法。

我在我的技术博客https://javapeanuts.blogspot.com/2018/10/set-of-responsibility.html上最近的一篇文章中谈到了这个主题。

  1. 您可以通过GoF的 visitor 模式来解决问题,这比我建议的方法要复杂一些,但正是针对这种情况而设计的
  2. 您可以按照其他回复中的建议,采用基于dynamic的方法

希望这对您有帮助!

答案 1 :(得分:0)

您可以这样称呼它:

DoSomething((dynamic)factory.GenerateOutput((dynamic)item));

这样,使用dynamic,您的对象将在运行时绑定到正确的方法。

使用此实现,您将不得不考虑自己面临发送C对象的风险,该对象未实现任何方法,并且您的代码仍会编译,但是会生成运行时错误