Scala:类型参数和继承

Scala: Type parameters and inheritance

我看到了一些我不理解的东西。我有一个(例如)车辆的层次结构,一个对应的VehicalReaders层次结构,以及一个带有apply方法的VehicleReader对象:

1
2
3
4
5
6
7
8
9
abstract class VehicleReader[T <: Vehicle] {
...

object VehicleReader {
  def apply[T <: Vehicle](vehicleId: Int): VehicleReader[T] = apply(vehicleType(vehicleId))

  def apply[T <: Vehicle](vehicleType VehicleType): VehicleReader[T] = vehicleType match {
    case VehicleType.Car => new CarReader().asInstanceOf[VehicleReader[T]]
    ...

请注意,当您拥有多个应用方法时,必须指定返回类型。无需指定返回类型时,我没有任何问题。

强制类型转换(.asInstanceOf [VehicleReader [T]])是出现问题的原因-没有它的结果是编译错误,例如:

1
2
3
4
5
type mismatch;
 found   : CarReader
 required: VehicleReader[T]
    case VehicleType.Car => new CarReader()
                                  ^

相关问题:

  • 为什么编译器不能将CarReader视为VehicleReader [T]?
  • 在这种情况下使用的正确的类型参数和返回类型是什么?

我怀疑这里的根本原因是VehicleReader的类型参数是不变的,但使其协变不会改变结果。

我觉得这应该很简单(即在Java中使用通配符很容易实现)。


该问题的起因很简单,实际上与差异无关。考虑甚至更简单的示例:

1
2
3
object Example {
  def gimmeAListOf[T]: List[T] = List[Int](10)
}

此代码段捕获了代码的主要思想。但这是不正确的:

1
val list = Example.gimmeAListOf[String]

list的类型是什么?我们要求专门针对List[String]gimmeAListOf方法,但是,它总是返回List[Int](10)。显然,这是一个错误。

因此,换句话说,当方法具有类似method[T]: Example[T]的签名时,它实际上声明:"对于任何类型的T,您给我,我将返回Example[T] \\的实例"。此类类型有时称为"通用量化",或简称为"通用"。

但是,情况并非如此:您的函数根据其参数的值(例如,VehicleReader[T])返回特定的VehicleReader[T]实例。 CarReader(我认为是VehicleReader[Car]的扩展)。假设我写了类似的东西:

1
2
3
4
class House extends Vehicle

val reader = VehicleReader[House](VehicleType.Car)
val house: House = reader.read()  // Assuming there is a method VehicleReader[T].read(): T

编译器会很高兴地对此进行编译,但是当执行此代码时,我会得到ClassCastException

对于这种情况,有两个可能的修复程序。首先,可以使用存在(或存在量化)类型,这可以作为更强大的Java通配符版本使用:

1
def apply(vehicleType: VehicleType): VehicleReader[_] = ...

此功能的签名基本上读为""您给我一个VehicleType,并且我为您返回了VehicleReader的某种类型的实例"。您将有一个VehicleReader[_]类型的对象;您不能说任何有关其参数类型的信息,除非该类型存在,这就是为什么这种类型称为存在的原因。

1
def apply(vehicleType: VehicleType): VehicleReader[T] forSome {type T} = ...

这是一个等效的定义,从中可能更清楚为什么这些类型具有此类属性-T类型隐藏在参数内部,因此您对此一无所知,但是它确实存在。 >

但是由于存在属性的这一特性,您实际上无法获得有关实型参数的任何信息。除非通过直接使用asInstanceOf强制转换,否则无法从VehicleReader[_]中获得VehicleReader[Car],这很危险,除非您将TypeTag / ClassTag作为类型参数存储在VehicleReader中,然后在投。有时(实际上,大多数情况下)很笨拙。

这是第二种方法。代码中的VehicleTypeVehicleReader[T]之间存在明显的对应关系,即当您具有VehicleType的特定实例时,您肯定知道VehicleReader[T]签名中的具体T

1
2
VehicleType.Car -> CarReader (<: VehicleReader[Car])
VehicleType.Truck -> TruckReader (<: VehicleReader[Truck])

以此类推。

因此,将类型参数添加到VehicleType是有意义的。在这种情况下,您的方法将类似于

1
def apply[T <: Vehicle](vehicleType: VehicleType[T]): VehicleReader[T] = ...

现在直接连接输入类型和输出类型,并且该方法的用户将被迫为其想要的T提供正确的VehicleType[T]实例。这排除了我前面提到的运行时错误。

尽管如此,您仍然需要asInstanceOf强制转换。为了避免完全强制转换,您必须将VehicleReader实例化代码(例如,您的new CarReader())移动到VehicleType,因为唯一知道VehicleType[T]类型参数的实际值的地方就是构造这种类型的实例的位置:

1
2
3
4
5
6
7
8
9
10
sealed trait VehicleType[T <: Vehicle] {
  def newReader: VehicleReader[T]
}

object VehicleType {
  case object Car extends VehicleType[Car] {
    def newReader = new CarReader
  }
  // ... and so on
}

然后VehicleReader工厂方法将看起来非常干净并且是完全类型安全的:

1
2
3
object VehicleReader {
  def apply[T <: Vehicle](vehicleType: VehicleType[T]) = vehicleType.newReader
}