结构类型 vs. 鸭子类型





4.00/5 (1投票)
一篇讨论结构类型与鸭子类型的文章
引言
不久前,我解释了为什么我认为鸭子类型是危险的。最近,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) |
- 我将类、接口和特性混为一谈。它们具有不同的语义,但在类型上下文中意义相同。
- 有关无法重构鸭子类型代码的更多详细信息,请参见此博客条目。
- 任何尝试重命名三个位置中的任何一个位置的
getName
方法(实例传入参数的类中、方法签名中或方法体中)都将导致所有其他位置也被正确重命名。 - 由于你使用鸭子类型完全不声明类型,因此没有重复。
- 如果你需要声明多个接受带有
getName
方法的参数的方法,你将必须为每个方法重复整个(f: { def getName(): String }
语句。 - 由于你使用 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,但是一旦你想多次重用该签名,或者如果该签名包含多个方法,你最好创建一个精心命名的类型来捕获该签名并改用它。
您有什么看法?