Scala 类型下界错误?

Scala type lowerbound bug?

case class Level[B](b: B){
  def printCovariant[A<:B](a: A): Unit = println(a)
  def printInvariant(b: B): Unit = println(b)
  def printContravariant[C>:B](c: C): Unit = println(c)
}

class First
class Second extends First
class Third extends Second

//First >: Second >: Third

object Test extends App {

  val second = Level(new Second) //set B as Second

  //second.printCovariant(new First) //error and reasonable
  second.printCovariant(new Second) 
  second.printCovariant(new Third) 

  //second.printInvariant(new First) //error and reasonable
  second.printInvariant(new Second) 
  second.printInvariant(new Third) //why no error?

  second.printContravariant(new First) 
  second.printContravariant(new Second)
  second.printContravariant(new Third) //why no error?
}

似乎 scala 的下界类型检查有错误...对于不变大小写和逆变大小写。

不知道上面的代码有没有bug

永远记住,如果 Third 扩展了 Second,那么每当需要 Second 时,都可以提供 Third。这称为子类型多态性。

考虑到这一点,second.printInvariant(new Third) 编译就很自然了。您提供了一个 Third,它是 Second 的子类型,所以它会被检出。这就像为采用 Fruit 的方法提供 Apple。

这意味着你的方法

def printCovariant[A<:B](a: A): Unit = println(a)

可以写成:

def printCovariant(a: B): Unit = println(a)

不丢失任何信息。由于子类型多态性,第二个接受B及其所有subclasses,与第一个相同

您的第二个错误案例也是如此 - 这是子类型多态性的另一种情况。您可以传递新的 Third,因为 Third 实际上是 Second(请注意,我使用的是 subclass 和 superclass 之间的“is-a”关系,取自面向对象的表示法)。

如果您想知道为什么我们甚至需要上限(子类型多态性还不够吗?),请观察这个例子:

def foo1[A <: AnyRef](xs: A) = xs
def foo2(xs: AnyRef) = xs
val res1 = foo1("something") // res1 is a String
val res2 = foo2("something") // res2 is an Anyref

现在我们确实观察到了差异。尽管子类型多态性允许我们在两种情况下都传入一个字符串,但只有方法 foo1 可以引用其参数的类型(在我们的例子中是一个字符串)。方法 foo2 会很乐意接受一个字符串,但不会真正知道它是一个字符串。因此,当您想保留类型时,上限会派上用场(在您的情况下,您只是打印出值,所以您并不真正关心类型 - 所有类型都有一个 toString 方法)。

编辑:
(额外的细节,你可能已经知道了,但为了完整起见我会把它放上去)

上限的用途比我在这里描述的要多,但在参数化方法时,这是最常见的情况。当参数化一个class时,那么你可以用上界描述协方差,用下界描述逆变。例如,

class SomeClass[U] {

  def someMethod(foo: Foo[_ <: U]) = ???

}

表示方法 someMethod 的参数 foo 在其类型上是协变的。怎么样?好吧,通常(也就是说,没有调整方差),子类型多态性不允许我们传递一个 Foo 参数化的类型参数的子类型。如果 T <: U,那并不意味着 Foo[T] <: Foo[U]。我们说 Foo 在它的类型上是不变的。但是我们只是调整了方法以接受使用 U 其任何子类型 参数化的 Foo。现在这是有效的协方差。所以,就 someMethod 而言——如果某种类型 TU 的子类型,那么 Foo[T] 就是 Foo[U] 的子类型。太好了,我们实现了协方差。但请注意,我说的是 "as long as someMethod is concerned"。 Foo 在此方法中其类型是协变的,但在其他方法中它可能是不变的或逆变的。

这种variance declaration叫做use-site variance 因为我们在使用的时候声明一个类型的variance(这里是作为方法参数类型的someMethod)。这是 Java 中唯一的一种方差声明。当使用 use-site variance 时,你要注意 get-put principle (google it)。基本上这个原则说我们只能从协变 classes 中得到东西(我们不能放),反之亦然对于逆变 classes(我们可以放但不能得到)。在我们的例子中,我们可以这样演示:

class Foo[T] { def put(t: T): Unit = println("I put some T") }

def someMethod(foo: Foo[_ <: String]) = foo.put("asd") // won't compile
def someMethod2(foo: Foo[_ >: String]) = foo.put("asd")

更一般地说,我们只能将协变类型用作 return 类型,将逆变类型用作参数类型。

现在,use-site declaration 很好,但在 Scala 中更常见的是利用 declaration-site variance(Java 没有).这意味着我们将在定义 Foo 时描述 Foo 的泛型类型的变化。我们会简单地说 class Foo[+T]。现在我们不需要在编写使用 Foo 的方法时使用边界;我们宣称 Foo 在每个用例和每个场景中其类型都是永久协变的。

有关 Scala 中方差的更多详细信息,请随时查看我关于此主题的 blog post