我如何得到类型擦除在Scala?或者,为什么我不能得到我的集合的类型参数?

在Scala中,如果你实例化一个List[Int],你可以验证你的实例是一个List,你可以验证它的任何单个元素是否是Int,但不能验证它是否是List[Int],这很容易验证:

scala> List(1,2,3) match {
| case l : List[String] => println("A list of strings?!")
| case _ => println("Ok")
| }
warning: there were unchecked warnings; re-run with -unchecked for details
A list of strings?!

unchecked选项直接将责任归咎于类型擦除:

scala>  List(1,2,3) match {
|  case l : List[String] => println("A list of strings?!")
|  case _ => println("Ok")
|  }
<console>:6: warning: non variable type-argument String in type pattern is unchecked since it is eliminated by erasure
case l : List[String] => println("A list of strings?!")
^
A list of strings?!

为什么会这样,我该如何解决呢?

82683 次浏览

这个答案使用Manifest-API,该api在Scala 2.10中已弃用。请参阅下面的答案,了解更多当前的解决方案。

Scala是用类型擦除来定义的,因为与Java不同,Java虚拟机(JVM)没有泛型。这意味着,在运行时,只有类存在,而不存在它的类型参数。在这个例子中,JVM知道它正在处理scala.collection.immutable.List,但不知道这个列表是用Int参数化的。

幸运的是,Scala中有一个特性可以解决这个问题。它是清单。Manifest是一个类,其实例是表示类型的对象。因为这些实例是对象,所以可以传递它们、存储它们,通常还可以对它们调用方法。在隐式参数的支持下,它成为一个非常强大的工具。举个例子:

object Registry {
import scala.reflect.Manifest
  

private var map= Map.empty[Any,(Manifest[_], Any)]
  

def register[T](name: Any, item: T)(implicit m: Manifest[T]) {
map = map.updated(name, m -> item)
}
  

def get[T](key:Any)(implicit m : Manifest[T]): Option[T] = {
map get key flatMap {
case (om, s) => if (om <:< m) Some(s.asInstanceOf[T]) else None
}
}
}


scala> Registry.register("a", List(1,2,3))


scala> Registry.get[List[Int]]("a")
res6: Option[List[Int]] = Some(List(1, 2, 3))


scala> Registry.get[List[String]]("a")
res7: Option[List[String]] = None

当存储一个元素时,我们存储一个“manifest”;还有它。Manifest是一个类,其实例代表Scala类型。这些对象比JVM拥有更多的信息,这使我们能够测试完整的参数化类型。

但是请注意,Manifest仍然是一个不断发展的特性。作为其局限性的一个例子,它目前对方差一无所知,并假设一切都是协变的。我希望在目前正在开发的Scala反射库完成之后,它会变得更加稳定和可靠。

我找到了一个稍微好一点的解决方法来克服这门很棒的语言的局限性。

在Scala中,数组不会出现类型擦除的问题。我认为用一个例子更容易说明这一点。

假设我们有一个(Int, String)的列表,然后下面给出一个类型擦除警告

x match {
case l:List[(Int, String)] =>
...
}

要解决这个问题,首先创建一个case类:

case class IntString(i:Int, s:String)

然后在模式匹配中执行如下操作:

x match {
case a:Array[IntString] =>
...
}

这似乎很有效。

这将需要对代码进行一些小更改,以使用数组而不是列表,但这应该不是一个大问题。

注意,使用case a:Array[(Int, String)]仍然会给出类型擦除警告,因此有必要使用一个新的容器类(在本例中为IntString)。

在Scala中有一种方法可以克服类型擦除问题。在克服匹配1中的类型擦除克服匹配2中的类型擦除(方差)中解释了如何编写一些helper来包装类型,包括方差,以便进行匹配。

我提出了一个相对简单的解决方案,在有限的使用情况下就足够了,本质上是在可以在match语句中使用的包装器类中包装会遭受类型擦除问题的参数化类型。

case class StringListHolder(list:List[String])


StringListHolder(List("str1","str2")) match {
case holder: StringListHolder => holder.list foreach println
}

这具有预期的输出,并将case类的内容限制为所需的类型String Lists。

更多细节:http://www.scalafied.com/?p=60

我想知道这是否是一个合适的变通方案:

scala> List(1,2,3) match {
|    case List(_: String, _*) => println("A list of strings?!")
|    case _ => println("Ok")
| }

它不匹配“空列表”的情况,但它给出了一个编译错误,而不是警告!

error: type mismatch;
found:     String
requirerd: Int

另一方面,这似乎工作....

scala> List(1,2,3) match {
|    case List(_: Int, _*) => println("A list of ints")
|    case _ => println("Ok")
| }

这样不是更好吗,还是我没抓住重点?

你可以使用不成形的中的Typeable类型类来获得你想要的结果,

示例REPL会话,

scala> import shapeless.syntax.typeable._
import shapeless.syntax.typeable._


scala> val l1 : Any = List(1,2,3)
l1: Any = List(1, 2, 3)


scala> l1.cast[List[String]]
res0: Option[List[String]] = None


scala> l1.cast[List[Int]]
res1: Option[List[Int]] = Some(List(1, 2, 3))

给定范围内可用的Typeable实例,cast操作将尽可能精确地擦除。

由于Java不知道实际的元素类型,我发现使用List[_]最有用。然后警告消失了,代码描述了现实——它是一个未知事物的列表。

你可以使用TypeTags (Daniel已经提到了,但我只是明确地拼写出来):

import scala.reflect.runtime.universe._
def matchList[A: TypeTag](list: List[A]) = list match {
case strlist: List[String @unchecked] if typeOf[A] =:= typeOf[String] => println("A list of strings!")
case intlist: List[Int @unchecked] if typeOf[A] =:= typeOf[Int] => println("A list of ints!")
}

你也可以使用ClassTags(这使你不必依赖scala-reflect):

import scala.reflect.{ClassTag, classTag}
def matchList2[A : ClassTag](list: List[A]) = list match {
case strlist: List[String @unchecked] if classTag[A] == classTag[String] => println("A List of strings!")
case intlist: List[Int @unchecked] if classTag[A] == classTag[Int] => println("A list of ints!")
}

只要你不期望类型参数A本身是泛型类型,就可以使用ClassTags。

不幸的是,它有点啰嗦,您需要@unchecked注释来抑制编译器警告。TypeTag可以在将来由编译器自动合并到模式匹配中:https://issues.scala-lang.org/browse/SI-6517

不是解决方案,而是一种生活的方式,而不是把它完全掩盖在地毯下: 添加@unchecked注释。看这里- http://www.scala-lang.org/api/current/index.html#scala.unchecked

使用模式匹配保护

    list match  {
case x:List if x.isInstanceOf(List[String]) => do sth
case x:List if x.isInstanceOf(List[Int]) => do sth else
}

我想添加一个答案,将问题概括为:如何在运行时获得我的列表类型的String表示

import scala.reflect.runtime.universe._


def whatListAmI[A : TypeTag](list : List[A]) = {
if (typeTag[A] == typeTag[java.lang.String]) // note that typeTag[String] does not match due to type alias being a different type
println("its a String")
else if (typeTag[A] == typeTag[Int])
println("its a Int")


s"A List of ${typeTag[A].tpe.toString}"
}


val listInt = List(1,2,3)
val listString = List("a", "b", "c")


println(whatListAmI(listInt))
println(whatListAmI(listString))