【问题标题】:Trouble with type variance类型差异的问题
【发布时间】:2017-08-17 17:28:02
【问题描述】:

所以,假设我有一个带有逆变类型参数的类:

  trait Storage[-T] {
    def list(path: String): Seq[String]
    def getObject[R <: T](path: String): Future[R]
  }

类型参数的想法是将实现限制在它可以返回的类型的上限。所以,Storage[Any] 可以读取任何内容,而Storage[avro.SpecificRecord] 可以读取 avro 记录,但不能读取其他类:

def storage: Storage[avro.SpecificRecord] 
storage.getObject[MyAvroDoc]("foo") // works
storage.getObject[String]("bar") // fails

现在,我有一个实用程序类,可用于遍历给定位置的对象:

class StorageIterator[+T](
  val storage: Storage[_ >: T], 
  location: String
)(filter: String => Boolean) extends AbstractIterator[Future[T]] {
  val it = storage.list(location).filter(filter)
  def hasNext = it.hasNext
  def next = storage.getObject[T](it.next)
}

这可行,但有时我需要从下游迭代器访问底层storage,以从辅助位置读取另一种类型的对象:

def storage: Storage[avro.SpecificRecord]
val iter = new StorageIterator[MyAvroDoc]("foo")
iter.storage.getObject[AuxAvroDoc](aux)

这当然行不通,因为storage类型参数是通配符,没有证据证明可以用来读取AuxAvroDoc

我尝试这样修复它:

class StorageIterator2[P, +T <: P](storage: Storage[P])
  extends StorageIterator[T](storage)

这可行,但现在我必须在创建它时指定两个类型参数,这很糟糕:( 我试图通过向Storage 本身添加一个方法来解决它:

trait Storage[-T] {
  ... 
  def iterate[R <: T](path: String) = 
    new StorageIterator2[T, R](this, path)
}

但这不会编译,因为它将T 置于一个不变的位置:( 如果我使P 逆变,那么StorageIterator2[-P, +T &lt;: P] 失败,因为它认为P occurs in covariant position in type P of value T

最后一个错误我不明白。为什么 P 在这里不能逆变?如果这个位置真的是协变的(为什么会这样?)那么为什么它允许我在那里指定一个不变的参数呢?

最后,有人知道我该如何解决这个问题吗? 基本上,这个想法是能够

  1. storage.iterate[MyAvroDoc]而不必再次给它上边界,并且
  2. 执行iterator.storage.getObject[AnotherAvroDoc] 无需强制转换存储以证明它可以读取此类对象。

感谢任何想法。

【问题讨论】:

    标签: scala types variance


    【解决方案1】:

    StorageIterator2[-P, +T &lt;: P] 失败,因为它是无意义的。如果你有一个StorageIterator2[Foo, Bar]Bar &lt;: Foo,那么因为它在第一个参数中是逆变的,所以它也是一个StorageIterator[Nothing, Bar],但是Nothing没有子类型,所以逻辑上不可能Bar &lt;: Nothing,然而这就是拥有StorageIterator2 的必要条件。因此,StorageIterator2 不能存在。

    根本问题是Storage 不应该是逆变的。想一想Storage[T] ,就它提供给用户的合同而言。 Storage[T] 是您提供路径的对象,并将输出 Ts。 Storage 是协变的非常有意义:例如,知道如何输出 Strings 的东西也在输出 Anys,所以 Storage[String] &lt;: Storage[Any] 是合乎逻辑的。你说它应该是相反的,Storage[T] 应该知道如何输出T 的任何子类型,但那将如何工作?如果有人在事后向T 添加子类型怎么办? T 甚至可以是 final 并且仍然存在这个问题,因为单例类型。这是不必要的复杂,并反映在您的问题中。也就是Storage应该是

    trait Storage[+T] {
      def list(path: String]: Seq[String]
      def get(path: String): T
    }
    

    这不会让您看到您在问题中给出的示例错误:

    val x: Storage[avro.SpecificRecord] = ???
    x.get(???): avro.SpecificRecord // ok
    x.get(???): String // nope
    (x: Storage[String]).get(???) // nope
    

    现在,你的问题是你不能做像storage.getObject[T] 这样的事情并且让演员表是隐式的。你可以改为match:

    storage.getObject(path) match {
      case value: CorrectType => ...
      case _ => // Oops, something else. Error?
    }
    

    一个普通的asInstanceOf(不受欢迎),或者您可以向Storage 添加一个辅助方法,就像您之前使用的那样:

    def getCast[U <: T](path: String)(implicit tag: ClassTag[U]): Option[U] = tag.unapply(get(path))
    

    【讨论】:

    • "一个 Storage[T] 是一个你提供路径的对象,并且会输出 Ts。"这不是真的。 T 不是 Storage 返回的对象的类型,而是它可能返回的 all 对象的常见超类型(当然,它们在某种程度上都是 T,但这是技术性问题,不是概念特征)。它需要是逆变的,因为Storage[Any] 可以像Stroage[Foo] 一样使用。
    • “你说它应该反过来,一个 Storage[T] 应该知道如何输出 T 的任何子类型,但它是如何工作的?”我在问题中展示了一个例子。例如,如果 T 是 avro 结构的基类,则 Storage 可以读取任何 avro 文档。可能是 ThriftStruct 来读取 thrift,或者 Message 用于 protobuf,Product 用于 csv 等。为应用程序中的每个 avro doc 设置单独的 Storage 类是没有意义的,那就是如果它是协变的会发生什么。
    • Casting 和 match'ing 可能是一种解决方法,但这是一种 java 做事的方式。避免这种黑客攻击正是首先具有类型差异的目的。这个想法是能够在编译时声明:“这个对象可以读取任何 avro doc,而不是其他任何东西”,而不是在运行时产生错误,当它遇到不应该的东西时。
    • 还有一点需要注意:StorageIterator2[-P, T &lt;: P] 失败,但 StorageIterator[T, -P &gt;: T] 实际上可以工作,即使它实际上是同一件事 :)
    • 你不能实现一个Storage[-T],这样它就有一个方法getObject[R &lt;: T](...): Future[R]。您将如何创建这样的方法?它需要为T 的任何子类型R 输出R,但它不知道R 是什么。它需要读取一个值,而它唯一可能知道的是“我刚刚读取的是一个T”。它无法判断它是R,也无法测试它,因为R 是未知数。 有道理,您不能创建Storage[Sup] 并让它成为Storage[_ &lt;: Sup]。我所要做的就是创建一个Sup 的新子类,它不知道它会中断。 >
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-08-27
    • 1970-01-01
    • 2011-10-18
    • 1970-01-01
    • 1970-01-01
    • 2015-12-02
    • 2015-12-12
    相关资源
    最近更新 更多