什么是TypeTag,我该如何使用它?

384

关于TypeTags我所知道的是它们在某种程度上取代了Manifests。然而,互联网上关于这个主题的信息很少,并不能让我深刻地理解这个主题。

因此,如果有人能分享一些有用的材料,包括示例和流行的使用案例,我将感到非常高兴。详细的答案和解释也是受欢迎的。


2
以下来自Scala文档的文章介绍了类型标签的何为何用,以及如何在您的代码中使用它们:http://docs.scala-lang.org/overviews/reflection/typetags-manifests.html - btiernay
https://stackoverflow.com/questions/59473734 https://dev59.com/emgu5IYBdhLWcg3wMkfF#74063258 https://dev59.com/w3wQtIcB2Jgan1znKSEZ#74377253 https://dev59.com/KMf6oIgBc1ULPQZFIFAF - Dmytro Mitin
1个回答

580

TypeTag 可以解决 Scala 类型在运行时被擦除(类型擦除)的问题。如果我们想要进行

class Foo
class Bar extends Foo

def meth[A](xs: List[A]) = xs match {
  case _: List[String] => "list of strings"
  case _: List[Foo] => "list of foos"
}

我们会收到警告:

<console>:23: warning: non-variable type argument String in type pattern List[String]
is unchecked since it is eliminated by erasure
         case _: List[String] => "list of strings"
                 ^
<console>:24: warning: non-variable type argument Foo in type pattern List[Foo]
is unchecked since it is eliminated by erasure
         case _: List[Foo] => "list of foos"
                 ^

为了解决这个问题,Scala 引入了 Manifests。但是它们存在一个问题,不能表示许多有用的类型,比如路径相关类型。
scala> class Foo{class Bar}
defined class Foo

scala> def m(f: Foo)(b: f.Bar)(implicit ev: Manifest[f.Bar]) = ev
warning: there were 2 deprecation warnings; re-run with -deprecation for details
m: (f: Foo)(b: f.Bar)(implicit ev: Manifest[f.Bar])Manifest[f.Bar]

scala> val f1 = new Foo;val b1 = new f1.Bar
f1: Foo = Foo@681e731c
b1: f1.Bar = Foo$Bar@271768ab

scala> val f2 = new Foo;val b2 = new f2.Bar
f2: Foo = Foo@3e50039c
b2: f2.Bar = Foo$Bar@771d16b9

scala> val ev1 = m(f1)(b1)
warning: there were 2 deprecation warnings; re-run with -deprecation for details
ev1: Manifest[f1.Bar] = Foo@681e731c.type#Foo$Bar

scala> val ev2 = m(f2)(b2)
warning: there were 2 deprecation warnings; re-run with -deprecation for details
ev2: Manifest[f2.Bar] = Foo@3e50039c.type#Foo$Bar

scala> ev1 == ev2 // they should be different, thus the result is wrong
res28: Boolean = true

因此,它们被TypeTags所取代,这些标签更加简单易用,并且与新的反射API很好地集成在一起。有了它们,我们可以优雅地解决上述关于路径相关类型的问题:
scala> def m(f: Foo)(b: f.Bar)(implicit ev: TypeTag[f.Bar]) = ev
m: (f: Foo)(b: f.Bar)(implicit ev: reflect.runtime.universe.TypeTag[f.Bar])↩
reflect.runtime.universe.TypeTag[f.Bar]

scala> val ev1 = m(f1)(b1)
ev1: reflect.runtime.universe.TypeTag[f1.Bar] = TypeTag[f1.Bar]

scala> val ev2 = m(f2)(b2)
ev2: reflect.runtime.universe.TypeTag[f2.Bar] = TypeTag[f2.Bar]

scala> ev1 == ev2 // the result is correct, the type tags are different
res30: Boolean = false

scala> ev1.tpe =:= ev2.tpe // this result is correct, too
res31: Boolean = false

它们也可以轻松用于检查类型参数:

import scala.reflect.runtime.universe._

def meth[A : TypeTag](xs: List[A]) = typeOf[A] match {
  case t if t =:= typeOf[String] => "list of strings"
  case t if t <:< typeOf[Foo] => "list of foos"
}

scala> meth(List("string"))
res67: String = list of strings

scala> meth(List(new Bar))
res68: String = list of foos

此时,非常重要的是要理解使用=:=(类型相等)和<:<(子类型关系)进行等式检查。除非您绝对知道自己在做什么,否则不要使用==!=

scala> typeOf[List[java.lang.String]] =:= typeOf[List[Predef.String]]
res71: Boolean = true

scala> typeOf[List[java.lang.String]] == typeOf[List[Predef.String]]
res72: Boolean = false

后者检查结构相等性,这通常不是应该做的,因为它不关心前缀等内容(例如示例中)。
一个TypeTag完全是由编译器生成的,这意味着当调用期望此TypeTag的方法时,编译器会创建并填充TypeTag。存在三种不同形式的标签:

ClassTag替代了ClassManifest,而TypeTag或多或少是Manifest的替代品。

前者允许完全使用泛型数组:

scala> import scala.reflect._
import scala.reflect._

scala> def createArr[A](seq: A*) = Array[A](seq: _*)
<console>:22: error: No ClassTag available for A
       def createArr[A](seq: A*) = Array[A](seq: _*)
                                           ^

scala> def createArr[A : ClassTag](seq: A*) = Array[A](seq: _*)
createArr: [A](seq: A*)(implicit evidence$1: scala.reflect.ClassTag[A])Array[A]

scala> createArr(1,2,3)
res78: Array[Int] = Array(1, 2, 3)

scala> createArr("a","b","c")
res79: Array[String] = Array(a, b, c)

ClassTag 提供了在运行时创建类型所需的信息(这些类型被擦除):

scala> classTag[Int]
res99: scala.reflect.ClassTag[Int] = ClassTag[int]

scala> classTag[Int].runtimeClass
res100: Class[_] = int

scala> classTag[Int].newArray(3)
res101: Array[Int] = Array(0, 0, 0)

scala> classTag[List[Int]]
res104: scala.reflect.ClassTag[List[Int]] =↩
        ClassTag[class scala.collection.immutable.List]

如上所示,他们不关心类型擦除,因此如果想要“完整”的类型,应该使用TypeTag

scala> typeTag[List[Int]]
res105: reflect.runtime.universe.TypeTag[List[Int]] = TypeTag[scala.List[Int]]

scala> typeTag[List[Int]].tpe
res107: reflect.runtime.universe.Type = scala.List[Int]

scala> typeOf[List[Int]]
res108: reflect.runtime.universe.Type = scala.List[Int]

scala> res107 =:= res108
res109: Boolean = true

正如我们所看到的,TypeTag 的方法tpe会得到完整的Type,这与调用typeOf时得到的结果相同。当然,可以同时使用ClassTagTypeTag
scala> def m[A : ClassTag : TypeTag] = (classTag[A], typeTag[A])
m: [A](implicit evidence$1: scala.reflect.ClassTag[A],↩
       implicit evidence$2: reflect.runtime.universe.TypeTag[A])↩
      (scala.reflect.ClassTag[A], reflect.runtime.universe.TypeTag[A])

scala> m[List[Int]]
res36: (scala.reflect.ClassTag[List[Int]],↩
        reflect.runtime.universe.TypeTag[List[Int]]) =↩
       (scala.collection.immutable.List,TypeTag[scala.List[Int]])

现在剩下的问题是什么是WeakTypeTag的意义?简而言之,TypeTag表示具体类型(这意味着它只允许完全实例化的类型),而WeakTypeTag则允许任何类型。大多数时候人们不关心哪个是哪个(这意味着应该使用TypeTag),但例如,在使用应该适用于通用类型的宏时,它们是必需的:
object Macro {
  import language.experimental.macros
  import scala.reflect.macros.Context

  def anymacro[A](expr: A): String = macro __anymacro[A]

  def __anymacro[A : c.WeakTypeTag](c: Context)(expr: c.Expr[A]): c.Expr[A] = {
    // to get a Type for A the c.WeakTypeTag context bound must be added
    val aType = implicitly[c.WeakTypeTag[A]].tpe
    ???
  }
}

如果将WeakTypeTag替换为TypeTag,则会抛出错误:
<console>:17: error: macro implementation has wrong shape:
 required: (c: scala.reflect.macros.Context)(expr: c.Expr[A]): c.Expr[String]
 found   : (c: scala.reflect.macros.Context)(expr: c.Expr[A])(implicit evidence$1: c.TypeTag[A]): c.Expr[A]
macro implementations cannot have implicit parameters other than WeakTypeTag evidences
             def anymacro[A](expr: A): String = macro __anymacro[A]
                                                      ^

关于TypeTagWeakTypeTag之间的差异的更详细解释,请参见此问题:Scala宏:“无法从具有未解析类型参数的类型T创建TypeTag”

Scala官方文档网站还包含有关反射的指南


20
谢谢你的回答!以下是我的一些评论:1)对于类型,== 表示结构相等,而非引用相等。=:= 考虑类型等价性(甚至包括来自不同镜像的前缀的等价性),2)TypeTagAbsTypeTag 两者都基于镜像。区别在于 TypeTag 仅允许完全实例化的类型(即没有任何类型参数或引用抽象类型成员的类型),3)详细解释请参见此处:https://dev59.com/d2fWa4cB1Zd3GeqPipy1 - Eugene Burmako
10
  1. 清单存在无法表示许多有用类型的问题。实际上,它们只能表达类型引用 (例如 IntList[Int] 这样的普通类型和泛型类型),而无法表示一些 Scala 类型,如细化类型、路径相关类型、存在类型和注释类型等。此外,清单是一个外置的工具,因此不能使用编译器所拥有的广泛知识来计算类型的线性化,找出一个类型是否是另一个类型的子类型等。
- Eugene Burmako
9
相比之下,类型标签并不是“更好地集成”,它们只是与新的反射API集成(不同于清单文件,其与任何东西都没有集成)。这使得类型标签可以访问编译器的某些方面,例如 Types.scala(了解如何支持类型一起工作的7kloc代码) , Symbols.scala(了解符号表如何工作的3kloc代码)等。 - Eugene Burmako
9
ClassTag 可以完全替换 ClassManifest,而 TypeTag 则可以更或多或少地替代 Manifest。虽然有一些差别:1) 类型标签不包含擦除信息,2) Manifest 是一个大型的 hack,我们放弃了使用类型标签模拟其行为。#1 可以通过在需要保留类型和擦除信息时同时使用 ClassTag 和 TypeTag 上下文限定来解决,而 #2 通常并不重要,因为现在可以丢弃所有的 hack,并使用完整的反射 API。 - Eugene Burmako
11
我希望Scala编译器能够在某个时候摆脱过时的功能,使可用功能集更加正交化。这就是我喜欢新的宏支持的原因,因为它提供了清理语言的潜力,将一些功能分离到独立的库中,而这些库并不属于基础语言。 - Alexandru Nedelcu
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接