在 GitHub 上编辑此页面

可匹配性特质

一个新的特性 Matchable 控制模式匹配的能力。

问题

Scala 3 标准库有一个类型 IArray 用于不可变数组,定义如下

opaque type IArray[+T] = Array[_ <: T]

IArray 类型提供 lengthapply 的扩展方法,但不提供 update;因此,似乎类型为 IArray 的值无法更新。

但是,由于模式匹配,存在一个潜在的漏洞。考虑以下情况

val imm: IArray[Int] = ...
imm match
  case a: Array[Int] => a(0) = 1

测试将在运行时成功,因为 IArray 在运行时表示为 Array。但是,如果我们允许这样做,它将破坏不可变数组的基本抽象。

旁注:也可以通过强制转换来实现相同的效果

imm.asInstanceOf[Array[Int]](0) = 1

但这并不是一个大问题,因为在 Scala 中,asInstanceOf 被理解为低级且不安全的。相比之下,在没有警告或错误的情况下编译的模式匹配不应该破坏抽象。

还要注意,问题并不局限于 不透明类型 作为模式选择器。以下使用参数类型 T 的值作为模式选择器的轻微变体会导致相同的问题

def f[T](x: T) = x match
  case a: Array[Int] => a(0) = 0
f(imm)

最后,请注意,问题并不仅仅与 不透明类型 相关。任何无界类型参数或抽象类型都不应该通过模式匹配进行分解。

解决方案

现在有一种新的类型 scala.Matchable 来控制模式匹配。当对构造函数模式 C(...) 或类型模式 _: C 进行模式匹配时,要求选择器类型符合 Matchable。如果情况并非如此,则会发出警告。例如,当编译本节开头示例时,我们会得到

> sc ../new/test.scala -source future
-- Warning: ../new/test.scala:4:12 ---------------------------------------------
4 |    case a: Array[Int] => a(0) = 0
  |            ^^^^^^^^^^
  |            pattern selector should be an instance of Matchable,
  |            but it has unmatchable type IArray[Int] instead

为了允许从 Scala 2 迁移以及在 Scala 2 和 3 之间进行交叉编译,该警告仅在 -source future-migration 或更高版本中启用。

Matchable 是一个通用特征,其父类为 Any。它由 AnyValAnyRef 扩展。由于 Matchable 是每个具体值或引用类的超类型,这意味着可以像以前一样匹配此类类的实例。但是,以下类型的匹配选择器将产生警告

  • 类型 Any:如果需要模式匹配,则应使用 Matchable 代替。
  • 无界类型参数和抽象类型:如果需要模式匹配,它们应该具有一个上界 Matchable
  • 仅受某些通用特征约束的类型参数和抽象类型:同样,应将 Matchable 添加为约束。

以下是顶级类和特征及其定义方法的层次结构

abstract class Any:
  def getClass
  def isInstanceOf
  def asInstanceOf
  def ==
  def !=
  def ##
  def equals
  def hashCode
  def toString

trait Matchable extends Any

class AnyVal extends Any, Matchable
class Object extends Any, Matchable

Matchable 目前是一个没有方法的标记特征。随着时间的推移,我们可能会将 getClassisInstanceOf 方法迁移到它,因为这些方法与模式匹配密切相关。

Matchable 和通用相等性

对类型为 Any 的选择器进行模式匹配的方法,一旦启用 Matchable 警告,就需要进行强制转换。最常见的此类方法是通用 equals 方法。它必须像以下示例中那样编写

class C(val x: String):

  override def equals(that: Any): Boolean =
    that.asInstanceOf[Matchable] match
      case that: C => this.x == that.x
      case _ => false

that强制转换为Matchable表明,在存在抽象类型和不透明类型的情况下,通用相等性是不安全的,因为它无法正确区分类型的含义与其表示。由于AnyMatchable都擦除为Object,因此强制转换在运行时保证成功。

例如,考虑以下定义

opaque type Meter = Double
def Meter(x: Double): Meter = x

opaque type Second = Double
def Second(x: Double): Second = x

这里,通用equals将对以下情况返回true

Meter(10).equals(Second(10))

尽管从数学角度来看这显然是错误的。使用多重宇宙相等性,可以通过将

import scala.language.strictEquality
  Meter(10) == Second(10)

转换为类型错误来在一定程度上缓解这个问题。