使用扩展方法规避方差检查
Circumventing variance checks with extension methods
这不编译:
class MyClass[+A] {
def myMethod(a: A): A = a
}
//error: covariant type A occurs in contravariant position in type A of value a
好吧,很公平。但这确实编译:
class MyClass[+A]
implicit class MyImplicitClass[A](mc: MyClass[A]) {
def myMethod(a: A): A = a
}
这让我们可以规避方差检查给我们带来的任何问题:
class MyClass[+A] {
def myMethod[B >: A](b: B): B = b //B >: A => B
}
implicit class MyImplicitClass[A](mc: MyClass[A]) {
def myExtensionMethod(a: A): A = mc.myMethod(a) //A => A!!
}
val foo = new MyClass[String]
//foo: MyClass[String] = MyClass@4c273e6c
foo.myExtensionMethod("Welp.")
//res0: String = Welp.
foo.myExtensionMethod(new Object())
//error: type mismatch
这感觉像是作弊。应该避免吗?还是编译器让它滑动有一些正当理由?
更新:
举个例子:
class CovariantSet[+A] {
private def contains_[B >: A](b: B): Boolean = ???
}
object CovariantSet {
implicit class ImpCovSet[A](cs: CovariantSet[A]) {
def contains(a: A): Boolean = cs.contains_(a)
}
}
看来我们已经成功完成了不可能的任务:仍然满足 A => Boolean
的协变 "set"。但如果这是不可能的,编译器难道不应该禁止它吗?
我不认为它比脱糖后的版本更作弊:
val foo: MyClass[String] = ...
new MyImplicitClass(foo).myExtensionMethod("Welp.") // compiles
new MyImplicitClass(foo).myExtensionMethod(new Object()) // doesn't
原因是 MyImplicitClass
构造函数的类型参数在考虑 myExtensionMethod
之前被推断出来。
最初我想说它不让你"circumvent whatever problems the variance checks are giving us",因为扩展方法需要用方差合法方法来表达,但这是错误的:它可以在伴侣中定义对象并使用私有状态。
我看到的唯一问题是修改代码的人可能会感到困惑(甚至不阅读它,因为那些人看不到非编译代码)。我不认为这是一个 大 问题,但如果不在实践中尝试,很难确定。
你没有实现不可能。您只是选择了与标准库中不同的权衡。
你失去了什么
签名
def contains[B >: A](b: B): Boolean
强制您以适用于 Any
的方式实现协变 Set
,因为 B
是完全不受约束的。这意味着:
- 没有
BitSet
仅 Int
- 没有
Ordering
- 没有自定义哈希函数。
此签名强制您实现 Set[Any]
.
你的收获
一个容易绕过的门面:
val x: CovariantSet[Int] = ???
(x: CovariantSet[Any]).contains("stuff it cannot possibly contain")
编译得很好。这意味着您的集合 x
已被构造为一组整数,因此只能包含整数,将在运行时强制调用方法 contains
以确定它是否包含 String
或不是,尽管它不可能包含任何 String
。因此,类型系统不会以任何方式帮助您消除此类总是会产生 false
.
的无意义查询
这不编译:
class MyClass[+A] {
def myMethod(a: A): A = a
}
//error: covariant type A occurs in contravariant position in type A of value a
好吧,很公平。但这确实编译:
class MyClass[+A]
implicit class MyImplicitClass[A](mc: MyClass[A]) {
def myMethod(a: A): A = a
}
这让我们可以规避方差检查给我们带来的任何问题:
class MyClass[+A] {
def myMethod[B >: A](b: B): B = b //B >: A => B
}
implicit class MyImplicitClass[A](mc: MyClass[A]) {
def myExtensionMethod(a: A): A = mc.myMethod(a) //A => A!!
}
val foo = new MyClass[String]
//foo: MyClass[String] = MyClass@4c273e6c
foo.myExtensionMethod("Welp.")
//res0: String = Welp.
foo.myExtensionMethod(new Object())
//error: type mismatch
这感觉像是作弊。应该避免吗?还是编译器让它滑动有一些正当理由?
更新:
举个例子:
class CovariantSet[+A] {
private def contains_[B >: A](b: B): Boolean = ???
}
object CovariantSet {
implicit class ImpCovSet[A](cs: CovariantSet[A]) {
def contains(a: A): Boolean = cs.contains_(a)
}
}
看来我们已经成功完成了不可能的任务:仍然满足 A => Boolean
的协变 "set"。但如果这是不可能的,编译器难道不应该禁止它吗?
我不认为它比脱糖后的版本更作弊:
val foo: MyClass[String] = ...
new MyImplicitClass(foo).myExtensionMethod("Welp.") // compiles
new MyImplicitClass(foo).myExtensionMethod(new Object()) // doesn't
原因是 MyImplicitClass
构造函数的类型参数在考虑 myExtensionMethod
之前被推断出来。
最初我想说它不让你"circumvent whatever problems the variance checks are giving us",因为扩展方法需要用方差合法方法来表达,但这是错误的:它可以在伴侣中定义对象并使用私有状态。
我看到的唯一问题是修改代码的人可能会感到困惑(甚至不阅读它,因为那些人看不到非编译代码)。我不认为这是一个 大 问题,但如果不在实践中尝试,很难确定。
你没有实现不可能。您只是选择了与标准库中不同的权衡。
你失去了什么
签名
def contains[B >: A](b: B): Boolean
强制您以适用于 Any
的方式实现协变 Set
,因为 B
是完全不受约束的。这意味着:
- 没有
BitSet
仅Int
- 没有
Ordering
- 没有自定义哈希函数。
此签名强制您实现 Set[Any]
.
你的收获
一个容易绕过的门面:
val x: CovariantSet[Int] = ???
(x: CovariantSet[Any]).contains("stuff it cannot possibly contain")
编译得很好。这意味着您的集合 x
已被构造为一组整数,因此只能包含整数,将在运行时强制调用方法 contains
以确定它是否包含 String
或不是,尽管它不可能包含任何 String
。因此,类型系统不会以任何方式帮助您消除此类总是会产生 false
.