如何解决Scala上的类型擦除?或者,为什么我无法获取集合的类型参数?


370

在Scala上生活的一个可悲的事实是,如果实例化一个List [Int],则可以验证您的实例是一个List,并且可以验证它的任何单个元素是一个Int,而不是它是一个List [可以很容易地验证:

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?!

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


Scala 2.8 Beta 1 RC4刚刚对类型擦除的工作方式进行了一些更改。我不确定这是否会直接影响您的问题。
Scott Morrison

1
这只是什么类型擦除,已经改变了。它的缩写可以总结为“ 建议:擦除“带有A的对象”是“ A”而不是“对象”。 ”实际的说明要复杂得多。无论如何,这都是关于mixin的,这个问题是关于泛型的。
Daniel C. Sobral

感谢您的澄清-我是scala新人。我觉得现在是进入Scala的糟糕时机。早些时候,我可以从一个好的基础中学到2.8中的更改,而后,我将永远不必知道它们之间的区别!
Scott Morrison

1
这是关于TypeTags的一些相关问题。
pvorb

2
运行时scala 2.10.2,我改为看到此警告:<console>:9: warning: fruitless type test: a value of type List[Int] cannot also be a List[String] (but still might match its erasure) case list: List[String] => println("a list of strings?") ^我发现您的问题和答案非常有帮助,但是我不确定此更新的警告是否对读者有用。
凯文·梅瑞迪斯

Answers:


243

此答案使用Manifest-API,从Scala 2.10开始不推荐使用。请参阅下面的答案以获取更多最新解决方案。

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

幸运的是,Scala中有一项功能可以让您解决这个问题。这是清单。清单是类,其实例是表示类型的对象。由于这些实例是对象,因此您可以传递它们,存储它们并通常在它们上调用方法。在隐式参数的支持下,它成为一个非常强大的工具。以以下示例为例:

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

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

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


3
get方法可以定义为for ((om, v) <- _map get key if om <:< m) yield v.asInstanceOf[T]
亚伦·诺夫斯特鲁普

4
@Aaron很好的建议,但我担心它可能会使对于Scala相对较新的人员的代码晦涩难懂。编写代码时,我本人对Scala的经验不是很丰富,那是在我将其放入此问题/答案之前的某个时间。
Daniel C. Sobral

6
@KimStebel您知道TypeTag实际上是在模式匹配中自动使用的吗?酷吧?
Daniel C. Sobral

1
凉!也许您应该将其添加到答案中。
Kim Stebel,2012年

1
要在上面回答我自己的问题:是的,编译器会Manifest自行生成参数,请参见:stackoverflow.com/a/11495793/694469 “ [清单/类型标签]实例[...]由编译器隐式创建。 “
KajMagnus 2012年

96

您可以使用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!")
}

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

不幸的是,这有点冗长,您需要@unchecked批注来禁止编译器警告。将来,编译器可能会自动将TypeTag合并到模式匹配中:https ://issues.scala-lang.org/browse/SI-6517


2
删除不必要的内容会怎么样,[List String @unchecked]因为它不会对该模式匹配添加任何内容(只需使用即可case strlist if typeOf[A] =:= typeOf[String] =>完成操作,即使case _ if typeOf[A] =:= typeOf[String] =>无需在主体中使用bound变量case)。
2014年

1
我想这对于给定的示例将起作用,但是我认为大多数实际用法将从元素类型的受益。
tksfz 2014年

在上面的示例中,防护条件前面的未经检查的零件是否不进行铸造?在无法将其强制转换为字符串的第一个对象上进行匹配时,是否会出现类强制转换异常?
Toby

嗯,不,我认为在应用防护之前不会进行强制转换-未选中的位有点无操作,直到=>执行右边的代码为止。(当执行rhs上的代码时,警卫人员将对元素的类型提供静态保证。可能在其中进行了
强制转换

此解决方案是否会产生大量的运行时开销?
stanislav.chetvertkov

65

您可以使用无形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))

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


14
应该注意的是,“ cast”操作将递归地遍历整个集合及其子集合,并检查所有涉及的值是否都是正确的类型。(即,l1.cast[List[String]]粗略地做for (x<-l1) assert(x.isInstanceOf[String])对于大型数据结构或如果强制转换非常频繁,这可能是不可接受的开销。
Dominique Unruh'9

16

我想出了一个相对简单的解决方案,该解决方案在有限的使用情况下就足够了,本质上是将参数化的类型包装起来,这种类型的参数会因可在match语句中使用的包装类中的类型擦除问题而受苦。

case class StringListHolder(list:List[String])

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

它具有预期的输出,并将案例类的内容限制为所需的类型,即字符串列表。

此处有更多详细信息:http : //www.scalafied.com/?p=60


14

有一种方法可以解决Scala中的类型擦除问题。在克服类型擦除在匹配1克服类型擦除在匹配2(方差)是如何代码一些助手来包装类型,包括方差,用于匹配一些解释。


这不能克服类型擦除。在他的示例中,执行val x:Any = List(1,2,3); x match {case IntList(l)=> println(s“ Match $ {l(1)}”); case _ => println(s“ No match”)}产生“ No match”
user48956 2013年

您可以看看scala 2.10宏。
亚历克斯

11

对于这种原本很棒的语言,我发现了一个更好的解决方法。

在Scala中,数组不会发生类型擦除的问题。我认为通过示例更容易证明这一点。

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

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

要解决此问题,请首先创建一个案例类:

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

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

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

这似乎工作完美。

这将需要对代码进行较小的更改以使用数组而不是列表,但是这不是主要问题。

请注意,using case a:Array[(Int, String)]仍会发出类型擦除警告,因此必须使用新的容器类(在此示例中为IntString)。


10
“限制本来很棒的语言”,它不是Scala的限制,而是JVM的限制。也许Scala可以被设计为包含在JVM上运行的类型信息,但是我认为这样的设计不会保留与Java的互操作性(即,按照设计,您可以从Java调用Scala。)
Carl G

1
作为后续措施,.NET / CLR中支持Scala的泛型泛型的可能性不断增加。
卡尔·G

6

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


4

我想知道这是否是合适的解决方法:

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")
     | }

是不是更好,还是我在这里遗漏了要点?


3
不适用于类型为List [Any]的List(1,“ a”,“ b”)
sullivan-

1
尽管sullivan的观点是正确的,并且继承存在相关问题,但我仍然发现这很有用。
塞斯


0

我想添加一个将问题概括为的答案:如何在运行时获取列表类型的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))

-18

使用模式匹配防护

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

4
该程序不起作用的原因是isInstanceOf基于JVM可用的类型信息进行运行时检查。并且该运行时信息将不包含的type参数List(由于类型擦除)。
Dominique Unruh
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.