Scala类型边界和Java泛型互操作性

5

我正试图封装rxjava的timeout 方法,以便将其用于scala

类似于许多其他方法,我尝试了以下代码:

def timeout[U >: T](timeout: Duration, other: Observable[U]): Observable[U] = {
  val otherJava: rx.Observable[_ <: U] = other.asJavaObservable
  val thisJava:  rx.Observable[_ <: U] = this.asJavaObservable
  toScalaObservable[U](thisJava.timeout(timeout.length, timeout.unit, otherJava))
}

但是我遇到了以下错误:
Observable.scala:1631: error: overloaded method value timeout with alternatives:
($1: Long,x$2: java.util.concurrent.TimeUnit,x$3: rx.Scheduler)rx.Observable[_$85] <and>
($1: Long,x$2: java.util.concurrent.TimeUnit,x$3: rx.Observable[_ <: _$85])rx.Observable[_$85]
cannot be applied to (Long, scala.concurrent.duration.TimeUnit, rx.Observable[_$84])
  toScalaObservable[U](thisJava.timeout(timeout.length, timeout.unit, otherJava))

原始的Java方法:

public Observable<T> timeout(long timeout, TimeUnit timeUnit, Observable<? extends T> other) {
   return create(OperationTimeout.timeout(this, timeout, timeUnit, other));
}

我对Java和Scala (以及所有类型边界) 都不是很熟悉,但我理解:无论是otherJava还是thisJava都是类型为rx.Observable[U],那么它们为什么不能对齐呢?

2个回答

4
嗯,您正好碰到了在Scala中使用Java泛型时的差异问题。让我们一步一步来看。
让我们来看看您的实现:
// does not compile (with your original error)
def timeout[U >: T](timeout: Duration, other: Observable[U]): Observable[U] = {
  val otherJava: rx.Observable[_ <: U] = other.asJavaObservable
  val thisJava:  rx.Observable[_ <: U] = this.asJavaObservable
  toScalaObservable[U](thisJava.timeout(timeout.length, timeout.unit, otherJava))
}

为了理解为什么这样做行不通,让我们称声明中的未命名类型为A,即thisJavaA <: U意味着thisJavarx.Observable[A])。thisJava:rx.Observable[A]timeout方法需要一个rx.Observable[_ <: A]类型的参数,而你提供的是一个rx.Observable[_ <: U]类型的参数。编译器无法知道这两种类型之间的关系。它们可能根本没有关联! 另一方面,如果AU,那么thisJava将是一个rx.Observable[U],它的timeout方法将期望一个rx.Observable[_ <: U],这恰好是otherJava的类型。 让我们试试:
// still does not compile, sadly
def timeout[U >: T](timeout: Duration, other: Observable[U]): Observable[U] = {
  val otherJava: rx.Observable[_ <: U] = other.asJavaObservable
  val thisJava:  rx.Observable[U] = this.asJavaObservable // variance error
  toScalaObservable[U](thisJava.timeout(timeout.length, timeout.unit, otherJava))
}

在完美的世界中,上面那段代码可以工作。然而,Java的 rx.Observable 没有定义为协变,因为Java中没有定义网站方差注释。因此Scala认为它是不变的。
因此,对于Scala来说,rx.Observable[_ <: U] 不是 rx.Observable[U],遗憾的是 this.asJavaObservable 返回的是 rx.Observable[_ <: U]
但我们知道 [*] rx.Observable<T> 应该是协变的,因此我们可以盲目地进行转换:
// this compiles and *should* work
def timeout[U >: T](timeout: Duration, other: Observable[U]): Observable[U] = {
  val otherJava: rx.Observable[_ <: U] = other.asJavaObservable
  val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[U]]
  toScalaObservable[U](thisJava.timeout(timeout.length, timeout.unit, otherJava))
}

这个故事的寓意是,混合使用Scala的variance和Java的variance总会在这里或那里花费一些类型转换,必须仔细考虑。此外,如果让asJavaObservable返回rx.Observable[T]而不是_ <: T,可能会更加容易,但也许有很好的理由不这样做...[*]更像是“但我怀疑”

但是这样我就失去了将超类型Observable作为“other”传递的灵活性。我认为为了更灵活,该库应该允许不同类型的“U”。另外请注意,"<? extends T>"确实被翻译为"[_ <: T]",正如scala历史记录中所述。 - ClojureMostly
@Aralo 哦,你说得完全正确。实际上,如果没有一些 U 类型参数,它甚至无法编译,因为协变类型不能出现在逆变位置(在 other 中)。我删除了我的第一个观点并更新了代码。 - gourlaysama
很好的解释。我理解了你的观点,而且它确实有效(请参见我的拉取请求)。然而,这仍然令人困惑,因为我认为它们是相同类型。让我们用List来测试一下,它也是协变的:http://paste.ubuntu.com/6692064/ 这就是为什么我认为它们是完全相同的类型。仍然很困惑。 - ClojureMostly

3

[这段内容应该放在@gourlaysama的答案评论中,但我没有足够的声望来进行评论]

@Aralo 语句"MyType[_ <: T]MyType[T]是一样的"只有当编译器知道MyType是协变的时才成立。这适用于List,因为它被定义为List[+A],但对于rx.Observable不是这种情况,因为它是一个Java类型,所以它的类型参数不能有变异注释,因此编译器无法知道它是想要协变的。

@gourlaysama 让asJavaObservable返回rx.Observable[T]而不是_ <: T不是解决方案,因为类型rx.lang.scala.Observable[T]的含义是"T或其子类型的Observable",而这个描述恰好对应于类型rx.Observable[_ <: T](它与rx.Observable<? extends T>相同)。

我们在Scala中必须进行转换的原因是Java中timeout的签名是"错误的":严格来说,它使Java Observable不变,因为T出现在逆变位置上。正确的方法应该使用另一个类型参数U,其下界为T,就像在Scala中一样,但Java不支持下界,所以最佳解决方案是保持"错误"的解决方案。这个问题也发生在reduce(请参见此评论)、onErrorReturn和其他几个操作符中。

总的来说,所有这些应该具有下界类型参数(但没有)的运算符只能用于Observable<T>,而不能用于Observable<? extends T>(这对Java用户来说是相当令人不便的),因此,它们需要在Scala适配器中进行转换。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接