C#: 避免使用 if (x is Foo) {...} else if (x is Bar) {...} 处理数据结构问题

6

我有一组数据结构,例如:

abstract class Base {...}
class Foo : Base {...}
class Bar : Base {...}

同时还需要一种方法,根据所属的子类将其转换为一个基础类型:

void Convert(Base b) {
  if (b is Foo) 
    // Do the Foo conversion
  else if (b is Bar) 
    // Do the Bar conversion
...

显然,这是非常糟糕的面向对象编程 - Convert方法必须知道Base的每个派生类,并且每次扩展Base时都必须更改该方法。解决此问题的“正常”面向对象方式是使每个Base的派生类负责自己的转换,例如:

abstract class Base {
  abstract Converted Convert();
...}
class Foo : Base {
  override Converted Convert(){...}
...}
class Bar : Base {
  override Converted Convert(){...}
...}

然而,在我编写的代码中,Base是一个纯数据结构(只有getter和setter - 没有逻辑),而且它位于另一个我无权更改的程序集中。是否有更好的代码结构方法,不会强制要求Base的派生类具有逻辑呢?
谢谢。

3
访问者模式可能有所帮助。 - SLaks
3
你可以引入一个中间抽象类,继承自基类 Base,Foo 和 Bar 再从这个中间类继承。然后在这个中间类中添加一个抽象的 Convert 方法。 - Matthew Watson
我不确定在无法修改“Base”、“Foo”和“Bar”类的情况下是否有好的解决方法。 - Tim S.
5
我不会通过批评前提来回答你的问题。你的问题是:“我有一个操作C,它可以处理任何B,但B的作者并不保证B的每个子类型都能支持操作C”。因此,操作C并不接受“任何B”,它只接受“某些”Bs,因此C的作者正在写一个他没有打算遵守的合同。不要这样做。 - Eric Lippert
FooBar 实现一个接口,定义所有其他方法可能需要对它们执行的操作;让你的方法接受一个类型为该接口的参数。 - Servy
显示剩余5条评论
2个回答

19

如果你做某件事会感到疼痛,那么就不要做。根本问题是:

我有一个接受基类(Base)的方法...

既然这是根本问题,那么就去除它。完全摆脱那个方法,因为它很糟糕。

用以下内容替换:

void Convert(Foo foo) {
   // Do the Foo conversion
}
void Convert(Bar bar) {
   // Do the Bar conversion
}

现在没有必要欺骗并说它可以转换任何Base,而实际上它不能。


只有在编译器在编译时知道具体类型时,这才有效。 - John Gibb
@JohnGibb:正确;这正是在原帖中提出的情况下,代码被认为是类型正确的情况。静态分析语言的整个目的就是告诉你程序何时不确定是否没有类型错误。如果程序员不知道对象的类型,则程序员不知道Convert是否是合法操作! - Eric Lippert
这绝对是正确的,但如果这个人不能编辑原始类型,那么他们除了枚举可能的子类型之外别无选择。我只是不明白你的答案如何帮助他们;他们甚至需要使用强制转换才能调用你的重载! - John Gibb
2
@JohnGibb:问题的存在并不意味着一定有令人愉悦的解决方案。我注意到,Base、Foo和Bar类的开发人员没有充分预见客户的需求。如果他们这样做了,要么(1)他们会自己提供必要的代码,要么(2)他们会更好地设计可扩展的类层次结构,或者(3)他们会设计出这样的层次结构,使得Foo和Bar是Base唯一可能的派生类型。 - Eric Lippert
只有在Convert是“公共”的情况下,这个解决方案才有意义,因为在这种情况下,需要考虑方法合同。如果它是“私有”的,那么您已经知道将如何调用该方法,因此合同就不那么重要了,但您只是将类型检测和转换从被调用者移动到调用者,使得这比OP的设计更加不优雅。 - MgSam

2
我遇到了类似的情况,并提出了一个类似于函数式语言中的模式匹配的解决方案。以下是语法:
请注意,在lambda内部,foo和bar被强类型为它们各自的类型;不需要进行转换。
var convertedValue = new TypeSwitch<Base, string>(r)
            .ForType<Foo>(foo => /* do foo conversion */)
            .ForType<Bar>(bar => /* do bar conversion */)
        ).GetValue();

以下是TypeSwitch类的实现:

public class TypeSwitch<T, TResult>
{
    bool matched;
    T value;
    TResult result;

    public TypeSwitch(T value)
    {
        this.value = value;
    }

    public TypeSwitch<T, TResult> ForType<TSpecific>(Func<TSpecific, TResult> caseFunc) where TSpecific : T
    {
        if (value is TSpecific)
        {
            matched = true;
            result = caseFunc((TSpecific)value);
        }
        return this;
    }

    public TResult GetValue()
    {
        if (!matched)
        {
            throw new InvalidCastException("No case matched");
        }
        return result;
    }
}

我相信这段代码可以进行清理,在大多数情况下,Eric Lippert是正确的,即该前提基本上是有缺陷的。然而,如果你遇到的情况是你唯一的选择是一堆is和强制转换,那么我认为这样会更加简洁!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接