此文档页面特定于 Scala 3,并且可能涵盖 Scala 2 中不可用的新概念。除非另有说明,此页面中的所有代码示例均假定您正在使用 Scala 3。
不透明类型别名提供类型抽象,没有任何开销。在 Scala 2 中,可以使用 值类 来实现类似的结果。
抽象开销
假设我们想要定义一个模块,该模块提供对数字的算术运算,这些数字由其对数表示。当涉及的数值往往非常大或接近零时,这有助于提高精度。
由于区分“常规”双值和存储为其对数的数字非常重要,因此我们引入了类 Logarithm
class Logarithm(protected val underlying: Double):
def toDouble: Double = math.exp(underlying)
def + (that: Logarithm): Logarithm =
// here we use the apply method on the companion
Logarithm(this.toDouble + that.toDouble)
def * (that: Logarithm): Logarithm =
new Logarithm(this.underlying + that.underlying)
object Logarithm:
def apply(d: Double): Logarithm = new Logarithm(math.log(d))
伴生对象上的 apply 方法让我们可以创建类型为 Logarithm
的值,我们可以按照如下方式使用
val l2 = Logarithm(2.0)
val l3 = Logarithm(3.0)
println((l2 * l3).toDouble) // prints 6.0
println((l2 + l3).toDouble) // prints 4.999...
虽然类 Logarithm
为存储在此特定对数形式中的 Double
值提供了一个不错的抽象,但它会带来严重的性能开销:对于每个数学运算,我们需要提取底层值,然后再次将其包装在 Logarithm
的新实例中。
模块抽象
让我们考虑另一种实现相同库的方法。这次我们不是将 Logarithm
定义为一个类,而是使用类型别名来定义它。首先,我们定义模块的抽象接口
trait Logarithms:
type Logarithm
// operations on Logarithm
def add(x: Logarithm, y: Logarithm): Logarithm
def mul(x: Logarithm, y: Logarithm): Logarithm
// functions to convert between Double and Logarithm
def make(d: Double): Logarithm
def extract(x: Logarithm): Double
// extension methods to use `add` and `mul` as "methods" on Logarithm
extension (x: Logarithm)
def toDouble: Double = extract(x)
def + (y: Logarithm): Logarithm = add(x, y)
def * (y: Logarithm): Logarithm = mul(x, y)
现在,让我们通过说类型 Logarithm
等于 Double
来实现此抽象接口
object LogarithmsImpl extends Logarithms:
type Logarithm = Double
// operations on Logarithm
def add(x: Logarithm, y: Logarithm): Logarithm = make(x.toDouble + y.toDouble)
def mul(x: Logarithm, y: Logarithm): Logarithm = x + y
// functions to convert between Double and Logarithm
def make(d: Double): Logarithm = math.log(d)
def extract(x: Logarithm): Double = math.exp(x)
在 LogarithmsImpl
的实现中,等式 Logarithm = Double
允许我们实现各种方法。
泄漏抽象
但是,此抽象有点泄漏。我们必须确保仅针对抽象接口 Logarithms
编程,并且永远不要直接使用 LogarithmsImpl
。直接使用 LogarithmsImpl
会让用户看到等式 Logarithm = Double
,用户可能会意外地在需要对数 double 的地方使用 Double
。例如
import LogarithmsImpl.*
val l: Logarithm = make(1.0)
val d: Double = l // type checks AND leaks the equality!
将模块分成抽象接口和实现可能很有用,但这样做也需要付出很多努力,仅仅是为了隐藏 Logarithm
的实现细节。针对抽象模块 Logarithms
编程可能会非常繁琐,并且通常需要使用高级功能(例如路径相关类型),如下例所示
def someComputation(L: Logarithms)(init: L.Logarithm): L.Logarithm = ...
装箱开销
类型抽象(例如 type Logarithm
擦除到它们的界限(在本例中为 Any
)。也就是说,虽然我们不需要手动包装和解包 Double
值,但仍然会有一些与包装原始类型 Double
相关的装箱开销。
不透明类型
我们无需手动将 Logarithms
组件拆分为抽象部分和具体实现,而是可以简单地在 Scala 3 中使用不透明类型来实现类似的效果
object Logarithms:
//vvvvvv this is the important difference!
opaque type Logarithm = Double
object Logarithm:
def apply(d: Double): Logarithm = math.log(d)
extension (x: Logarithm)
def toDouble: Double = math.exp(x)
def + (y: Logarithm): Logarithm = Logarithm(math.exp(x) + math.exp(y))
def * (y: Logarithm): Logarithm = x + y
Logarithm
与 Double
相同的事实仅在定义 Logarithm
的范围内得知,在上述示例中,它对应于对象 Logarithms
。类型等式 Logarithm = Double
可用于实现方法(如 *
和 toDouble
)。
然而,在模块外部,类型 Logarithm
是完全封装的,或“不透明的”。对于 Logarithm
的用户来说,不可能发现 Logarithm
实际上是作为 Double
实现的。
import Logarithms.*
val log2 = Logarithm(2.0)
val log3 = Logarithm(3.0)
println((log2 * log3).toDouble) // prints 6.0
println((log2 + log3).toDouble) // prints 4.999...
val d: Double = log2 // ERROR: Found Logarithm required Double
即使我们对 Logarithm
进行了抽象,但抽象是免费的:由于只有一种实现,因此在运行时,对于像 Double
这样的基本类型将没有装箱开销。
不透明类型的摘要
不透明类型提供对实现细节的健全抽象,而不会造成性能开销。如上所述,不透明类型使用方便,并且与 扩展方法 特性很好地集成在一起。