Scala类型的低端错误?

时间:2016-04-13 06:54:30

标签: scala types lower-bound

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的下限类型检查似乎有错误...对于不变情况和逆变情况。

我不知道上面的代码是否有错误。

1 个答案:

答案 0 :(得分:4)

请务必注意,如果Third延长Second,那么每当需要Second时,都可以提供Third。这称为亚型多态性。

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

这意味着你的方法

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

可以写成:

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

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

同样适用于您的第二个错误案例 - 这是子类型多态性的另一个案例。你可以传递新的第三个,因为第三个实际上是第二个(请注意,我使用了从面向对象的符号中取得的子类和超类之间的“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

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

修改
(额外的细节,你可能已经知道这一点,但我会说它是完整的)

我在这里描述的上限有更多用途,但在参数化方法时,这是最常见的情况。在参数化类时,您可以使用上限来描述协方差和下限来描述逆变。例如,

class SomeClass[U] {

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

}

表示方法foo的参数someMethod在其类型中是协变的。怎么样?好吧,通常(也就是说,没有调整方差),子类型多态性不允许我们传递一个Foo参数化的类型参数的子类型。如果T <: U,则不代表Foo[T] <: Foo[U]。我们说Foo在其类型中是不变的。但我们只是调整了方法,以接受Foo参数化的U其任何子类型。现在这是有效的协方差。因此,只要涉及someMethod - 如果某个类型TU的子类型,则Foo[T]Foo[U]的子类型。太棒了,我们实现了协方差。但请注意,我说“只要关注someMethod”。 Foo在此方法中的类型是协变的,但在其他方法中,它可能是不变的或逆变的。

这种方差声明称为 use-site 方差,因为我们在其使用点声明了类型的方差(此处它用作someMethod的方法参数类型)。这是Java中唯一的一种方差声明。使用使用场地差异时,您需要注意 get-put原则(google it)。基本上这个原则说我们只能从协变类(我们不能放)得到东西,反之亦然,对于逆变类(我们可以放但不能得到)。在我们的例子中,我们可以这样证明:

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")

更一般地说,我们只能使用协变类型作为返回类型和逆变类型作为参数类型。

现在,使用站点声明很好,但在Scala中,利用声明站点方差(Java没有的东西)更为常见。这意味着我们将在定义Foo时描述Foo的泛型类型的方差。我们只想说class Foo[+T]。现在,在编写与Foo一起使用的方法时,我们不需要使用边界;我们宣称Foo在其类型,每个用例和每个场景中都是永久协变的。

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