1. 概述
Scala 本质上是一个名义类型(nominally typed)语言,也就是说,两个对象的类型只有在名称相同时才被认为是相同的。
但如果我们遇到一些类型名称不同,却具有某些共同特征的情况呢?又或者我们无法通过修改类型体系来建立类型之间的关系时该怎么办?
在这篇文章中,我们将深入探讨 Scala 的结构类型(structural types),学习如何在无法使用 继承 或 类型类 的情况下,编写运行时检查的 多态代码。
2. 如果它像鸭子一样叫
在 Scala 中,我们依赖类型系统来确保程序的正确性,通常通过在类型中编码不变量以及类型之间的关系来实现。编译器会强制执行这些不变量,从而确保我们的程序不会出现某些类型的错误。
✅ 这种保障是有代价的:有时会让代码写起来更麻烦。
举个例子,如果我们尝试用 Scala 编写类似以下 Python 3 的代码(使用了“鸭子类型”):
class Duck:
def fly(self):
print("Ducks fly together")
class Eagle:
def fly(self):
print("Eagles fly better than MJ")
class Walrus:
def swim(self):
print("I am faster on the water than on the land")
def flyLikeAnEagle(animal):
animal.fly()
animals = [Duck(), Eagle(), Walrus()]
for animal in animals:
flyLikeAnEagle(animal)
这段代码在 Python 中可以运行,因为 Python 是在运行时进行类型检查的 —— 只要对象有 fly()
方法,就能调用。
❌ 但在 Scala 中,编译器会报错,因为它不允许我们对 Walrus
这种没有 fly()
方法的对象调用该方法。尽管对 Duck
和 Eagle
来说调用是合理的,但编译器一视同仁,这就让人有点“踩坑”了。
这种特性在编程圈被称为 “鸭子类型(duck typing)”,源自一句名言:“如果它走起来像鸭子,叫起来也像鸭子,那它就是鸭子。”
3. 整理我们的鸭子队伍
虽然我们可以通过 Scala 的反射机制来模拟鸭子类型,但那样写出来的代码会很难维护。
✅ 幸运的是,Scala 提供了结构类型(structural types)这一机制,让我们能以更地道的方式实现类似的功能。
来看一个结构类型的例子:
type Flyer = { def fly(): Unit }
def callFly(thing: Flyer): Unit = thing.fly()
def callFly2(thing: { def fly(): Unit }): Unit = thing.fly()
def callFly3[T <: { def fly(): Unit }](thing: T): Unit = thing.fly()
从上面的例子可以看到,结构类型可以出现在大多数需要类型约束的地方,如:
- 类型别名(type alias)
- 方法参数类型
- 类型上界/下界
⚠️ 虽然底层实现是通过反射来调用方法的,但结构类型仍然保留了编译时的类型安全:编译器会确保对象拥有结构中声明的方法,从而避免运行时异常。
来看一下测试示例:
"Ducks" should "fly together" in {
callFly(new Duck())
callFly2(new Duck())
callFly3(new Duck())
}
"Eagles" should "soar above all" in {
callFly(new Eagle())
callFly2(new Eagle())
callFly3(new Eagle())
}
"Walrus" should "not fly" in {
// 以下代码不会通过编译
// callFly(new Walrus())
// callFly2(new Walrus())
// callFly3(new Walrus())
}
4. 一个更实用的例子
结构类型的一个经典应用场景是:确保资源总是被正确关闭,防止资源泄漏。
我们当然可以用传统的 try-catch-finally
来处理,但这依赖于开发者遵守规范。
✅ 使用结构类型,我们可以写出更灵活的控制结构:
type Closable = { def close(): Unit }
def using(resource: Closable)(fn: () => Unit) {
try {
fn()
} finally { resource.close() }
}
using(file) {
() =>
// 使用 file 的代码
}
虽然 close()
方法的调用是通过反射完成的,但传入的函数体本身是静态链接的。
⚠️ 单次反射调用的性能开销通常可以忽略不计。但在一些对性能要求极高的场景中,我们也可以通过重载 + 特化的方式,为已知类型提供静态版本的实现。
例如,为 Source
类提供一个专门的版本:
def using(file: Source)(fn: () => Unit) {
try {
fn()
} finally {
file.close()
}
}
5. 总结
在这篇文章中,我们学习了如何在无法使用传统多态机制(如继承或类型类)的情况下,使用 结构类型 实现灵活的类型抽象。
✅ 结构类型虽然底层依赖反射,但仍然保证了编译期的类型安全,避免了运行时异常。
⚠️ 唯一的缺点是:由于使用了运行时反射,可能会带来轻微的性能损耗。
如需查看本文完整代码,请前往 GitHub 仓库: https://github.com/Baeldung/scala-tutorials/tree/master/scala-core-modules/scala-core-4