在 Scala 宏中,如何获取 class 在运行时的全名?

In a scala macro, how to get the full name that a class will have at runtime?

目的是在 运行 时获取特定 classes 的一些信息,这些信息仅在编译时可用。

我的方法是在编译时使用宏生成信息,该宏扩展为包含由 运行time class 名称索引的信息的地图。 像这样:

object macros {
    def subClassesOf[T]: Map[String, Info] = macro subClassesOfImpl[T];

    def subClassesOfImpl[T: ctx.WeakTypeTag](ctx: blackbox.Context): ctx.Expr[Map[String, Info]] = {
        import ctx.universe._

        val classSymbol = ctx.weakTypeTag[T].tpe.typeSymbol.asClass
        val addEntry_codeLines: List[Tree] =
            for {baseClassSymbol <- classSymbol.knownDirectSubclasses.toList} yield {
                val key = baseClassSymbol.asType.toType.erasure.typeSymbol.fullName
                q"""builder.addOne($key -> new Info("some info"));"""
            }
        q"""
            val builder = Map.newBuilder[String, Info];
            {..$addEntry_codeLines}
            builder.result();""";
        ctx.Expr[Map[String, Info]](body_code);
    }
}

我们会这样使用哪个:

object shapes {
    trait Shape;
    case class Box(h: Int, w: Int);
    case class Sphere(r: Int);
}

val infoMap = subclassesOf[shapes.Shape];
val box = Box(3, 7);
val infoOfBox = infoMap.get(box.getClass.getName)

问题是那个宏给出的被擦除的classes的名称与运行时通过someInstance.getClass.getName方法得到的名称略有不同。第一个使用点分隔容器和成员,第二个使用美元。

scala> infoMap.mkString("\n")
val res7: String =
shapes.Box -> Info(some info)
shapes.Sphere -> Info(some info)

scala> box.getClass.getName
val res8: String = shapes$Box

如何在编译时获取 class 在 运行 时的名称的正确方法?

反之亦然,在运行时,具有 Java 名称的 class(带美元)您可以获得 Scala 名称(带点)。

box.getClass.getName 
// com.example.App$shapes$Box

import scala.reflect.runtime
val runtimeMirror = runtime.currentMirror

runtimeMirror.classSymbol(box.getClass).fullName // com.example.App.shapes.Box

即使 replace

也可以做到这一点
val nameWithDot = box.getClass.getName.replace('$', '.')
if (nameWithDot.endsWith(".")) nameWithDot.init else nameWithDot 
// com.example.App.shapes.Box

反正编译的时候可以试试

def javaName[T]: String = macro javaNameImpl[T]

def javaNameImpl[T: ctx.WeakTypeTag](ctx: blackbox.Context): ctx.Expr[String] = {
  import ctx.universe._
  val symbol = weakTypeOf[T].typeSymbol
  val owners = Seq.unfold(symbol)(symb => 
    if (symb != ctx.mirror.RootClass) Some((symb, symb.owner)) else None
  )
  val nameWithDollar = owners.foldRight("")((symb, str) => {
    val sep = if (symb.isPackage) "." else "$"
    s"$str${symb.name}$sep"
  })
  val name = if (symbol.isModuleClass) nameWithDollar else nameWithDollar.init
  ctx.Expr[String](q"${name: String}")
}

javaName[shapes.Shape] // com.example.App$shapes$Shape

另一种选择是在宏内使用运行时反射。替换

val key = baseClassSymbol.asType.toType.erasure.typeSymbol.fullName

val key = javaName(baseClassSymbol.asType.toType.erasure.typeSymbol.asClass)

哪里

def subClassesOfImpl[T: ctx.WeakTypeTag](ctx: blackbox.Context): ctx.Expr[Map[String, Info]] = {
  import ctx.universe._

  def javaName(symb: ClassSymbol): String = {
    val rm = scala.reflect.runtime.currentMirror
    rm.runtimeClass(symb.asInstanceOf[scala.reflect.runtime.universe.ClassSymbol]).getName
  }

  ...
}

这仅适用于编译时存在的 classes。所以项目应该组织如下

  • 子项目普通ShapeBoxSphere
  • 子项目macros(依赖于common)。 def subClassesOf...
  • 子项目核心(取决于普通)。 subclassesOf[shapes.Shape]...

@Dmytro_Mitin 的回答帮助我注意到 scala reflect API 没有提供快速的一行方法来获取 class 在运行时将具有的名称,并指导我以另一种方式解决我的特定问题。

如果您需要知道的不是 run-time class 名称本身,而是仅当它与 compile-time 处可访问的名称有效匹配时,那么此答案可能会有用。

而不是在运行时弄清楚名称是什么,这在知道所有 classes 之前显然是不可能的;只需找出我们在编译时知道的名称是否对应于在运行时获得的名称即可。 这可以通过考虑相关字符并忽略编译器稍后附加的任何字符串比较器来实现。

/** Compares two class names based on the length and, if both have the same length, by alphabetic order of the reversed names.
 * If the second name (`b`) ends with a dollar, or a dollar followed by digits, they are removed before the comparison begins. This is necessary because the runtime class name of: module classes have an extra dollar at the end, local classes have a dollar followed by digits, and local object digits surrounded by dollars.
 * Differences between dots and dollars are ignored if the dot is in the first name (`a`) and the dollar in the second name (`b`). This is necessary because the runtime class name of nested classes use a dollar instead of a dot to separate the container from members.
 * The names are considered equal if the fragments after the last dot of the second name (`b`) are equal. */
val productNameComparator: Comparator[String] = { (a, b) =>
    val aLength = a.length;
    var bLength = b.length;
    var bChar: Char = 0;
    var index: Int = 0;

    // Ignore the last segment of `b` if it matches "($\d*)+". This is necessary because the runtime class name of: module classes have an extra dollar at the end, local classes have a dollar followed by a number, and local object have a number surrounded by dollars.
    // Optimized versión
    var continue = false;
    do {
        index = bLength - 1;
        continue = false;
        //  find the index of the last non digit character
        while ( {bChar = b.charAt(index); Character.isDigit(bChar)}) {
            index -= 1;
        }
        // if the index of the last non digit character is a dollar, remove it along with the succeeding digits for the comparison.
        if (b.charAt(index) == '$') {
            bLength = index;
            // if something was removed, repeat the process again to support combinations of edge cases. It is not necessary to know all the edge cases if it's known that any dollar or dollar followed by digits at the end are not part of the original class name. So we can remove combinations of them without fear.
            continue = true;
        }
    } while(continue)

    // here starts the comparison
    var diff = aLength - bLength;
    if (diff == 0 && aLength > 0) {
        index = aLength - 1;
        do {
            val aChar = a.charAt(index);
            bChar = b.charAt(index);
            diff = if (aChar == '.' && bChar == '$') {
                0 // Ignore difference between dots and dollars. This assumes that the first name (an) is obtained by the macro, and the second (bn) may be obtained at runtime from the Class object.
            } else {
                aChar - bChar;
            }
            index -= 1;
        } while (diff == 0 && index >= 0 && bChar != '.')
    }
    diff
}

请注意,它旨在比较扩展相同密封特征或抽象 class 的 class 的名称。这意味着名称可能仅在最后一个点之后有所不同。

另请注意,第一个参数仅支持编译时名称(仅点),而第二个参数同时支持 compile-time 或 run-time 名称。