反射

註解、名稱、範圍等

語言
此文件頁面特定於 Scala 2 中發布的功能,這些功能已在 Scala 3 中移除或由替代方案取代。除非另有說明,此頁面中的所有程式碼範例都假設您使用 Scala 2。

實驗性

註解

在 Scala 中,可以使用 scala.annotation.Annotation 的子類別註解宣告。此外,由於 Scala 與 Java 的註解系統 整合,因此可以處理標準 Java 編譯器產生的註解。

如果對應的註解已持久化,則可以反思檢視註解,以便從包含註解宣告的類別檔中讀取。可透過繼承 scala.annotation.StaticAnnotationscala.annotation.ClassfileAnnotation,讓自訂註解類型持久化。因此,註解類型的執行個體會儲存在對應類別檔中的特殊屬性中。請注意,僅子類化 scala.annotation.Annotation 不足以讓對應的元資料持久化以供執行時期反思。此外,子類化 scala.annotation.ClassfileAnnotation 並不會讓您的註解在執行時期顯示為 Java 註解;這需要使用 Java 編寫註解類別。

API 區分兩種註解

  • Java 註解:Java 編譯器產生的定義上的註解,附加到程式定義上的 java.lang.annotation.Annotation 子類型。Scala 反思讀取時,scala.annotation.ClassfileAnnotation 特質會自動新增為每個 Java 註解的子類別。
  • Scala 註解:Scala 編譯器產生的定義或類型上的註解。

Java 和 Scala 注解之間的區別在 scala.reflect.api.Annotations#Annotation 合約中體現,它同時公開了 scalaArgsjavaArgs。對於擴充 scala.annotation.ClassfileAnnotation 的 Scala 或 Java 注解,scalaArgs 為空,且參數(如果有的話)儲存在 javaArgs 中。對於所有其他 Scala 注解,參數儲存在 scalaArgs 中,且 javaArgs 為空。

scalaArgs 中的參數表示為型別樹。請注意,這些樹不會被型別檢查器之後的任何階段轉換。在 javaArgs 中的參數表示為從 scala.reflect.api.Names#Namescala.reflect.api.Annotations#JavaArgument 的映射。JavaArgument 的實例表示不同類型的 Java 注解參數

  • 字面值(原始值和字串常數)、
  • 陣列,以及
  • 巢狀註解。

名稱

名稱是字串的簡單包裝器。 Name 有兩個子類型 TermNameTypeName,用來區分項(例如物件或成員)和類型(例如類別、特質和類型成員)的名稱。名稱相同的項和類型可以在同一個物件中並存。換句話說,類型和項有各自的命名空間。

名稱與一個宇宙相關。範例

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> val mapName = TermName("map")
mapName: scala.reflect.runtime.universe.TermName = map

在上面,我們建立一個與執行時期反射宇宙相關的 Name(這也可以在它的路徑依賴類型 reflect.runtime.universe.TermName 中看到)。

名稱通常用於查詢類型的成員。例如,要搜尋 List 類別中宣告的 map 方法(它是一個項),可以執行

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

scala> listTpe.member(mapName)
res1: scala.reflect.runtime.universe.Symbol = method map

要搜尋類型成員,可以遵循相同的程序,改用 TypeName

標準名稱

某些名稱,例如「_root_」,在 Scala 程式中具有特殊意義。因此,它們對於反射性地存取某些 Scala 建構至關重要。例如,反射性地呼叫建構函式需要使用標準名稱 universe.termNames.CONSTRUCTOR,這是 JVM 上表示建構函式名稱的項名稱 <init>

同時有

  • 標準項名稱,例如「<init>」、「package」和「_root_」,以及
  • 標準類型名稱,例如「<error>」、「_」和「_*」。

有些名稱,例如「package」,同時存在類型名稱和項名稱。標準名稱可透過類別 UniversetermNamestypeNames 成員取得。如需所有標準名稱的完整說明,請參閱 API 文件

範圍

範圍物件通常將名稱對應到對應詞彙範圍中可用的符號。範圍可以巢狀。不過,反射 API 中公開的基本類型只公開一個最小介面,將範圍表示為 Symbol 的可迭代物件。

成員範圍 中公開了附加功能,這些範圍由 scala.reflect.api.Types#TypeApi 中定義的 membersdecls 傳回。scala.reflect.api.Scopes#MemberScope 支援 sorted 方法,此方法會依宣告順序對成員進行排序。

下列範例會傳回 List 類別所有 final 成員的符號清單,並依宣告順序排列

scala> val finals = listTpe.decls.sorted.filter(_.isFinal)
finals: List(method isEmpty, method map, method collect, method flatMap, method takeWhile, method span, method foreach, method reverse, method foldRight, method length, method lengthCompare, method forall, method exists, method contains, method find, method mapConserve, method toList)

Expr

除了抽象語法樹的基本類型 scala.reflect.api.Trees#Tree 之外,已類型化的樹也可以表示為類型 scala.reflect.api.Exprs#Expr 的執行個體。Expr 會包裝一個抽象語法樹和一個內部類型標籤,以提供對樹類型的存取權。 Expr 主要用於簡單且方便地建立已類型化的抽象語法樹,以便在巨集中使用。在大部分情況下,這會涉及 reifysplice 方法(請參閱 巨集指南 以取得詳細資料)。

旗標和旗標集

旗標用來提供抽象語法樹的修改項,這些語法樹透過 scala.reflect.api.Trees#Modifiersflags 欄位來表示定義。接受修改項的語法樹為

  • scala.reflect.api.Trees#ClassDef。類別和特質。
  • scala.reflect.api.Trees#ModuleDef。物件。
  • scala.reflect.api.Trees#ValDef。值、變數、參數和自我類型註解。
  • scala.reflect.api.Trees#DefDef。方法和建構函式。
  • scala.reflect.api.Trees#TypeDef。類型別名、抽象類型成員和類型參數。

例如,要建立一個名為 C 的類別,可以寫類似以下的程式碼

ClassDef(Modifiers(NoFlags), TypeName("C"), Nil, ...)

在此,旗標集是空的。要讓 C 為私有的,可以寫類似以下的程式碼

ClassDef(Modifiers(PRIVATE), TypeName("C"), Nil, ...)

旗標也可以與垂直線運算子 (|) 結合使用。例如,一個私有的 final 類別可以寫成類似以下的程式碼

ClassDef(Modifiers(PRIVATE | FINAL), TypeName("C"), Nil, ...)

所有可用的旗標清單定義在 scala.reflect.api.FlagSets#FlagValues 中,可透過 scala.reflect.api.FlagSets#Flag 取得。(通常會針對此部分使用萬用字元匯入,例如 import scala.reflect.runtime.universe.Flag._。)

定義樹會編譯成符號,因此這些樹的修改項上的旗標會轉換成結果符號上的旗標。與樹不同的是,符號不會顯示旗標,而是提供遵循 isXXX 模式的測試方法(例如,可以使用 isFinal 來測試最終性)。在某些情況下,這些測試方法需要使用 asTermasTypeasClass 進行轉換,因為某些旗標只對特定類型的符號有意義。

請注意:鏡像 API 的此部分正被視為重新設計的候選對象。在鏡像 API 的未來版本中,旗標組很可能會被其他東西取代。

常數

Scala 規範中稱為常數表達式的特定表達式可以在編譯時間由 Scala 編譯器評估。下列類型的表達式是編譯時間常數(請參閱Scala 語言規範的第 6.24 節

  1. 原始值類別的文字(ByteShortIntLongFloatDoubleCharBooleanUnit) - 直接表示為對應的類型。

  2. 字串文字 - 表示為字串的實例。

  3. 類別的參考,通常使用 scala.Predef#classOf 建構 - 表示為類型

  4. Java 列舉值的參考 - 表示為符號

常數表達式用於表示

  • 抽象語法樹中的文字(請參閱 scala.reflect.api.Trees#Literal),以及
  • Java 類別檔案註解的文字引數(請參閱 scala.reflect.api.Annotations#LiteralArgument)。

範例

scala> Literal(Constant(5))
val res6: reflect.runtime.universe.Literal = 5

上述表達式會建立一個 AST,表示 Scala 原始碼中的整數文字 5

Constant 是「虛擬案例類別」的一個範例,,一個類別,其實例可以建構並比對,就像它是案例類別一樣。類型 LiteralLiteralArgument 都有一個 value 方法,用於傳回文字底層的編譯時間常數。

範例

Constant(true) match {
  case Constant(s: String)  => println("A string: " + s)
  case Constant(b: Boolean) => println("A Boolean value: " + b)
  case Constant(x)          => println("Something else: " + x)
}
assert(Constant(true).value == true)

類別參考表示為 scala.reflect.api.Types#Type 的執行個體。此類參考可以使用 RuntimeMirrorruntimeClass 方法轉換為執行時期類別,例如 scala.reflect.runtime.currentMirror。(必須從類型轉換為執行時期類別,因為當 Scala 編譯器處理類別參考時,底層執行時期類別可能尚未編譯。)

Java 列舉值參考表示為符號(scala.reflect.api.Symbols#Symbol 的執行個體),在 JVM 上指向傳回底層列舉值的函式。可以使用 RuntimeMirror 來檢查底層列舉或取得對列舉的參考的執行時期值。

範例

// Java source:
enum JavaSimpleEnumeration { FOO, BAR }

import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface JavaSimpleAnnotation {
  Class<?> classRef();
  JavaSimpleEnumeration enumRef();
}

@JavaSimpleAnnotation(
  classRef = JavaAnnottee.class,
  enumRef = JavaSimpleEnumeration.BAR
)
public class JavaAnnottee {}

// Scala source:
import scala.reflect.runtime.universe._
import scala.reflect.runtime.{currentMirror => cm}

object Test extends App {
  val jann = typeOf[JavaAnnottee].typeSymbol.annotations(0).javaArgs

  def jarg(name: String) = jann(TermName(name)) match {
    // Constant is always wrapped in a Literal or LiteralArgument tree node
    case LiteralArgument(ct: Constant) => value
    case _ => sys.error("Not a constant")
  }

  val classRef = jarg("classRef").value.asInstanceOf[Type]
  println(showRaw(classRef))         // TypeRef(ThisType(), JavaAnnottee, List())
  println(cm.runtimeClass(classRef)) // class JavaAnnottee

  val enumRef = jarg("enumRef").value.asInstanceOf[Symbol]
  println(enumRef)                   // value BAR

  val siblings = enumRef.owner.typeSignature.decls
  val enumValues = siblings.filter(sym => sym.isVal && sym.isPublic)
  println(enumValues)                // Scope {
                                     //   final val FOO: JavaSimpleEnumeration;
                                     //   final val BAR: JavaSimpleEnumeration
                                     // }

  val enumClass = cm.runtimeClass(enumRef.owner.asClass)
  val enumValue = enumClass.getDeclaredField(enumRef.name.toString).get(null)
  println(enumValue)                 // BAR
}

印表機

用於漂亮列印 TreesTypes 的公用程式。

列印樹狀結構

方法 show 顯示反射成品的「美化」表示法。此表示法提供 Scala 程式碼的去糖 Java 表示法。例如

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> def tree = reify { final class C { def x = 2 } }.tree
tree: scala.reflect.runtime.universe.Tree

scala> show(tree)
res0: String =
{
  final class C extends AnyRef {
    def <init>() = {
      super.<init>();
      ()
    };
    def x = 2
  };
  ()
}

方法 showRaw 顯示給定反射物件的內部結構,為 Scala 抽象語法樹 (AST),這是 Scala 型別檢查器運作的表示法。

請注意,儘管此表示形式似乎會產生正確的樹狀結構,您可能會認為可以在巨集實作中使用,但通常並非如此。符號並未完全表示(僅表示其名稱)。因此,此方法最適合用於在給定一些有效的 Scala 程式碼的情況下單純檢查 AST。

scala> showRaw(tree)
res1: String = Block(List(
  ClassDef(Modifiers(FINAL), TypeName("C"), List(), Template(
    List(Ident(TypeName("AnyRef"))),
    emptyValDef,
    List(
      DefDef(Modifiers(), termNames.CONSTRUCTOR, List(), List(List()), TypeTree(),
        Block(List(
          Apply(Select(Super(This(typeNames.EMPTY), typeNames.EMPTY), termNames.CONSTRUCTOR), List())),
          Literal(Constant(())))),
      DefDef(Modifiers(), TermName("x"), List(), List(), TypeTree(),
        Literal(Constant(2))))))),
  Literal(Constant(())))

方法 showRaw 也可以列印 scala.reflect.api.Types,並放在要檢查的工件旁邊。

scala> import scala.tools.reflect.ToolBox // requires scala-compiler.jar
import scala.tools.reflect.ToolBox

scala> import scala.reflect.runtime.{currentMirror => cm}
import scala.reflect.runtime.{currentMirror=>cm}

scala> showRaw(cm.mkToolBox().typeCheck(tree), printTypes = true)
res2: String = Block[1](List(
  ClassDef[2](Modifiers(FINAL), TypeName("C"), List(), Template[3](
    List(Ident[4](TypeName("AnyRef"))),
    emptyValDef,
    List(
      DefDef[2](Modifiers(), termNames.CONSTRUCTOR, List(), List(List()), TypeTree[3](),
        Block[1](List(
          Apply[4](Select[5](Super[6](This[3](TypeName("C")), typeNames.EMPTY), ...))),
          Literal[1](Constant(())))),
      DefDef[2](Modifiers(), TermName("x"), List(), List(), TypeTree[7](),
        Literal[8](Constant(2))))))),
  Literal[1](Constant(())))
[1] TypeRef(ThisType(scala), scala.Unit, List())
[2] NoType
[3] TypeRef(NoPrefix, TypeName("C"), List())
[4] TypeRef(ThisType(java.lang), java.lang.Object, List())
[5] MethodType(List(), TypeRef(ThisType(java.lang), java.lang.Object, List()))
[6] SuperType(ThisType(TypeName("C")), TypeRef(... java.lang.Object ...))
[7] TypeRef(ThisType(scala), scala.Int, List())
[8] ConstantType(Constant(2))

列印類型

方法 show 可用於產生類型的可讀取字串表示形式

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> def tpe = typeOf[{ def x: Int; val y: List[Int] }]
tpe: scala.reflect.runtime.universe.Type

scala> show(tpe)
res0: String = scala.AnyRef{def x: Int; val y: scala.List[Int]}

scala.reflect.api.Trees 的方法 showRaw 類似,scala.reflect.api.TypesshowRaw 提供了 Scala AST 的視覺化,由 Scala 型別檢查器操作。

scala> showRaw(tpe)
res1: String = RefinedType(
  List(TypeRef(ThisType(scala), TypeName("AnyRef"), List())),
  Scope(
    TermName("x"),
    TermName("y")))

showRaw 方法也有命名參數 printIdsprintKinds,兩者的預設引數皆為 false。當將 true 傳遞給這些參數時,showRaw 會另外顯示符號的唯一識別碼,以及它們的種類(套件、類型、方法、取得器等)。

scala> showRaw(tpe, printIds = true, printKinds = true)
res2: String = RefinedType(
  List(TypeRef(ThisType(scala#2043#PK), TypeName("AnyRef")#691#TPE, List())),
  Scope(
    TermName("x")#2540#METH,
    TermName("y")#2541#GET))

位置

位置(Position 特質的實例)用於追蹤符號和樹狀節點的來源。它們通常用於顯示警告和錯誤,以指出程式中不正確的點。位置會指出原始檔中的欄和行(從原始檔開頭的偏移量稱為其「點」,有時使用起來較不方便)。它們也會載入它們所參考的行內容。並非所有樹狀結構或符號都有位置;使用 NoPosition 物件指出遺失的位置。

位置可以只參考原始檔中的單一字元,或參考一個範圍。在後者的情況下,會使用範圍位置(不是範圍位置的位置也稱為偏移位置)。範圍位置額外有 startend 偏移量。可以使用 focusStartfocusEnd 方法「聚焦」於 startend 偏移量,這些方法會傳回位置(當呼叫在非範圍位置的位置上時,它們只會傳回 this)。

可以使用 precedes 等方法來比較位置,如果兩個位置都已定義(,位置不是 NoPosition),且 this 位置的終點不超過給定位置的起點,則此方法會成立。此外,可以測試範圍位置是否包含(使用 includes 方法)和重疊(使用 overlaps 方法)。

範圍位置不是透明就是不透明(非透明)。範圍位置是否不透明會影響其允許的使用方式,因為包含範圍位置的樹狀結構必須符合下列不變式

  • 具有偏移位置的樹狀結構絕不會包含具有範圍位置的子項
  • 如果具有範圍位置的樹狀結構的子項也具有範圍位置,則子項的範圍會包含在父項的範圍內。
  • 同一個節點的子項的不透明範圍位置不會重疊(表示它們的重疊最多只有一個點)。

使用 makeTransparent 方法,可以將不透明範圍位置轉換為透明範圍位置;所有其他位置則維持不變。

此頁面的貢獻者