【问题标题】:scala contravariant position on methodscala在方法上的逆变位置
【发布时间】:2014-09-29 21:36:18
【问题描述】:

我正在阅读http://oldfashionedsoftware.com/2008/08/26/variance-basics-in-java-and-scala/

正在查看代码

class CoVar[+T](param1: T) {
  def method1(param2: T) = { }
  def method2: T = { param1 }
  def method3: List[T] = { List[T](param1) }
  def method4[U >: T]: List[U] = { List[U](param1) }
  val val1: T = method2
  val val2: Any = param1
  var var1: T = method2
  var var2: Any = param1
}

如果我有一个

val covar1 = new CoVar(new Car)
val covar2: CoVar[Vehicle] = covar1 //completely legal with covariant

现在,让我们来看看方法

  • method1 - 我不明白为什么它不能编译,这是我的主要问题
  • method2 - param1 是一辆汽车,method2 返回一个 Vehicle 很好,因为 Car 是一辆 Vehicle
  • method3 - 因为 List[Vehicle] 返回并且 Car 是 Vehicle 这很好
  • var1 - 我相信同样的问题,而且差别不大

我认为使用 method1(param: Vehicle) 可以,因为我可以通过一辆新车就好了,或者一辆新车就好了

但是原始的 CoVar 类没有编译,因为它说 method1 是逆变位置。我认为逆变意味着我可以传入一个

现在,我们用 ContraVar 和方法 1 遍历它

class ContraVar[-T](param1: T) {
    def method1(param2: T) = { }
    val val2: Any = param1
    var var2: Any = param1
}

val temp1 = new ContraVar(new Car)
val temp2: ContraVar[Ford] = temp1

temp2.method1(new Ford)
temp2.method1(new FordMustang)
temp2.method1(new Car) //fails to compile(good)

效果很好。有人可以解释一下为什么方法1会在CoVar上中断吗?也许我走上了完全错误的道路,让方法 1 编译得很好会出现什么问题?

谢谢, 院长

【问题讨论】:

    标签: scala scala-2.10


    【解决方案1】:

    您的问题归结为为什么以下内容是非法的

    trait Tool[+A] {
      def treat(c: A): Unit
    }
    

    想象一下它会编译......让我们从外部看一个用例:

    def apply[A](tool: Tool[A], car: A): Unit = tool.treat(car)
    

    假设A 有两种可能的类型:

    trait Car
    trait Mustang extends Car { def awe(): Unit }
    

    协方差意味着Tool[Mustang] <: Tool[Car]。因此,每当要求 Tool[Car] 时,您都可以使用 Tool[Mustang]。现在想象一个Tool[Mustang]

    val tm = new Tool[Mustang] { def treat(m: Mustang) = m.awe() }
    

    现在你可以打电话了:

    apply[Car](tm, new Car {})
    

    这意味着tm 可以访问通用汽车中不存在的方法awe。显然这不是一个健全的类型关系。 因此,无论何时在参数位置使用类型,它都必须是不变的或逆变的。

    【讨论】:

    • hmmmm,那么如果他们决定只允许一个子类化 Tool[+A] 并且不允许将 Tool[Mustang] 子类化怎么办。 IE。是否还有另一种情况,方法 1 出现故障,或者一种语言选择会阻止这个问题?
    • 我不明白你的问题。您可以创建一个子类SubTool[A] extends Tool[A],您可以在其中决定A 变得不变。然后您可以允许A 出现在SubTool 的方法参数中。
    • 哦,没关系,我明白了......我认为添加“trait SubTool extends Tool[Mustang] { def Treat(m: Mustang) = m.awe 可能会更好() } 好的,当我这样想时,这对我来说完全有意义,因为它是典型的 Java 模式。另一种方式让我感到困惑,直到我在这个问题上睡了一会儿,并在以下方面对其进行了改革我的 Java 知识。
    【解决方案2】:

    让我们给你的 method1 一个主体:

    class CoVar[+T] {
        var listOfT: List[T] = Nil
    
        // method1 prepends the given element to listOfT
        def method1(param2: T) = { listOfT = param2 :: ListOfT }
    }
    

    现在我们实际上对传递给 method1 的参数做了一些事情,如果允许的话,很容易看出会发生什么错误。

    // Lets construct one of these that holds Ints. and add something to it
    val covarInt = new CoVar[Int]
    covarInt.method1(1)
    
    // lets assign to a more general value, this is no problem because of the covariance
    val covarAny: CoVar[Any] = covarInt
    
    // now lets do a bad thing:
    covarAny.method1("this is a string not an Int")
    

    最后一行显示了允许的坏事。由于covarAnyCoVar[Any] 类型,这意味着在CoVar 类中,type T = Any,所以预期作为method1 输入的类型是Any,所以将String 传递给这个函数应该是允许,因为 StringAny。然而,方法体随后会尝试将我们传递的String 添加到不应被允许的Ints 列表中。

    【讨论】:

    • 自从 val myNewList: Any param2 :: ListOfT 是有效的,这似乎还不错,对吧?我记得当您添加到列表时,类型推断使列表成为最高类型,因此看起来很好且有效。
    • 或者在 scala repl, scala> val myList = 56 :: "hi there" :: Nil myList: List[Any] = List(56, hi there) 是有效的。
    • 是的,但 listOfT 不是 List[Any] 类型,而是 List[T] 类型。考虑一下:def head: T = listOfT.head
    • 一旦您将其更改为 covarAny: CoVar[Any],那么 listOfT: List[T] 就是 listOfT[Any] 并且 method1 是 method1(param2: Any)....或至少我就是这么理解的。我认为另一个答案展示了一个更直接的突破。
    • 是的,但不是从covarInt的角度来看,调用covarInt.head会发生什么?一个 CoVar[Int] 返回一个字符串?
    【解决方案3】:

    我有一个新的答案,我认为这对为什么 params 是逆变的而不是协变的有很大帮助。这个例子也可以在没有泛型的情况下完成。

    让我们改用函数对象并定义一个 Animal、Bird 和 Duck 类

    class Animal {
       def makeSound() = "animalsound"
       def walk()
    }
    class Bird extends Animal {
       def makeSound() = "tweeeeet"
       def fly()
    }
    class Duck extends Bird {
       def makeSound() = "quack"
       def paddle()
    }
    

    现在,让我们尝试定义一个协变函数

    val doSomething: (Bird => String) = { d:Duck => d.paddle() }
    

    自然,当传入 Bird 时,我们不能将其强制转换为 Duck,因为它可能不是 Duck,而且它当然不能在 Bird 类中没有 paddle 方法的情况下划桨。

    现在,让我们尝试定义一个逆变函数

    val doSomething: (Bird => String) = { a:Animal => a.walk() }
    

    现在可以编译并运行,因为鸟是动物,所以它会有一个 walk 方法。这对我来说真的更清楚了,自然方法非常像函数,或者你总是可以将方法转换为函数,在这种情况下,它需要与参数逆变。

    现在,我们还有其他方法可以在 T > 中做点什么:Bird 可能吗?我想知道我是否要做一个 ( T >: Bird => String ) = .....

    好吧,我想我目前已经超出了我的知识范围,并将其作为待办事项留在我的学习清单中。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-07-27
      • 2013-04-10
      • 1970-01-01
      • 2015-02-09
      • 2015-04-11
      • 1970-01-01
      • 2012-03-26
      • 1970-01-01
      相关资源
      最近更新 更多