【问题标题】:Scala Covariance and Lower Type Bounds ExplanationScala 协方差和下限类型解释
【发布时间】:2013-10-17 13:41:02
【问题描述】:

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

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 列表然后添加了一个Customer 我只能返回一个Customers 列表,因为不允许将Customer 分配给Student父类型。但是为什么我可以使用Student

我错过了什么?

谢谢布莱尔

【问题讨论】:

  • 我不明白你的问题。你能举出一个完整的例子来说明什么不是你期望的工作方式吗?
  • 您展示了可以工作的代码,但向我们展示不工作的代码会更有用。
  • 我想知道为什么我需要 [S >: T]
  • @BlairDavidson 因为函数参数是逆变的并且你的 T 是协变的,这个 [S >: T] tric 提供了将 S 类型的 S 值传递给函数的能力,导致 S 不是协变的
  • “将类型设为 U Student” - 您的意思是在示例类中键入 S 吗?

标签: scala generics type-bounds


【解决方案1】:

您的课程提供了 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 是 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开头,Output Data Structure = Contra-Variant to T,Type S使用T作为下界,所以Input Parameter = Co-Variant with S。这意味着如果T1是一个子类型的 T2 则 ImmutableArray[T1] 是 ImmutableArray[T2] 的子类型,并且它可以在预期后者的任何地方替换,所有方法都遵循 Liskov 的替换原则。

【讨论】:

    【解决方案2】:

    第一个问题:

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

    好吧,它可以使用。 S >: T 仅仅意味着如果你传入一个等于T 或其parant 的S 类型,那么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,您必须拥有T 的超类的参数类型,并且它不允许您将子类型的参数传递给T。在 scala 中,编译器会处理这个问题(感谢类型推断)。

    【讨论】:

      【解决方案3】:

      考虑以下层次结构:

      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 &gt;: BarList[Foo] &gt;: List[Bar]Cov[Foo] &gt;: 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)
        }
      }
      

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

      【讨论】:

      • 很好的解释,恕我直言,这应该是公认的答案。
      【解决方案4】:

      之所以有效,是因为 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)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-03-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-08-24
        相关资源
        最近更新 更多