在集合中使用Scala类标签



我试图在Scala中创建一个泛型类型的类enum类型,稍后对依赖于泛型使用Scala的reflect.ClassTag获取泛型类型信息的类型实例进行操作。像这样:

import scala.reflect.ClassTag
sealed trait X[T : ClassTag] {
def value: T
}
case object I1 extends X[Int] { override def value = 1 }
case object I2 extends X[Int] { override def value = 2 }
case object Sa extends X[String] { override def value = "a" }
case object Sb extends X[String] { override def value = "b" }
val values = IndexedSeq(I1, I2, Sa, Sb)
values.foreach{
case i: X[Int] => println(s"${i.value} => ${i.value + 1}")
case s: X[String] => println(s"${s.value} => ${s.value.toUpperCase}")
}

这会产生以下警告:

the type test for Playground.X[Int] cannot be checked at runtime
the type test for Playground.X[String] cannot be checked at runtime

为了完整起见,当运行时,它产生以下输出(考虑到警告,这是合理的):

1 => 2
2 => 3
java.lang.ExceptionInInitializerError
at Main$.<clinit>(main.scala:24)
at Main.main(main.scala)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at sbt.Run.invokeMain(Run.scala:143)
at sbt.Run.execute$1(Run.scala:93)
at sbt.Run.$anonfun$runWithLoader$5(Run.scala:120)
at sbt.Run$.executeSuccess(Run.scala:186)
at sbt.Run.runWithLoader(Run.scala:120)
at sbt.Run.run(Run.scala:127)
at com.olegych.scastie.sbtscastie.SbtScastiePlugin$$anon$1.$anonfun$run$1(SbtScastiePlugin.scala:38)
at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23)
at sbt.util.InterfaceUtil$$anon$1.get(InterfaceUtil.scala:17)
at sbt.ScastieTrapExit$App.run(ScastieTrapExit.scala:259)
at java.base/java.lang.Thread.run(Thread.java:831)
Caused by: java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:99)
at Playground$.$anonfun$1(main.scala:17)
at scala.runtime.function.JProcedure1.apply(JProcedure1.java:15)
at scala.runtime.function.JProcedure1.apply(JProcedure1.java:10)
at scala.collection.immutable.Vector.foreach(Vector.scala:1856)
at Playground$.<clinit>(main.scala:20)
... 17 more

我也试着这样做,用单例objects作为case classes的实例实现:

import scala.reflect.ClassTag
sealed trait X[T : ClassTag] {
def value: T
}
case class I(value: Int) extends X[Int]
case class S(value: String) extends X[String]
val values = IndexedSeq(I(1), I(2), S("a"), S("b"))
values.foreach{
case i: X[Int] => println(s"${i.value} => ${i.value + 1}")
case s: X[String] => println(s"${s.value} => ${s.value.toUpperCase}")
}

但是我得到了几乎相同的结果。

当我使用ScalaArray做一些对我来说类似的事情时,它可以工作:

val values = IndexedSeq(
Array(1, 2),
Array(3, 4),
Array("a", "b"),
Array("c", "d")
)
values.foreach{
case i: Array[Int] => println(s"""${i.mkString(",")} => ${i.map(_ * 2).mkString(",")}""")
case s: Array[String] => println(s"""${s.mkString(",")} => ${s.map(_.toUpperCase).mkString(",")}""")
}

这不会产生警告和正确的输出:

1,2 => 2,4
3,4 => 6,8
a,b => A,B
c,d => C,D

我在这里做错了什么?我认为ClassTag应该在运行时保存关于泛型类型的信息?我已经看到reflect.runtime.universe.TypeTag可能会更好,但是包含它的包似乎在Scala 3中不可用,并且Array无论如何都能够做我想要的。

从文档中,class:

ClassTag[T]存储给定类型T的擦除类,可访问通过runtimeClass字段。

不能避免T的类型擦除。T仍然被擦除。ClassTag所做的只是将T的擦除类型存储在ClassTag[T]类型的对象中(通过上下文绑定隐式添加),但是隐式参数仅在定义它们的方法或类的主体中具有作用域,因此您只能在trait的主体中使用它,因为那是您定义它的地方。

TraitX在运行时仍然不知道它的类型参数T,但是你可以在Trait的主体中访问实际的类型T,因为你在上下文绑定中有对它的引用。但是对于trait之外的模式匹配,你根本不需要在trait中绑定ClassTag上下文。

那么通过这样做:

sealed trait X[T : ClassTag]

相当于:

sealed trait X[T](implicit cls: ClassTag[T])

您假设模式匹配的行为会有所不同。但事实并非如此。模式匹配仍然是相同的:它不检查泛型类型,因为它们在编译时被擦除。这就是为什么你会得到警告。所以case的第一行使用:

case i: X[Int]

等价于:

case i: X[_]

实际上,你的两种情况都等价于这个。但是第二个代码变成了死代码,因为第一个代码总是匹配的。因此,values的所有元素都在第一个case上匹配,当到达${i.value + 1}时,这将适用于前2个元素,但它将在元素3和4上抛出ClassCastException,因为它将尝试通过将String转换为Integer以添加1来进行整数加法。

使用ClassTag最接近的方法是将模式匹配移动到多态方法中,并通过隐式参数传递ClassTag值,或者更方便地使用上下文绑定:

import scala.reflect.ClassTag
sealed trait X[T] {
def value: T
}
case object I1 extends X[Int] { override def value = 1 }
case object I2 extends X[Int] { override def value = 2 }
case object Sa extends X[String] { override def value = "a" }
case object Sb extends X[String] { override def value = "b" }
val values = IndexedSeq(I1, I2, Sa, Sb)
def extract[T : ClassTag] = 
values.flatMap { x => 
x.value match {
case y: T => Some(y)
case _    => None
} 
}
val result = extract[Int]
val result2 = extract[String]
println(result)    // Vector(1, 2)
println(result2)   // Vector(a, b)

这实际上给人一种错觉,认为它不会在运行时擦除泛型类型T,但它确实擦除了。如果您反编译extract方法,您将看到如下内容:

public <T> IndexedSeq<T> extract(final ClassTag<T> evidence$1) {
return (IndexedSeq<T>)this.values().flatMap(x -> {
final Object value = ((MainScala.X)x).value();
if (value != null) {
final Option unapply = evidence$1.unapply(value);
if (!unapply.isEmpty() && unapply.get() instanceof Object) {
final Object module$ = new Some(value);
return (Option)module$;
}
}
final Object module$ = None$.MODULE$;
return (Option)module$;
});
}

可以看到,instanceof Object检查意味着类型T已被擦除。但它前面的一行是我们更感兴趣的:

final Option unapply = evidence$1.unapply(value);

这就是ClassTag发挥作用的地方,如果我们检查ClassTag.scalaunapply方法,我们可以看到它的作用:

/** A ClassTag[T] can serve as an extractor that matches only objects of type T.
*
* The compiler tries to turn unchecked type tests in pattern matches into checked ones
* by wrapping a `(_: T)` type pattern as `ct(_: T)`, where `ct` is the `ClassTag[T]` instance.
* Type tests necessary before calling other extractors are treated similarly.
* `SomeExtractor(...)` is turned into `ct(SomeExtractor(...))` if `T` in `SomeExtractor.unapply(x: T)`
* is uncheckable, but we have an instance of `ClassTag[T]`.
*/
def unapply(x: Any): Option[T] =
if (runtimeClass.isInstance(x)) Some(x.asInstanceOf[T])
else None

检查这个,我们看到runtimeClass字段按照文档说明。因此,ClassTag能够将类型参数T恢复为被调用的方法的实际类型参数,因此T对于extract[Int]变为Int,对于extract[String]变为String,以此类推。

关于你的其他问题:使它们成为case类不会改变任何东西,因为您仍然在泛型类型上进行模式匹配,泛型类型的类型参数已被删除。

改变模式匹配使用Arrays工作,因为Arrays不是Scala(也不是Java)的泛型类型。实际上,Scala数组是作为Java数组实现的,并且JVM保留了它们的类型,因为数组在Java 5中引入泛型之前就存在了,所以它们的类型从一开始就没有被擦除过。

这个例子只是为了理解这个概念。如果您不需要反射,就不应该使用它,在这个特殊的用例中,您不需要。有多种选择:直接匹配case类,如AminMal所提到的,或者直接匹配value字段,等等:

values.foreach{ _.value match {
case s: String => println(s"${s} => ${s.toUpperCase}")
case i: Int    => println(s"${i} => ${i + 1}")
} 
}

最新更新