在 GitHub 上編輯此頁面

安全初始化

Scala 3 實作實驗性的安全初始化檢查,可透過編譯器選項 -Ysafe-init 啟用。

初始化檢查器的設計和實作說明在論文中安全物件初始化,抽象地 [3] 中。

快速瀏覽

為了了解其運作方式,我們先在下方顯示幾個範例。

父類-子類互動

針對以下程式碼片段

abstract class AbstractFile:
  def name: String
  val extension: String = name.substring(4)

class RemoteFile(url: String) extends AbstractFile:
  val localFile: String = s"${url.##}.tmp"  // error: usage of `localFile` before it's initialized
  def name: String = localFile

檢查器會報告

-- Warning: tests/init/neg/AbstractFile.scala:7:4 ------------------------------
7 |	val localFile: String = s"${url.##}.tmp"  // error: usage of `localFile` before it's initialized
  |	    ^
  |    Access non-initialized field value localFile. Calling trace:
  |     -> val extension: String = name.substring(4)	[ AbstractFile.scala:3 ]
  |      -> def name: String = localFile            	[ AbstractFile.scala:8 ]

內部與外部互動

給定以下程式碼

object Trees:
  class ValDef { counter += 1 }
  class EmptyValDef extends ValDef
  val theEmptyValDef = new EmptyValDef
  private var counter = 0  // error

檢查器會報告

-- Warning: tests/init/neg/trees.scala:5:14 ------------------------------------
5 |  private var counter = 0  // error
  |              ^
  |             Access non-initialized field variable counter. Calling trace:
  |              -> val theEmptyValDef = new EmptyValDef    [ trees.scala:4 ]
  |               -> class EmptyValDef extends ValDef       [ trees.scala:3 ]
  |                -> class ValDef { counter += 1 }	     [ trees.scala:2 ]

函式

給定以下程式碼

abstract class Parent:
  val f: () => String = () => this.message
  def message: String

class Child extends Parent:
  val a = f()
  val b = "hello"           // error
  def message: String = b

檢查器報告

-- Warning: tests/init/neg/features-high-order.scala:7:6 -----------------------
7 |  val b = "hello"           // error
  |      ^
  |Access non-initialized field value b. Calling trace:
  | -> val a = f()                              	[ features-high-order.scala:6 ]
  |   -> val f: () => String = () => this.message	[ features-high-order.scala:2 ]
  |    -> def message: String = b	                [ features-high-order.scala:8 ]

設計目標

我們建立以下設計目標

  • 健全:檢查總是會終止,並且對一般且合理的用法是健全的(過度近似)
  • 表達性:支援一般且合理的初始化模式
  • 友善:簡單的規則、最小的語法開銷、提供有意義的錯誤訊息
  • 模組化:模組化檢查,不分析專案邊界之外的內容
  • 快速:即時回饋
  • 簡單:不變更核心類型系統,可以用簡單的理論來解釋

合理的用法中,我們包含以下用例(但不限於這些用例)

  • 在初始化期間存取 this 和外部 this 上的欄位
  • 在初始化期間呼叫 this 和外部 this 上的方法
  • 在初始化期間建立內部類別並呼叫這些實例上的方法
  • 在函式中擷取欄位

原則

為了達成目標,我們堅持以下基本原則:可堆疊性單調性可作用域性權威性

可堆疊性表示類別的所有欄位都在類別主體的結尾處初始化。Scala 透過在語法中要求所有欄位都在主建構函式的結尾處初始化來強制執行此屬性,但下列語言功能除外

var x: T = _

控制效果(例如例外狀況)可能會中斷此屬性,如下列範例所示

class MyException(val b: B) extends Exception("")
class A:
  val b = try { new B } catch { case myEx: MyException => myEx.b }
  println(b.a)

class B:
  throw new MyException(this)
  val a: Int = 1

在上述程式碼中,控制效果會傳送包裝在例外狀況中的未初始化值。在實作中,我們透過確保擲出的值必須是遞移初始化來避免問題。

單調性表示物件的初始化狀態不應後退:已初始化的欄位會持續初始化,指向已初始化物件的欄位不會在稍後指向正在初始化的物件。例如,以下程式碼會被拒絕

trait Reporter:
  def report(msg: String): Unit

class FileReporter(ctx: Context) extends Reporter:
  ctx.typer.reporter = this                // ctx now reaches an uninitialized object
  val file: File = new File("report.txt")
  def report(msg: String) = file.write(msg)

在上面的程式碼中,假設 ctx 指向一個暫時初始化的物件。現在第 3 行的指定會讓 this(尚未完全初始化)可以從 ctx 存取。這會讓欄位使用變得危險,因為它可能間接存取到未初始化的欄位。

單調性基於一種稱為「堆單調型態狀態」的知名技術,以確保在別名存在的情況下依然健全 [1]。粗略來說,這表示初始化狀態不應該倒退。

可掃描性表示沒有側頻道可以存取部分建構的物件。控制效果(例如:coroutine、分界控制、可恢復例外)可能會破壞這個屬性,因為它們可以傳送堆疊中較上層(不在範圍內)的值,讓目前的範圍可以存取。靜態欄位也可以作為傳送點,因此會破壞這個屬性。在實作中,我們需要強制執行已傳送的值暫時初始化。

上述三個原則有助於「初始化的局部推理」,表示

已初始化的環境只能產生已初始化的值。

例如,如果 new 表達式的引數暫時初始化,則結果也會初始化。如果方法呼叫中的接收器和引數暫時初始化,則結果也會初始化。

初始化的局部推理會產生快速的初始化檢查器,因為它避免了全程式分析。

權威原則與單調性相輔相成:單調性原則規定初始化狀態不能倒退,而權威原則規定初始化狀態不能因為別名而任意向前。在 Scala 中,我們只能在類別主體中定義欄位為強制初始化項或在物件暫時初始化的局部推理點時,才能提升物件的初始化狀態。

抽象值

物件初始化狀態有三個基本抽象

  • :冷物件可能有未初始化的欄位。
  • :溫物件的所有欄位都已初始化,但可能會存取到物件。
  • :熱物件暫時初始化,也就是說它只會存取溫物件。

在初始化檢查器中,抽象 Warm 會經過細化,以處理內部類別和多個建構函式

  • Warm[C] { outer = V, ctor, args = Vs }:類別 C 的溫物件,其中 C 的立即外部物件為 V,建構函式為 ctor,建構函式引數為 Vs

初始化檢查器會個別檢查每個具體類別。抽象 ThisRef 代表目前正在初始化的物件

  • ThisRef[C]:初始化中的類別 C 的目前物件。

目前物件的初始化狀態儲存在抽象堆疊中,作為抽象物件。抽象堆疊也作為熱物件的欄位值快取。WarmThisRef 是儲存在抽象堆疊中的抽象物件的「位址」。

引入了兩個抽象概念來支援函式和條件式

  • Fun(e, V, C):抽象函式值,其中 e 是程式碼,V 是函式主體內 this 的抽象值,而函式位於類別 C 內。

  • Refset(Vs):抽象值 Vs 的集合。

如果符合下列任一條件,則值 v實際上已熱

  • vHot
  • vThisRef,且基礎物件的所有欄位都已指派。
  • vWarm[C] { ... },且
    1. C 不包含內部類別;且
    2. v 呼叫任何方法都不會遇到初始化錯誤,且方法傳回值為實際上已熱;且
    3. v 的每個欄位都為實際上已熱
  • vFun(e, V, C),且呼叫函式不會遇到錯誤,且函式傳回值為實際上已熱
  • 根物件(由 ThisRef 參照)為實際上已熱

實際上已熱的值可視為傳遞初始化,因此可透過方法引數或重新指派右側安全地洩漏。初始化檢查器會在可能時嘗試將非熱值提升為實際上已熱。

規則

在既定的原則和設計目標下,實施下列規則

  1. 如果 e,則欄位存取 e.f 或方法呼叫 e.m() 為非法。

    不應使用冷值。

  2. 如果 e 的值為 ThisRef,且 f 未初始化,則欄位存取 e.f 無效。

  3. 在指派 o.x = e 中,表示式 e 必須為實際上已熱

    這是系統中如何強制單調性的方式。請注意,在初始化 val f: T = e 中,表示式 e 可能指向非熱值。

  4. 方法呼叫的引數必須為實際上已熱

    建構函式中 this 的逸出通常被視為反模式。

    不過,傳遞非熱值作為另一個建構式的引數是允許的,以支援循環資料結構的建立。檢查程式會確保已逃逸的非初始化物件不會被使用,也就是說,不允許呼叫方法或存取已逃逸物件的欄位。

    呼叫案例類別的合成 apply 是個例外。例如,方法呼叫 Some.apply(e) 會被解釋為 new Some(e),因此即使 e 不是熱值,仍然有效。

    這項規則的另一個例外是參數化方法呼叫。例如,在 List.apply(e) 中,引數 e 可能是非熱值。如果是這樣,參數化方法呼叫的結果值會被視為冷值

  5. 對熱值呼叫方法,並使用有效熱值作為引數,會產生熱值結果。

    這項規則是由關於初始化的局部推論所確保的。

  6. ThisRef 和溫值呼叫方法會在靜態中解析,並檢查對應的方法主體。

  7. 在新的表達式 new p.C(args) 中,如果 pargs 的值有效熱值,則結果值也是熱值。

    這項規則是由關於初始化的局部推論所確保的。

  8. 在新的表達式 new p.C(args) 中,如果 pargs 的任何值都不是有效熱值,則結果值會採取 Warm[C] { outer = Vp, args = Vargs } 形式。類別 C 的初始化程式碼會再次檢查,以確保非熱值已正確使用。

    在上述範例中,Vpp 的擴充值 --- 如果 p 是溫值 Warm[D] { outer = V, args },擴充就會發生,而我們會將其擴充為 Warm[D] { outer = Cold, args }

    變數 Vargs 代表 args 的值,其中非熱值已擴充為 Cold

    擴充的動機是將抽象網域有限化,並確保初始化檢查的終止。

  9. 樣式比對中的被檢驗者和 return 和 throw 陳述式中的值必須是有效熱值

模組化

分析將具體類別的主要建構式視為進入點。它遵循超類別的建構式,這些建構式可能定義在其他專案中。分析利用 TASTy 分析在其他專案中定義的超類別。

跨越專案邊界會引發對模組化的疑慮。在物件導向程式設計中,超類別和子類別緊密結合,這是眾所周知的。例如,在超類別中新增方法需要重新編譯子類別,以檢查安全的覆寫。

初始化在這個方面並非例外。物件初始化基本上涉及子類別與超類別之間的密切互動。如果超類別是在另一個專案中定義,則為了分析的健全性,無法避免跨專案邊界的交叉。

同時,跨專案邊界的繼承一直受到審查,而開放類別的引入在此緩解了疑慮。例如,初始化檢查可以強制執行開放類別的建構函式可能不包含對 this 的方法呼叫,或將註解作為合約引入。

歡迎社群對此主題的回饋。

後門

偶爾,您可能希望抑制檢查器報告的警告。您可以撰寫 e: @unchecked 來告訴檢查器略過檢查表達式 e,或者您可以使用舊技巧:將某些欄位標記為延遲。

注意事項

  • 在延伸 Java 或 Scala 2 類別時,系統無法提供安全性保證。
  • 僅部分檢查了全域物件的安全初始化。

參考文獻

  1. Fähndrich, M. 和 Leino, K.R.M.,2003 年 7 月。堆單調類型狀態。在物件導向程式設計中關於別名、限制和擁有權的國際研討會 (IWACO)。
  2. Fengyun Liu、Ondřej Lhoták、Aggelos Biboudis、Paolo G. Giarrusso 和 Martin Odersky。物件初始化的類型和效果系統。OOPSLA,2020 年。
  3. Fengyun Liu、Ondřej Lhoták、Enze Xing、Nguyen Cao Pham。安全的物件初始化,抽象地。Scala 2021 年。