内联
内联定义
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
将重写为其 then
或 else
部分。因此,在上面的 log
方法中,条件为 Config.logging == true
的 if 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)
正如你所看到的,由于没有使用 msg
或 indentMargin
,它们不会出现在为 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()}")
覆盖规则
内联方法可以覆盖其他非内联方法。规则如下
-
如果内联方法
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)
内联调用和动态分派调用给出了相同的结果。
-
内联方法实际上是 final。
-
内联方法也可以是抽象的。抽象内联方法只能由其他内联方法实现。它不能直接调用
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 定义,包括纯数字计算的常量折叠等特定于平台的扩展。
内联值必须具有文字类型,例如 1
或 true
。
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
返回类型 A
或 B
中任一类型的实例。如果 choose
未声明为 transparent
,则其展开结果始终为类型 A
,即使计算值可能是子类型 B
。内联方法在某种意义上是一个“黑匣子”,即不会泄露其实现的详细信息。但是,如果给出了 transparent
修饰符,则展开就是展开体的类型。如果参数 b
为 true
,则该类型为 A
,否则为 B
。因此,由于 obj2
与 choose(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:元编程的语义保留内联 论文。