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

问题描述

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

我的方法是在编译时使用一个宏来生成信息,该宏会扩展到包含由运行时类名称索引的信息的映射。 像这样:

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)

问题是该宏给出的已擦除类的名称与运行时通过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

如何在编译时获取类在运行时将具有的名称的正确方法

解决方法

反之亦然,在运行时,拥有类的Java名称(带美元),您可以获得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
  }

  ...
}

这仅适用于编译时存在的类。因此,该项目应组织如下

  • 子项目 common ShapeBoxSphere
  • 子项目(取决于 common )。 def subClassesOf...
  • 子项目 core (取决于 common )。 subclassesOf[shapes.Shape]...
,

@Dmytro_Mitin的回答使我注意到,scala反射API并没有提供一种快速的单行方法来获取类在运行时将具有的名称,并引导我以另一种方式解决了我的特殊问题。

如果您需要知道的不是运行时类名称本身,而是仅在运行时类名称与编译时可访问的名称有效地匹配时,此答案可能会有用。

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

/** 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
}

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

还请注意,第一个参数仅支持编译时名称(仅点),而第二个参数均支持编译时名称或运行时名称。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...