在 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 的赋值的特殊处理。它是通过生成一个 setter 方法 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. 内联方法实际上是 final。

  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。因此,由于 obj2choose(false) 的展开类型相同(即 B),因此在 obj2 上调用 m 会进行类型检查。透明内联方法在某种意义上是“白匣子”,因为此类方法的应用类型可以比其声明的返回类型更专门,具体取决于该方法的展开方式。

在以下示例中,我们将看到 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 ...

在透明内联中,inline if 将强制在类型检查期间内联其条件中的任何内联定义。

内联匹配

inline 方法定义的主体中的 match 表达式可以添加 inline 修饰符前缀。如果在编译时有足够的类型信息来选择分支,则表达式将简化为该分支,并且表达式的类型是该结果右侧的类型。如果没有,则会引发编译时错误,报告无法简化匹配。

下面的示例定义了一个内联方法,其中包含一个内联匹配表达式,该表达式根据静态类型选择一个 case

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 匹配 Church 编码 中数字的结构,并计算相应的整数。

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:元编程的语义保留内联 论文。