在 GitHub 上編輯此頁面

內嵌

內嵌定義

inline 是一個新的 軟修改項,它保證定義會在使用時內嵌。範例

object Config:
  inline val logging = false

object Logger:

  private var indent = 0

  inline def log[T](msg: String, indentMargin: =>Int)(op: => T): T =
    if Config.logging then
      println(s"${"  " * indent}start $msg")
      indent += indentMargin
      val result = op
      indent -= indentMargin
      println(s"${"  " * indent}$msg = $result")
      result
    else op
end Logger

Config 物件包含 內嵌值 logging 的定義。這表示 logging 被視為一個常數值,等於其右側的 false。此類 inline val 的右側本身必須是一個 常數表達式。以這種方式使用時,inline 等同於 Java 和 Scala 2 的 final。請注意,表示內嵌常數final 仍受 Scala 3 支援,但會逐漸淘汰。

Logger 物件包含 內嵌方法 log 的定義。此方法會在呼叫時永遠內嵌。

在內聯程式碼中,具有常數條件的 if-then-else 將會改寫成其 thenelse 部分。因此,在上面的 log 方法中,具有 Config.logging == trueif Config.logging 將會改寫成其 then 部分。

以下是一個範例

var indentSetting = 2

def factorial(n: BigInt): BigInt =
  log(s"factorial($n)", indentSetting) {
    if n == 0 then 1
    else n * factorial(n - 1)
  }

如果 Config.logging == false,這將會改寫(簡化)成

def factorial(n: BigInt): BigInt =
  if n == 0 then 1
  else n * factorial(n - 1)

正如您所看到的,由於 msgindentMargin 都不會使用,因此它們不會出現在 factorial 的產生程式碼中。另外請注意我們的 log 方法的主體:else- 部分簡化成只有一個 op。在產生的程式碼中,我們不會產生任何封閉,因為我們只參照一次依名稱傳遞的參數。因此,程式碼會直接內聯,而呼叫會進行 β 簡約。

true 的情況下,程式碼會改寫成

def factorial(n: BigInt): BigInt =
  val msg = s"factorial($n)"
  println(s"${"  " * indent}start $msg")
  Logger.inline$indent_=(indent.+(indentSetting))
  val result =
    if n == 0 then 1
    else n * factorial(n - 1)
  Logger.inline$indent_=(indent.-(indentSetting))
  println(s"${"  " * indent}$msg = $result")
  result

請注意,依值傳遞的參數 msg 只會評估一次,根據一般的 Scala 語意,透過繫結值並在 factorial 的主體中重複使用 msg。另外,請注意對私有變數 indent 的指定之特殊處理。這是透過產生一個設定方法 def inline$indent_= 並呼叫它來達成的。

內聯方法必須永遠完全套用。例如,對

Logger.log[String]("some op", indentSetting)

的呼叫會是格式錯誤的,而編譯器會抱怨缺少引數。但是,可以傳遞萬用字元引數。例如,

Logger.log[String]("some op", indentSetting)(_)

會進行型別檢查。

遞迴內聯方法

內聯方法可以是遞迴的。例如,當呼叫具有常數指數 n 時,以下的 power 方法將會透過直接內聯程式碼實作,而不會有任何迴圈或遞迴。值得注意的是,連續內聯的數量限制為 32,且可以透過編譯器設定 -Xmax-inlines 來修改。

inline def power(x: Double, n: Int): Double =
  if n == 0 then 1.0
  else if n == 1 then x
  else
    val y = power(x, n / 2)
    if n % 2 == 0 then y * y else y * y * x

power(expr, 10)
// translates to
//
//   val x = expr
//   val y1 = x * x   // ^2
//   val y2 = y1 * y1 // ^4
//   val y3 = y2 * x  // ^5
//   y3 * y3          // ^10

內聯方法的參數也可以有inline修飾詞。這表示這些參數的實際引數將會內聯在inline def的主體中。inline參數的呼叫語意等同於依名稱參數,但允許在引數中複製程式碼。這通常在需要傳播常數值以允許進一步最佳化/簡約時很有用。

以下範例顯示依值、依名稱和inline參數之間的轉譯差異

inline def funkyAssertEquals(actual: Double, expected: =>Double, inline delta: Double): Unit =
  if (actual - expected).abs > delta then
    throw new AssertionError(s"difference between ${expected} and ${actual} was larger than ${delta}")

funkyAssertEquals(computeActual(), computeExpected(), computeDelta())
// translates to
//
//   val actual = computeActual()
//   def expected = computeExpected()
//   if (actual - expected).abs > computeDelta() then
//     throw new AssertionError(s"difference between ${expected} and ${actual} was larger than ${computeDelta()}")

覆寫規則

內聯方法可以覆寫其他非內聯方法。規則如下

  1. 如果內聯方法f實作或覆寫另一個非內聯方法,則內聯方法也可以在執行階段呼叫。例如,考慮下列情況

    abstract class A:
      def f: Int
      def g: Int = f
    
    class B extends A:
      inline def f = 22
      override inline def g = f + 11
    
    val b = new B
    val a: A = b
    // inlined invocatons
    assert(b.f == 22)
    assert(b.g == 33)
    // dynamic invocations
    assert(a.f == 22)
    assert(a.g == 33)
    

    內聯呼叫和動態派送呼叫會產生相同的結果。

  2. 內聯方法實際上是最後的。

  3. 內聯方法也可以是抽象的。抽象內聯方法只能由其他內聯方法實作。它無法直接呼叫

    abstract class A:
      inline def f: Int
    
    object B extends A:
      inline def f: Int = 22
    
    B.f         // OK
    val a: A = B
    a.f         // error: cannot inline f in A.
    

@inline的關係

Scala 2 也定義了@inline註解,用作後端內聯程式的提示。inline修飾詞是一個更強大的選項

  • 展開是有保證的,而不是盡力而為,
  • 展開發生在前置端而不是後置端,以及
  • 展開也適用於遞迴方法。

常數表達式的定義

內嵌值和內嵌參數的引數的右手邊必須是常數表達式,定義在 SLS §6.24 中,包括純數值運算的常數摺疊等平台特定的擴充功能。

內嵌值必須具有文字類型,例如 1true

inline val four = 4
// equivalent to
inline val four: 4 = 4

也可以有類型沒有語法的內嵌值,例如 Short(4)

trait InlineConstants:
  inline val myShort: Short

object Constants extends InlineConstants:
  inline val myShort/*: Short(4)*/ = 4

透明內嵌方法

內嵌方法還可以宣告為 transparent。這表示內嵌方法的傳回類型可以在擴充時特化為更精確的類型。範例

class A
class B extends A:
  def m = true

transparent inline def choose(b: Boolean): A =
  if b then new A else new B

val obj1 = choose(true)  // static type is A
val obj2 = choose(false) // static type is B

// obj1.m // compile-time error: `m` is not defined on `A`
obj2.m    // OK

在此,內聯方法 choose 傳回兩個類型 AB 的其中一個實例。如果 choose 未宣告為 transparent,其展開的結果將永遠是類型 A,即使計算出的值可能是子類型 B。內聯方法在於其實作的詳細資料不會外洩,因此是「黑盒子」。但如果給定 transparent 修飾詞,則展開會是展開主體的類型。如果引數 btrue,則該類型為 A,否則為 B。因此,在 obj2 上呼叫 m 會進行類型檢查,因為 obj2choose(false) 的展開具有相同的類型,也就是 B。透明內聯方法在於此類方法的應用程式類型可以比其宣告的傳回類型更特殊化,具體取決於方法的展開方式,因此是「白盒子」。

在以下範例中,我們會看到 zero 的傳回類型如何專門化為單例類型 0,允許將加法指定為正確的類型 1

transparent inline def zero: Int = 0

val one: 1 = zero + 1

透明與非透明內聯

正如我們已經討論過的,透明內聯方法可能會影響呼叫站台的類型檢查。技術上,這表示在程式類型檢查期間,必須展開透明內聯方法。其他內聯方法會在程式完全輸入類型後稍後內聯。

例如,以下兩個函式將以相同的方式輸入類型,但會在不同的時間內聯。

inline def f1: T = ...
transparent inline def f2: T = (...): T

一個值得注意的差異是 transparent inline given 的行為。如果在內聯該定義時報告錯誤,它將被視為隱式搜尋不匹配,並且搜尋將繼續。一個 transparent inline given 可以在其 RHS 中新增一個類型註解(如同前一個範例中的 f2)以避免精確類型,但保留搜尋行為。另一方面,一個 inline given 被視為一個隱式,然後在鍵入後內聯。任何錯誤將照常發出。

內聯條件式

一個 if-then-else 表達式,其條件為常數表達式,可以簡化為所選分支。使用 inline 為 if-then-else 表達式加上前置詞,強制條件必須為常數表達式,從而保證條件式將始終簡化。

範例

inline def update(delta: Int) =
  inline if delta >= 0 then increaseBy(delta)
  else decreaseBy(-delta)

一個呼叫 update(22) 會改寫為 increaseBy(22)。但如果 update 被呼叫時使用一個不是編譯時常數的值,我們會得到一個像下面這樣的編譯時錯誤

|  inline if delta >= 0 then ???
  |  ^
  |  cannot reduce inline if
  |   its condition
  |     delta >= 0
  |   is not a constant value
  | This location is in code that was inlined at ...

在一個 transparent inline 中,一個 inline if 將強制在類型檢查期間內聯其條件中的任何內聯定義。

內聯比對

在一個 inline 方法定義的主體中的 match 表達式可以加上 inline 修飾詞。如果在編譯時有足夠的類型資訊來選擇一個分支,該表達式將簡化為該分支,並且表達式的類型是該結果的右邊的類型。如果不是,將引發一個編譯時錯誤,報告無法簡化比對。

下面的範例定義了一個內聯方法,其中包含一個單一的內聯比對表達式,該表達式根據其靜態類型選擇一個案例

transparent inline def g(x: Any): Any =
  inline x match
    case x: String => (x, x) // Tuple2[String, String](x, x)
    case x: Double => x

g(1.0d) // Has type 1.0d which is a subtype of Double
g("test") // Has type (String, String)

靜態檢查受檢者 x,並相應地簡化內聯比對,傳回對應的值(類型已專門化,因為 g 已宣告為 transparent)。此範例對受檢者執行簡單的類型測試。該類型可以有更豐富的結構,如下方的簡單 ADT。toInt 比對 邱奇編碼 中數字的結構,並計算對應的整數。

trait Nat
case object Zero extends Nat
case class Succ[N <: Nat](n: N) extends Nat

transparent inline def toInt(n: Nat): Int =
  inline n match
    case Zero     => 0
    case Succ(n1) => toInt(n1) + 1

inline val natTwo = toInt(Succ(Succ(Zero)))
val intTwo: 2 = natTwo

推論出 natTwo 具有單例類型 2。

參考

如需有關 inline 語意的更多資訊,請參閱 Scala 2020:用於元程式設計的語意保留內聯 論文。