检测一个对象是否实现了接口有什么问题?

30

这个回答的评论中,有人说检查对象是否实现了接口,尽管很流行,但是是一件坏事。

以下是我认为是此做法的一个示例:

public interface IFoo
{
   void Bar();
}

public void DoSomething(IEnumerable<object> things)
{
   foreach(var o in things)
   {
       if(o is IFoo)
           ((IFoo)o).Bar();
   }
}

作为一个之前使用过这种模式的人,我很好奇为什么这是不好的,并且搜索了一个好的例子或解释,但无法找到。

虽然我很可能误解了评论,但是否有人能提供一个例子或链接来更好地解释这个评论呢?


7
首先,询问自己是否可以使用 IEnumerable<IFoo> 替代。很可能没有任何理由不能这样做。 - user395760
@delnan - 请问,如果您不知道对象是否实现了IFoo,如何使用IEnumerable<IFoo>?整个重点在于您拥有一组不同类型的对象,除了共同的基类object之外没有其他共同点。 - Erik Funkenbusch
2
糟糕的设计可能是缺乏共同基础/接口的原因,不过这取决于情况,有时对象确实是最佳选择。 - jk.
8个回答

36

这要取决于你想做什么。有时候它是合适的——例如:

  • LINQ to Objects中,当使用专门成员在 IList<T> 上执行比如 Count 更高效的操作时。
  • LINQ to XML中,用于提供一个非常友好的API,可以接受各种类型,并在适当的情况下迭代值。
  • 如果你想在 Windows表单中找到特定控件下的所有控件,那么你需要检查每个控件是否为容器,以确定是否进行递归。

在其他一些情况下,这样做则不太合适,你应该考虑是否可以改变参数类型。这肯定是一种“坏味道”——通常你不应该关心被交给你的任何东西的实现细节;你应该只使用声明参数类型提供的API。这也被称为违反了里氏替换原则

无论那些教条主义者怎么说,有时你确实需要检查对象的运行时类型。例如,很难正确地重写object.Equals(object) 而不使用is/as/GetType。这并不总是一件坏事,但应该让你考虑是否有更好的方法。只在真正需要时使用,用得较少,同时要确保它是最合适的设计。


尽管如此,个人而言,我更愿意将您展示的代码写成这样:

public void DoSomething(IEnumerable<object> things)
{
    foreach(var foo in things.OfType<IFoo>())
    {
        foo.Bar();
    }
}

它实现了相同的功能,但以更整洁的方式 :)


-1(不回答问题,与Erno不同,他回答了所问的问题)。 - Sverre Rabbelier
@SverreRabbelier:我认为并不总是不合适的解释可能对提问者有所帮助,这也是他们接受答案的原因。 - Jon Skeet
啊,我没注意到(我只看到Erno的票数较少)。显然,在问题被编辑之前,我的投票是“锁定”的。 - Sverre Rabbelier
@SverreRabbelier:同时,我添加了更多关于为什么它有一点气味的信息 - 看看你是否认为这使得答案更好 :) - Jon Skeet

18

我认为方法应该是这样的,这样看起来更安全:

public void DoSomething(IEnumerable<IFoo> things)
{
   foreach(var o in things)
   {
           o.Bar();
   }
}

阅读关于Liskov原则的相关违规问题:什么是Liskov替换原则?


另一个优点是,这也将类型检查从运行时移动到编译时,因此您可能更早地捕获错误。 - Mechanical snail
1
只有当您知道对象是IEnumerable<IFoo>时,它才有效,它也可能是IEnumerable<IBar>或一组不同的对象。 - Erik Funkenbusch
@MystereMan - 是的,在这些情况下,您可以选择Jon建议的解决方案或创建单独的DoSomethings。 - Emond
问题在于你可能会假设太多,从而使代码变得脆弱(你如何为这个方法编写好的测试 - 你需要传递任何适合对象的类型... - Emond

10

如果你想知道为什么评论者发出了那个评论,最好向他们询问以获取解释。

我不认为你发布的代码是“糟糕的”。更加“真正”糟糕的做法是将接口用作标记。也就是说,你不打算实际使用接口的方法;而是在类上声明接口作为某种描述方式。在类上使用属性而不是接口作为标记的方式。请使用属性而不是接口作为类上的标记。

标记接口在许多方面都存在危险。我曾经遇到过的一个现实情况,一款重要产品基于标记接口做出了错误决策,详细信息可以参考这里:http://blogs.msdn.com/b/ericlippert/archive/2004/04/05/108086.aspx

话虽如此,C#编译器自己在某些情况下使用“标记接口”。Mads在这里讲述了这个故事:http://blogs.msdn.com/b/madst/archive/2006/10/10/what-is-a-collection_3f00_.aspx


使用空接口来实现混入扩展也被认为是不好的吗?实际上,使用属性并没有这样的方法。 - asawyer
FxCop表示,如果您需要编译时验证,则这些标记接口是可以的。如果您控制接口并且不打算更新它,那么有什么优势呢? - Michael B

6
一个原因是,如果不深入代码进行查看,就无法立即发现对该接口存在依赖关系。

4

该声明

检查对象是否已实现接口,尽管可能存在滥用,但是这是一件不好的事情

在我看来过于教条。正如其他人所回答的,您完全可以将IFoo的集合传递给您的方法,并实现相同的结果。

然而,接口可以用于向类添加可选功能。例如,.net框架提供了IDataErrorInfo接口*。当实现此接口时,它会向使用者指示除了类的标准功能之外,它还可以提供错误信息

在这种情况下,错误信息是可选的。WPF视图模型可能会提供错误信息,也可能不提供。如果不查询接口,则在没有具有巨大表面积的基类的情况下,无法使用此可选功能。

*暂且忽略IDataErrorInfo接口的可怕设计。


1
如果你的方法需要注入一个接口实例,那么你应该无论实现方式如何都要同样对待它。
在你的例子中,通常不会有一个对象的通用列表,而是一个 ISomething 列表,并且调用 ISomething.Bar() 将由具体类型实现,因此调用其实现。如果该实现什么也不做,那么你就不必进行检查。

1

我不喜欢这种“基于类型开关”的编码风格,有几个原因。(以下是与我的行业游戏开发相关的例子,提前道歉。:)

首先,我认为拥有异构的物品集合是很随意的。例如,我可以拥有一个“无所不在的一切”集合,但是当迭代该集合以应用子弹效果、火焰伤害或敌人 AI 时,我必须遍历这个列表,其中大部分都是我不关心的东西。在我的看法中,拥有单独的子弹、熊熊燃烧的火焰和敌人集合要更加“清晰”。请注意,我可以在多个集合中拥有单个项目;一个燃烧的机器人导弹可以在这三个列表中引用,根据它需要运行的三种类型的逻辑的适当部分来完成其“更新”。除了拥有“一个单一的包含所有内容的集合”之外,我认为包含无处不在的所有内容的集合并不是非常有用;除非您查询它的功能,否则您无法对列表中的任何内容执行任何操作。

我讨厌做不必要的工作。这与上面的内容密切相关,但是当你创建一个给定的东西时,你知道它的能力是什么(或者可以在那个时候查询它们),所以你可能会趁此机会将它们放入更具体的正确集合中。你只有16毫秒来处理世界上的一切,你想浪费时间处理、查询和从通用事物中选择,还是想专注于你关心的特定事物并开始操作呢?

根据我的经验,将代码库从对异构数据集的通用操作转换为具有同质数据集的操作不仅可以提高性能,而且还可以通过更简单的代码执行更明显的工作,从而增加理解度,并且通常减少完成任何给定任务所需的代码量。

所以,说查询接口是不好的是教条主义的,但如果你能想出如何避免需要查询任何东西,似乎会使事情变得更简单。至于我的“性能”陈述和反驳“如果你不测量它,你就不能对它做出任何评论”的论点,显然不做某件事比做某件事要快。无论这对于个人项目、程序员或函数是否重要,都取决于拥有编辑器的人,但如果我可以简化代码,并在此过程中使其为了相同的结果而做更少的工作,我将这样做而不必测量。


0

我并不认为这本身是一个“坏事”。代码只是对“在z中的所有y”进行了文字转录,如果你需要这样做,那么这是完全可以接受的。当然,你也可以使用things.OfType<Foo>()来简洁地表达。

推荐避免这种方式的主要原因是,根据面向对象编程(OOP)的理论,接口旨在模拟可以替换对象的不同种类的“黑盒子”。将算法基于接口的实现构成了将应该在接口中的行为移动到算法中。

本质上,接口是一种行为角色。如果你认为OOP是一个好主意,那么你应该仅使用接口来模拟行为,以便算法不必如此。我认为现在所谓的OOP实际上并不是一个好主意,所以我的回答只能有限。


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