此文件頁面專屬於 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
,而預期的是對數雙精度浮點數。例如
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
等原始類型,將沒有封裝開銷。
不透明類型的摘要
不透明類型提供對實作細節的健全抽象化,而不會造成效能開銷。如上所述,不透明類型使用方便,且與 擴充方法 功能整合得很好。