在 GitHub 上编辑此页面

已删除的定义

erased 是一个修饰符,表示某些定义或表达式被编译器擦除,而不是在编译后的输出中表示。它还不是 Scala 语言标准的一部分。要启用 erased,请打开语言功能 experimental.erasedDefinitions。这可以通过语言导入来完成

import scala.language.experimental.erasedDefinitions

或通过设置命令行选项 -language:experimental.erasedDefinitions 来完成。已擦除的定义必须在实验性范围内(参见 实验性定义)。

为什么擦除项?

让我们用一个例子来描述擦除项背后的动机。在下面,我们展示了一个简单的状态机,它可以处于 OnOff 状态。该机器只能在当前处于 Off 状态的情况下,通过 turnedOnOff 状态变为 On 状态。最后一个约束通过 IsOff[S] 上下文证据来捕获,该证据仅存在于 IsOff[Off] 中。例如,不允许在 On 状态下调用 turnedOn,因为我们需要一个类型为 IsOff[On] 的证据,而该证据将找不到。

sealed trait State
final class On extends State
final class Off extends State

@implicitNotFound("State must be Off")
class IsOff[S <: State]
object IsOff:
  given isOff: IsOff[Off] = new IsOff[Off]

class Machine[S <: State]:
  def turnedOn(using IsOff[S]): Machine[On] = new Machine[On]

val m = new Machine[Off]
m.turnedOn
m.turnedOn.turnedOn // ERROR
//                 ^
//                  State must be Off

请注意,在上面的代码中,IsOff 的实际上下文参数在运行时从未使用过;它们仅用于在编译时建立正确的约束。由于这些术语在运行时从未使用过,因此没有真正需要保留它们,但它们仍然需要以某种形式存在于生成的代码中,以便能够进行单独编译并保留二进制兼容性。我们引入 *擦除项* 来克服此限制:我们能够在编译时对项强制执行正确的约束。这些项没有运行时语义,并且它们被完全擦除。

如何定义擦除项?

方法和函数的参数可以声明为擦除的,在每个擦除参数前面放置 erased(类似于 inline)。

def methodWithErasedEv(erased ev: Ev, x: Int): Int = x + 2

val lambdaWithErasedEv: (erased Ev, Int) => Int =
  (erased ev, x) => x + 2

erased 参数不能用于计算,但可以作为其他 erased 参数的参数使用。

def methodWithErasedInt1(erased i: Int): Int =
  i + 42 // ERROR: can not use i

def methodWithErasedInt2(erased i: Int): Int =
  methodWithErasedInt1(i) // OK

不仅参数可以标记为擦除的,valdef 也可以标记为 erased。这些也只可作为 erased 参数的参数使用。

erased val erasedEvidence: Ev = ...
methodWithErasedEv(erasedEvidence, 40) // 42

擦除值在运行时会发生什么?

由于 erased 保证不会在计算中使用,因此它们可以并且将被擦除。

// becomes def methodWithErasedEv(x: Int): Int at runtime
def methodWithErasedEv(x: Int, erased ev: Ev): Int = ...

def evidence1: Ev = ...
erased def erasedEvidence2: Ev = ... // does not exist at runtime
erased val erasedEvidence3: Ev = ... // does not exist at runtime

// evidence1 is not evaluated and only `x` is passed to methodWithErasedEv
methodWithErasedEv(x, evidence1)

带有擦除证据的状态机示例

以下示例是简单状态机的扩展实现,该状态机可以处于 OnOff 状态。机器只能在当前处于 Off 状态时使用 turnedOnOff 状态变为 On 状态,反之,只能在当前处于 On 状态时使用 turnedOffOn 状态变为 Off 状态。这些最后的约束通过 IsOff[S]IsOn[S] 来捕获,给定的证据仅存在于 IsOff[Off]IsOn[On] 中。例如,不允许在 Off 状态下调用 turnedOff,因为我们需要一个 IsOn[Off] 证据,而该证据将找不到。

由于 turnedOnturnedOff 的给定证据未在这些函数的主体中使用,因此我们可以将它们标记为 erased。这将在运行时删除证据参数,但我们仍然会评估作为参数找到的 isOnisOff 给定值。由于 isOnisOff 除了作为 erased 参数之外没有其他用途,因此我们可以将它们标记为 erased,从而删除 isOnisOff 证据的评估。

import scala.annotation.implicitNotFound

sealed trait State
final class On extends State
final class Off extends State

@implicitNotFound("State must be Off")
class IsOff[S <: State]
object IsOff:
  // will not be called at runtime for turnedOn, the
  // compiler will only require that this evidence exists
  given IsOff[Off] = new IsOff[Off]

@implicitNotFound("State must be On")
class IsOn[S <: State]
object IsOn:
  // will not exist at runtime, the compiler will only
  // require that this evidence exists at compile time
  erased given IsOn[On] = new IsOn[On]

class Machine[S <: State] private ():
  // ev will disappear from both functions
  def turnedOn(using erased ev: IsOff[S]): Machine[On] = new Machine[On]
  def turnedOff(using erased ev: IsOn[S]): Machine[Off] = new Machine[Off]

object Machine:
  def newMachine(): Machine[Off] = new Machine[Off]

@main def test =
  val m = Machine.newMachine()
  m.turnedOn
  m.turnedOn.turnedOff

  // m.turnedOff
  //            ^
  //            State must be On

  // m.turnedOn.turnedOn
  //                    ^
  //                    State must be Off

请注意,在 编译时操作 中,我们讨论了 erasedValue 和内联匹配。erasedValue 在内部使用 erased 实现(并且不是实验性的),因此上面的状态机可以编码如下

import scala.compiletime.*

sealed trait State
final class On extends State
final class Off extends State

class Machine[S <: State]:
  transparent inline def turnOn(): Machine[On] =
    inline erasedValue[S] match
      case _: Off => new Machine[On]
      case _: On  => error("Turning on an already turned on machine")

  transparent inline def turnOff(): Machine[Off] =
    inline erasedValue[S] match
      case _: On  => new Machine[Off]
      case _: Off => error("Turning off an already turned off machine")

object Machine:
  def newMachine(): Machine[Off] =
    println("newMachine")
    new Machine[Off]
end Machine

@main def test =
  val m = Machine.newMachine()
  m.turnOn()
  m.turnOn().turnOff()
  m.turnOn().turnOn() // error: Turning on an already turned on machine

擦除类

erased 也可以用作类的修饰符。擦除类仅用于擦除定义。如果 val 定义或参数的类型是(可能被别名化、细化或实例化的)擦除类,则该定义被认为是 erased 本身。类似地,具有擦除类返回值类型的方法被认为是 erased 本身。由于给定实例扩展为 vals 和 defs,因此如果它们生成的类型是擦除类,则它们也被认为是擦除的。最后,具有擦除类作为参数的函数类型将变成擦除函数类型。

示例

erased class CanRead

val x: CanRead = ...        // `x` is turned into an erased val
val y: CanRead => Int = ... // the function is turned into an erased function
def f(x: CanRead) = ...     // `f` takes an erased parameter
def g(): CanRead = ...      // `g` is turned into an erased def
given CanRead = ...         // the anonymous given is assumed to be erased

上面的代码扩展为

erased class CanRead

erased val x: CanRead = ...
val y: (erased CanRead) => Int = ...
def f(erased x: CanRead) = ...
erased def g(): CanRead = ...
erased given CanRead = ...

在擦除后,它会检查是否没有对擦除类值的引用,并且没有创建擦除类的实例。因此,以下将是错误的

val err: Any = CanRead() // error: illegal reference to erased class CanRead

这里,err 的类型是 Any,因此 err 不被认为是擦除的。然而,它的初始化值是对擦除类 CanRead 的引用。

更多细节