14

关于使用下限创建新的不可变类型的方法,我试图了解协方差

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

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

我错过了什么?

谢谢布莱尔

4

4 回答 4

14

您的课程提供 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 类型的对象。
    • 结果:不可变数组[S]
    • 如果允许 S 是 T 的正确子类型(除了 T 本身),那么新数组就不能包含 T 类型的原始元素!
    • 如果 S 是 T 类型或 T 的超类型,那么一切都很好 - 可以包含原始元素,加上新元素!

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

    想一想:如果我从一组橙子开始,然后添加一个柠檬,我最终会得到一组柑橘类水果(不是橙子、脐橙,也不是柠檬)。


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

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

在追加的情况下:以T开头,输出数据结构=与T相反的变量,类型S使用T作为下限,所以输入参数=与S的协变。这意味着如果T1是T2的子类型,那么ImmutableArray[T1] 是 ImmutableArray[T2] 的子类型,它可以在任何预期后者的地方被替换,所有方法都遵循 Liskov 的替换原则。

于 2013-10-24T05:26:57.910 回答
12

第一个问题:

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

那么它可以使用。S >: T只是意味着如果你传入一个S等于T或它的parant的类型,那么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)操作仍然为您提供 Canine 类型的 ImmutableArray。而且,如果您以某种方式认为它是完全有意义的,因为将 Dog 添加到 Canine Array 仍将保留数组 Canine。但是将 Animal 添加到 Canine Array 使其成为 Animal,因为它不再是完美的犬(可以是臼齿或其他东西)。

这是一个完美的例子,说明为什么通常知道逆变类型声明使其非常适合写入(您的情况)和读取的协变。

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

于 2013-10-23T17:59:47.020 回答
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 >: Bar, List[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]

wherevalue.bar将在 a 上调​​用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)
  }
}

所以类型系统仍然是健全的。

于 2013-10-23T17:45:26.487 回答
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))

尝试做同样的事情使 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)
于 2013-10-23T11:33:00.997 回答