如何绕过 Scala 上的类型擦除?或者,为啥我不能获取我的集合的类型参数?

Posted

技术标签:

【中文标题】如何绕过 Scala 上的类型擦除?或者,为啥我不能获取我的集合的类型参数?【英文标题】:How do I get around type erasure on Scala? Or, why can't I get the type parameter of my collections?如何绕过 Scala 上的类型擦除?或者,为什么我不能获取我的集合的类型参数? 【发布时间】:2009-07-07 19:03:38 【问题描述】:

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

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

【问题讨论】:

Scala 2.8 Beta 1 RC4 刚刚对类型擦除的工作方式进行了一些更改。我不确定这是否会直接影响您的问题。 这只是类型擦除,已经改变了。它的简称可以概括为“提案:“Object with A”的擦除是“A”而不是“Object”。” 实际的规范是相当复杂的。无论如何,它是关于 mixins,而这个问题是关于泛型的。 感谢您的澄清——我是 scala 新手。我觉得现在是跳入 Scala 的糟糕时机。早些时候,我可以从一个良好的基础上了解 2.8 中的变化,后来我永远不必知道其中的区别! 这是一个关于TypeTags的有点相关的问题。 运行scala 2.10.2,我看到了这个警告:&lt;console&gt;: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] =&gt; println("a list of strings?") ^ 我觉得你的问题和回答很有帮助,但我不确定这个更新的警告是否对读者有用。跨度> 【参考方案1】:

此答案使用 Manifest-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 是一个类,其实例代表 Scala 类型。这些对象比 JVM 拥有更多的信息,这使我们能够测试完整的参数化类型。

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

【讨论】:

get方法可以定义为for ((om, v) &lt;- _map get key if om &lt;:&lt; m) yield v.asInstanceOf[T] @Aaron 非常好的建议,但我担心对于 Scala 相对较新的人来说,这可能会使代码变得模糊。当我编写该代码时,我自己对 Scala 的经验并不丰富,这是在我将其放入此问题/答案之前的某个时间。 @KimStebel 你知道TypeTag 实际上是自动用于模式匹配的吗?很酷,嗯? 酷!也许您应该将其添加到答案中。 回答我自己上面的问题:是的,编译器会自己生成Manifest 参数,请参阅:***.com/a/11495793/694469 "[manifest/type-tag] 实例 [... ] 是由编译器隐式创建的"【参考方案2】:

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

【讨论】:

如何删除不必要的[List String @unchecked],因为它不会向此模式匹配添加任何内容(只需使用case strlist if typeOf[A] =:= typeOf[String] =&gt; 即可,如果在主体中不需要绑定变量,甚至使用case _ if typeOf[A] =:= typeOf[String] =&gt; case)。 我想这对于给定的示例来说是可行的,但我认为大多数实际用法都会受益于元素的类型。 在上面的例子中,守卫条件前面未检查的部分不是进行强制转换吗?在第一个不能转换为字符串的对象上进行匹配时,您不会得到类转换异常吗? 嗯,不,我相信在应用保护之前没有强制转换 - 未经检查的位在执行 =&gt; 右侧的代码之前是一种无操作。 (并且当执行 rhs 上的代码时,守卫对元素的类型提供静态保证。那里可能有强制转换,但它是安全的。) 此解决方案是否会产生显着的运行时开销?【参考方案3】:

您可以使用shapeless 中的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 操作将尽可能精确地擦除。

【讨论】:

需要注意的是,“cast”操作会递归地遍历整个集合及其子集合,并检查所有涉及的值是否都是正确的类型。 (即,l1.cast[List[String]] 大致相当于for (x&lt;-l1) assert(x.isInstanceOf[String])对于大型数据结构或者如果转换经常发生,这可能是不可接受的开销。【参考方案4】:

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

case class StringListHolder(list:List[String])

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

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

更多详情:http://www.scalafied.com/?p=60

【讨论】:

【参考方案5】:

有一种方法可以克服 Scala 中的类型擦除问题。在 Overcoming Type Erasure in matching 1Overcoming Type Erasure in Matching 2 (Variance) 中解释了如何编写一些帮助程序来包装类型,包括 Variance,以进行匹配。

【讨论】:

这并不能克服类型擦除。在他的例子中,做 val x:Any = List(1,2,3); x match case IntList(l) => println( s"Match $l(1)" ); case _ => println( s"No match" ) 产生“No match”【参考方案6】:

我发现了一个更好的解决方法来解决这个原本很棒的语言的限制。

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

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

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

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

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

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

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

这似乎工作得很好。

这将需要对您的代码进行微小的更改才能使用数组而不是列表,但这应该不是主要问题。

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

【讨论】:

“其他很棒的语言的限制”它不是 Scala 的限制,而是 JVM 的限制。也许 Scala 可以设计为在 JVM 上运行时包含类型信息,但我认为这样的设计不会保留与 Java 的互操作性(即,按照设计,您可以从 Java 调用 Scala。) 作为后续,support for reified generics for Scala in .NET/CLR 是一种持续的可能性。【参考方案7】:

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

【讨论】:

【参考方案8】:

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

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

这不是更好还是我错过了这里的重点?

【讨论】:

不适用于 List(1, "a", "b"),它的类型为 List[Any] 虽然 sullivan 的观点是正确的,并且继承存在相关问题,但我仍然觉得这很有用。【参考方案9】:

不是一个解决方案,而是一种在不把它完全扫到地毯下的情况下与之共处的方式: 添加@unchecked 注释。看这里 - http://www.scala-lang.org/api/current/index.html#scala.unchecked

【讨论】:

【参考方案10】:

我想添加一个将问题概括为:How do a get a String representation of my list of my list at runtime

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

【讨论】:

【参考方案11】:

使用模式匹配保护

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

【讨论】:

这个不起作用的原因是isInstanceOf根据JVM可用的类型信息进行了运行时检查。并且该运行时信息将不包含 List 的类型参数(因为类型擦除)。

以上是关于如何绕过 Scala 上的类型擦除?或者,为啥我不能获取我的集合的类型参数?的主要内容,如果未能解决你的问题,请参考以下文章

Scala模式匹配泛型类型擦除问题

Scala:数组和类型擦除

Scala双重定义(2个方法具有相同的类型擦除)

Scala:抽象类型模式 A 未选中,因为它已被擦除消除

Kotlin的类型具体化在Java或Scala中是不可能实现的?

scala的反射