65.9K
CodeProject 正在变化。 阅读更多。
Home

结构类型 vs. 鸭子类型

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (1投票)

2008 年 7 月 5 日

CPOL

3分钟阅读

viewsIcon

18246

一篇讨论结构类型与鸭子类型的文章

引言

不久前,我解释了为什么我认为鸭子类型是危险的。最近,Scala 推广了一种不同的类型系统,称为结构类型,它通常被描述为“类型安全的鸭子类型”。让我们仔细看看结构类型,看看它是否实现了这个承诺。

使用鸭子类型,你会在完全黑暗中向对象发送消息。你不知道这个对象是否知道这个消息,你只是相信调用者传递给你的对象确实知道这个消息

def test(o)
  log o.getName   # Let's hope this will work
end

上面的代码可能存在问题,但你只能在运行时发现。

结构类型允许你对这个对象抱有的期望更加明确。在下面的 Scala 示例中,我声明传入参数的对象应该至少有一个名为 getName 的方法,该方法应返回一个 String

def test(f: { def getName(): String }) {
  log(f.getName)
}

这看起来没什么,但它确实为我们带来了很多类型安全性。例如,如果我尝试传递一个未定义 getName 方法的对象,Scala 编译器会报错

type mismatch;
 found   : Test
 required: AnyRef{def getName(): String}

如果我尝试传递一个具有 getName 方法但返回 int 而不是 String 的对象,我也会收到相同的错误消息。

从这个角度来看,结构类型确实优于鸭子类型,我对鸭子类型的主要反对意见之一(“它不是类型安全的”)消失了。然而,结构类型并不能解决所有问题。以下是可用主要技术的快速并排比较

鸭子类型 结构类型 类/接口/Trait (1)
类型安全
可以自动重构 (2) (3)
遵守“不要重复自己”原则 (4) (5) (6)
  1. 我将类、接口和特性混为一谈。它们具有不同的语义,但在类型上下文中意义相同。
  2. 有关无法重构鸭子类型代码的更多详细信息,请参见此博客条目
  3. 任何尝试重命名三个位置中的任何一个位置的 getName 方法(实例传入参数的类中、方法签名中或方法体中)都将导致所有其他位置也被正确重命名。
  4. 由于你使用鸭子类型完全不声明类型,因此没有重复。
  5. 如果你需要声明多个接受带有 getName 方法的参数的方法,你将必须为每个方法重复整个 (f: { def getName(): String } 语句。
  6. 由于你使用 Class 仅声明一个类型,因此没有重复。

正如你所看到的,结构类型的表现相当不错,唯一的问题是,一旦你需要多次将它用于签名,它就违反了 DRY 原则。如果只需避免在这种情况下使用结构类型,而是将该方法封装在 Class/Interface/Trait 中,就可以轻松避免这种情况。

错误

def test(f: { def getName(): String }) {
  log(f.getName)
}

def toXml(f: { def getName(): String }) {
  log("" + f.getName + "");
}

尽管存在重复,但上面代码的问题在于,重命名其中一个 getName 方法不会导致另一个方法也被自动重命名,因为编译器无法知道这些方法是相同的。在某种程度上,具有讽刺意味的是,所谓的结构类型并没有保留……我们实际传递的类型的结构。

更好

trait HasName {
  def getName : String = { ... }
}

def test(HasName f) = {
  log(f.getName)
}

def toXml(HasName f) = {
  log("<log>" + f.getName + "</log>");
}

那么,结构类型……好还是坏?

我感觉复杂。

结构类型对于偶尔的代码片段可能很有用,这些代码片段可能不值得为其创建 Class/Trait/Interface,但是一旦你想多次重用该签名,或者如果该签名包含多个方法,你最好创建一个精心命名的类型来捕获该签名并改用它。

您有什么看法?

© . All rights reserved.