Scala协方差和下限类型解释

时间:2013-10-17 13:41:02

标签: scala generics type-bounds

我试图通过使用下限创建新的不可变类型的方法来了解协方差

class ImmutableArray[+T](item: T, existing: List[T] = Nil) {  
  private val items = item :: existing

  def append[S >: T](value: S) = new ImmutableArray[S](value, items)
}

我知道类型参数T不能在append方法中使用,因为它违反了规则但如果我说Customer类和子类Student我仍然可以制作类型U Student

我可以看到这有效,但为什么这不违反规则?我可以理解,如果我有一个Student的列表,然后添加了Customer我只能返回Customer的列表,原因是不允许分配CustomerStudent,因为它是父类型。但为什么我可以使用Student

我错过了什么?

谢谢Blair

4 个答案:

答案 0 :(得分:13)

您的课程提供2项涉及T的操作:

  1. 构建

    nextImmutableArray = new ImmutableArray(nextT, priorImmutableArray)
    

    由于此操作,类型参数T必须是共变量:+ T。这允许您使用参数设置构造为类型的对象(T或T的子类型)。

    想一想:通过加入 Valencia Orange构建一个Oranges数组是有效的。

  2. 组合

    nextImmutableArray.append(newItemTorAncestor)
    

    此方法不会附加到您的数据结构中。它需要两个独立的元素(您的数组实例 this 和一个额外的对象),它在新构造的数组中组合。您可以考虑将方法名称更改为 appendIntoCopy 。更好的是,您可以使用名称 + 。但为了最正确并与Scala惯例一致,最好的名称是:+

    当你问一个特定问题时,为什么我会对一个'随机'方法名称感到困惑?

    因为该方法的确切性质决定了返回的数据结构是否是(a)与T(b)共变体的非变体与T(c)反变体与T.

    • 开头:ImmutableArray [T] - 包含类型T(或子类型)
    • 与:S型对象结合使用。
    • 结果:ImmutableArray [S]
    • 如果允许S是T的正确子类型(超出T本身),则新数组不能包含T类型的原始元素!
    • 如果S是T型或T的超类型,那么一切都很好 - 可以包含原始元素,加上新元素!

    组合数组和元素时,新创建的数据结构必须具有一个类型参数,该参数是共同祖先类型的超类型。否则它不能包含原始元素。通常,当执行“a:+ b”时,其中A是数组[A],b是类型B,结果数据结构是数组[Some_SuperType_Of_Both_A_and_B]。

    想想:如果我从一系列橘子开始,然后添加柠檬,我最终会得到一系列柑橘类水果(不是橙子,脐橙,也不是柠檬)。< /强>


  3. 方法规则(输入严格,适应输出):

    • a)输入参数提供插入元素(变异): Co-Variant
    • a)输出参数从数据结构中返回一个元素: Contra-Variant
    • c)输出参数,合并后返回数据结构: Contra-Variant
    • c)使用类型作为下限:“翻转”方差(“Contra-variant to T”=“Co-Variant to S,其具有下限T”)

    在追加的情况下:从T开始,输出数据结构=对比变量为T,类型S使用T作为下限,因此输入参数=与S的共变量。这意味着如果T1是子类型然后,ImmutableArray [T1]是ImmutableArray [T2]的子类型,并且只要符合后者,它就可以替换,所有方法都遵循Liskov的替换原则。

答案 1 :(得分:10)

第一个问题:

  

我知道类型参数T不能在append方法中使用,因为它违反了规则

好吧,它可以使用。 S >: T只是意味着如果您传入的S类型等于T或其参与者,则会使用S。如果您将子级别的类型传递给T,则会使用T

scala> class Animal
defined class Animal

scala> class Canine extends Animal
defined class Canine

scala> class Dog extends Canine
defined class Dog

scala> new ImmutableArray[Canine](new Canine)
res6: ImmutableArray[Canine] = ImmutableArray@a47775

scala> res6.append(new Animal)
res7: ImmutableArray[Animal] = ImmutableArray@1ba06f1

scala> res6.append(new Canine)
res8: ImmutableArray[Canine] = ImmutableArray@17e4626

scala> res6.append(new Dog)
res9: ImmutableArray[Canine] = ImmutableArray@a732f0

上面做res6.append(new Dog)仍然给你ImminedArray类型的犬。如果你想某种方式它是完全合理的,因为添加Dog to Canine Array仍将保留阵列Canine。但是将动物添加到犬阵列会使它成为动物,因为它不再是完美的犬(可以是磨牙或其他东西)。

这是一个很好的例子,说明为什么通常会知道反变体类型声明使其适合写入(您的情况)和读取的协方差。

在您的示例中,我认为混淆可能是因为您正在将S >: TS super T(来自Java世界)进行比较。使用S super T,您必须具有超级类T的参数类型,并且它不允许您将子类型的参数传递给T。在scala中,编译器会处理这个问题(感谢类型推断)。

答案 2 :(得分:4)

考虑以下层次结构:

class Foo
class Bar extends Foo { def bar = () }
class Baz extends Bar { def baz = () }

和你的类似:

class Cov[+T](val item: T, val existing: List[T] = Nil) {
  def append[S >: T](value: S) = new Cov[S](value, item :: existing)
}

然后我们可以为每个Foo子类型构建三个实例:

val cFoo = new Cov(new Foo)
val cBar = new Cov(new Bar)
val cBaz = new Cov(new Baz)

需要bar个元素的测试函数:

def test(c: Cov[Bar]) = c.item.bar

它拥有:

test(cFoo) // not possible (otherwise `bar` would produce a problem)
test(cBaz) // ok, since T covariant, Baz <: Bar --> Cov[Baz] <: Cov[Bar]; Baz has bar

现在append方法,回到上限:

val cFoo2 = cBar.append(new Foo)

这没关系,因为Foo >: BarList[Foo] >: List[Bar]Cov[Foo] >: Cov[Bar]

现在,您的bar访问权限正确无误:

cFoo2.item.bar // bar is not a member of Foo

要理解为什么需要上限,想象以下是可能的

class Cov[+T](val item: T, val existing: List[T] = Nil) {
  def append(value: T) = new Cov[T](value, item :: existing)
}

class BarCov extends Cov[Bar](new Bar) {
  override def append(value: Bar) = {
    value.bar // !
    super.append(value)
  }
}

然后你可以写

def test2[T](cov: Cov[T], elem: T): Cov[T] = cov.append(elem)

以下非法行为将被允许:

test2[Foo](new BarCov, new Foo) // BarCov <: Cov[Foo]

value.bar将调用Foo。使用(正确)上限,您将无法像假设的最后一个例子那样实现append

class BarCov extends Cov[Bar](new Bar) {
  override def append[S >: Bar](value: S) = {
    value.bar // error: value bar is not a member of type parameter S
    super.append(value)
  }
}

所以类型系统仍然健全。

答案 3 :(得分:2)

它的工作原理是因为append方法返回的是比原始类更广泛的类。 我们进行一些实验。

    scala> case class myIntClass(a:Int)
    defined class myIntClass

    scala> case class myIntPlusClass(a:Int, b:Int)
    defined class myIntPlusClass

   scala> class ImmutableArray[+T](item: T, existing: List[T] = Nil){
         | 
         | private val items = item :: existing
         | 
         | def append[S >: T](value: S) = new ImmutableArray[S](value,items)
         | def getItems = items
         | }
    defined class ImmutableArray

    scala> val ia = new ImmutableArray[myIntClass](myIntClass(3))
    ia: ImmutableArray[myIntClass] = ImmutableArray@5aa91edb

    scala> ia.getItems
    res15: List[myIntClass] = List(myIntClass(3))

    scala> ia.append(myIntPlusClass(3,5))
    res16: ImmutableArray[Product with Serializable] = ImmutableArray@4a35a157

    scala> res16.getItems
    res17: List[Product with Serializable] = List(myIntPlusClass(3,5), myIntClass(3))

    scala> res16
    res18: ImmutableArray[Product with Serializable] = ImmutableArray@4a35a157

所以你可以在这里添加一个派生类,但它只能起作用,因为结果数组的基类型被降级为最小公分母(在本例中为Serializable)。

如果我们尝试在结果数组上强制派生类型,它将无法工作:

scala> ia.append[myIntPlusClass](myIntPlusClass(3,5))
<console>:23: error: type arguments [myIntPlusClass] do not conform to method append's type parameter bounds [S >: myIntClass]
              ia.append[myIntPlusClass](myIntPlusClass(3,5))

尝试做同样的make append返回一个派生类型数组将不起作用,因为T不是S的子类:

scala> class ImmutableArray[+T](item: T, existing: List[T] = Nil){
     |           
     |          private val items = item :: existing
     |          
     |          def append[S <: T](value: S) = new ImmutableArray[S](value,items)
     |          def getItems = items
     |          }
<console>:21: error: type mismatch;
 found   : List[T]
 required: List[S]
                def append[S <: T](value: S) = new ImmutableArray[S](value,items)