可匹配性特质
一个新的特性 Matchable
控制模式匹配的能力。
问题
Scala 3 标准库有一个类型 IArray
用于不可变数组,定义如下
opaque type IArray[+T] = Array[_ <: T]
IArray
类型提供 length
和 apply
的扩展方法,但不提供 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
。它由 AnyVal
和 AnyRef
扩展。由于 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
目前是一个没有方法的标记特征。随着时间的推移,我们可能会将 getClass
和 isInstanceOf
方法迁移到它,因为这些方法与模式匹配密切相关。
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
表明,在存在抽象类型和不透明类型的情况下,通用相等性是不安全的,因为它无法正确区分类型的含义与其表示。由于Any
和Matchable
都擦除为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)
转换为类型错误来在一定程度上缓解这个问题。