X =:= Y
只是类型 =:=[X, Y]
的语法糖(中缀表示法)。
因此,当你执行 implicitly[Y =:= Y]
时,你只是在查找类型为 =:=[X, Y]
的隐式值。
=:=
是在 Predef
中定义的通用 trait。
另外,=:=
是一个有效的类型名称,因为类型名称(就像任何标识符一样)可以包含特殊字符。
从现在开始,让我们将 =:=
重命名为 IsSameType
并删除中缀表示法,以使我们的代码看起来更直观、不那么神奇。
这给我们带来了 implicitly[IsSameType[X,Y]]
以下是该类型定义的简化版本:
sealed abstract class IsSameType[X, Y]
object IsSameType {
implicit def tpEquals[A] = new IsSameType[A, A]{}
}
注意,
tpEquals
为任何类型
A
提供了一个隐式值
IsSameType[A, A]
。
换句话说,它仅在
X
和
Y
是同一类型时提供
IsSameType[X, Y]
的隐式值。
因此,
implicitly[IsSameType[Foo, Foo]]
可以编译通过。
但是,
implicitly[IsSameType[Int, String]]
不行,因为在这里没有适用于类型
IsSameType[Int, String]
的隐式值,因为
tpEquals
在这里不适用。
所以,通过这个非常简单的构造,我们能够静态检查某些类型
X
是否与另一个类型
Y
相同。
现在这里有一个示例,展示了它可能会如何有用。假设我想定义一个 Pair 类型(忽略它已经存在于标准库中的事实):
case class Pair[X,Y]( x: X, y: Y ) {
def swap: Pair[Y,X] = Pair( y, x )
}
Pair
是由其两个元素的类型参数化的,这些元素可以是任何类型,最重要的是它们没有关联。
那么如果我想定义一个方法
toList
来将这个 pair 转换成一个包含 2 个元素的列表呢?
只有在
X
和
Y
相同时,这个方法才真正有意义,否则我将被迫返回一个
List[Any]
。
而我肯定不希望改变
Pair
的定义为
Pair[T]( x: T, y: T )
,因为我真的想要能够拥有异构类型的 pair。
毕竟,只有在调用
toList
时,我才需要 X == Y。所有其他方法(例如
swap
)都应该可用于任何类型的异构 pair。
因此,最终我真正想要的是在调用
toList
时静态确保 X == Y,这样就可以一致地返回一个
List[X]
(或者一个等同的
List[Y]
)。
case class Pair[X,Y]( x: X, y: Y ) {
def swap: Pair[Y,X] = Pair( y, x )
def toList( implicit evidence: IsSameType[X, Y] ): List[Y] = ???
}
但是在实际实现toList
时仍然存在一个严重的问题。如果我尝试编写显而易见的实现,它将无法编译:
def toList( implicit evidence: IsSameType[X, Y] ): List[Y] = List[Y]( x, y )
编译器会报告
x
不是类型
Y
。实际上,对于编译器来说,
X
和
Y
仍然是不同的类型。
只有通过精心构造,我们才能静态地确保X == Y(即,
toList
需要一个隐式值为
IsSameType[X, Y]
的事实,并且它们只能由
tpEquals
方法提供,前提是X == Y)。
但编译器肯定不会解密这个巧妙的构造,以得出X == Y的结论。
为了解决这种情况,我们可以提供一个从X到Y的隐式转换,前提是我们知道X == Y(或者换句话说,在范围内有一个
IsSameType[X, Y]
的实例)。
implicit def sameTypeConvert[X,Y]( x: X )( implicit evidence: IsSameType[X, Y] ): Y = x.asInstanceOf[Y]
现在,我们实现的
toList
终于编译正常了:通过隐式转换
sameTypeConvert
,
x
将简单地转换为
Y
。
最后的微调是,我们可以进一步简化:鉴于我们已经将一个隐式值(
evidence
)作为参数,为什么不让这个值实现转换呢?如下所示:
sealed abstract class IsSameType[X, Y] extends (X => Y) {
def apply( x: X ): Y = x.asInstanceOf[Y]
}
object IsSameType {
implicit def tpEquals[A] = new IsSameType[A, A]{}
}
我们现在可以移除方法
sameTypeConvert
,因为隐式转换现在由
IsSameType
实例自身提供。现在
IsSameType
具有双重作用:静态确保X == Y,并(如果是)提供隐式转换,实际上允许我们将
X
的实例视为
Y
的实例。
我们现在基本上重新实现了在
Predef
中定义的类型
=:=
更新:从评论中看来,使用
asInstanceOf
似乎让人们感到困扰(即使它只是一个实现细节,
IsSameType
的任何用户都不需要进行转换)。事实证明,即使在实现中也很容易摆脱它。看这里:
sealed abstract class IsSameType[X, Y] extends (X => Y) {
def apply(x: X): Y
}
object IsSameType {
implicit def tpEquals[A] = new IsSameType[A, A]{
def apply(x: A): A = x
}
}
基本上,我们只是保留了
apply
的抽象,只在
tpEquals
中实现它,在那里我们(和编译器)知道传递的参数和返回值确实具有相同的类型。因此不需要任何转换。就是这样。
请注意,最终生成的字节码仍然存在相同的转换,但现在从源代码中消失了,并且从编译器的角度来看是可证明正确的。虽然我们引入了一个额外的(匿名)类(因此从抽象类到具体类还有一个附加的间接),但由于我们处于“单态方法分派”的简单情况下(如果您对虚拟机的内部工作原理感兴趣,请查找),它应该在任何良好的虚拟机上运行得像闪电一样快。尽管这可能使虚拟机更难将调用内联到
apply
(运行时虚拟机优化是一种黑色的艺术,很难做出明确的陈述)。
最后要强调的是,如果可以证明是正确的,则在代码中有转换并不是什么大问题。毕竟,标准库自己最近也有着完全相同的转换(现在整个实现已经重新设计,看起来更加强大,但仍然在其他地方包含转换)。如果对于标准库来说足够好,那对我也足够好。
apply
方法如何作为隐式转换?在 repl 中,如果我执行implicit def blah = new SomeClass
,其中SomeClass
有一个apply
方法来进行所需的转换,它不起作用 - 没有可用的隐式转换。 - Ragapply
就足以将一个对象用作函数(多亏了简单的语法糖),但是该对象仍然不是一个真正的函数,因此即使它有一个apply
方法,也不能作为隐式转换的候选。所以我在代码中的修复方法是让IsSameType
扩展(X => Y)
(如果你查看Predef
中=:=
的定义,你会发现它确实扩展了From => To
)。感谢你指出这个问题,我已经更新了我的答案。 - Régis Jean-Gilles=:=
,并逐步从头开始实现它。在此过程中,我揭示了=:=
(在我的情况下命名为IsSameType
)的内部工作方式,这确实需要使用asInstanceOf
(请参见原始代码:https://github.com/scala/scala/blob/v2.11.8/src/library/scala/Predef.scala#L406)。 - Régis Jean-GillesIsSameType[X,Y]
定义了一对方法:def to(x : X) : Y
和def from(y: Y) : X
,则不需要使用asInstanceOf
。https://gist.github.com/edgarklerks/b57dbf554bcee21ae389f2aee45b5bba - Edgar KlerksIsSameType
中正确实现“apply”方法时才需要进行类型转换。 - Régis Jean-Gilles