反射

符號、樹狀結構和類型

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

實驗性

符號

符號用於在名稱和它所指的實體(例如類別或方法)之間建立繫結。您在 Scala 中定義且可以給予名稱的任何東西都有關聯的符號。

符號包含實體宣告(class/object/trait 等)或成員(val/var/def 等)的所有可用資訊,因此是執行時期反射和編譯時期反射(巨集)的核心抽象。

符號可以提供豐富的資訊,從所有符號上可用的基本 name 方法,到其他更複雜的概念,例如從 ClassSymbol 取得 baseClasses。其他符號的常見使用案例包括檢查成員簽章、取得類別的類型參數、取得方法的參數類型,或找出欄位的類型。

符號擁有者階層

符號以階層方式組織。例如,表示方法參數的符號由對應的方法符號擁有,方法符號由其封裝的類別、特徵或物件擁有,類別由包含的套件擁有,以此類推。

如果符號沒有擁有者,例如,因為它參照頂層實體,例如頂層套件,則其擁有者是特殊 NoSymbol 單例物件。表示遺失符號的 NoSymbol 通常在 API 中用於表示空值或預設值。存取 NoSymbolowner 會擲回例外。請參閱 API 文件,以取得類型 Symbol 提供的一般介面

TypeSymbol

TypeSymbol 表示類型、類別和特徵宣告,以及類型參數。不適用於更具體的 ClassSymbol 的有趣成員包括 isAbstractTypeisContravariantisCovariant

  • ClassSymbol:提供存取類別或特質宣告中所含所有資訊,例如 name、修飾詞 (isFinalisPrivateisProtectedisAbstractClass 等)、baseClassestypeParams

TermSymbol

代表 val、var、def 和 object 宣告以及套件和值參數的術語符號類型。

  • MethodSymbol:代表 def 宣告的 method 符號類型(TermSymbol 的子類別)。它支援查詢,例如檢查 method 是否為(主要)建構函式,或 method 是否支援可變長度引數清單。
  • ModuleSymbol:代表 object 宣告的 module 符號類型。它允許透過成員 moduleClass 查詢與 object 定義隱式關聯的類別。反向查詢也是可行的。可以從 module 類別透過檢查其 selfType.termSymbol 回到關聯的 module 符號。

符號轉換

在某些情況下,可能會使用回傳一般 Symbol 類別實例的方法。在這些情況下,可以將取得的較一般 Symbol 類別轉換為所需的特定、更專業的符號類別。

符號轉換,例如 asClassasMethod,用於轉換為 Symbol 的更特定子類型(例如,如果您想要使用 MethodSymbol 介面)。

例如,

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

scala> class C[T] { def test[U](x: T)(y: U): Int = ??? }
defined class C

scala> val testMember = typeOf[C[Int]].member(TermName("test"))
testMember: scala.reflect.runtime.universe.Symbol = method test

在這種情況下,member 會傳回 Symbol 的執行個體,而不是預期中的 MethodSymbol。因此,我們必須使用 asMethod 來確保我們取得 MethodSymbol

scala> testMember.asMethod
res0: scala.reflect.runtime.universe.MethodSymbol = method test

自由符號

兩種符號類型 FreeTermSymbolFreeTypeSymbol 具有特殊狀態,因為它們指的是可用資訊不完整的符號。這些符號在實體化過程中某些情況下產生(請參閱關於實體化樹的對應區段以取得更多背景資訊)。每當實體化無法找到符號(表示符號在對應的類別檔案中不可用,例如,因為符號指的是區域類別),它會將其實體化為所謂的「自由類型」,一種記住原始名稱和擁有者並具有緊密遵循原始簽名的代理類型簽名的合成虛擬符號。您可以透過呼叫 sym.isFreeType 來檢查符號是否為自由類型。您也可以透過呼叫 tree.freeTypes 來取得樹及其子項所引用的所有自由類型的清單。最後,您可以透過使用 -Xlog-free-types 在實體化產生自由類型時取得警告。

類型

如同其名稱所暗示的,Type 的實例表示對應符號類型相關資訊。這包含其成員(方法、欄位、類型別名、抽象類型、巢狀類別、特質等),不論是直接宣告或繼承而來,其基礎類型、其擦除等等。類型也提供用於測試類型相符性或等價性的運算。

建立類型實例

一般來說,有三個方法可以建立 Type 實例。

  1. 透過 scala.reflect.api.TypeTags 上的 typeOf 方法,它會混入 Universe(最簡單且最常見)。
  2. 標準類型,例如 IntBooleanAnyUnit 可透過可用 universe 存取。
  3. 使用工廠方法建立實例,例如 scala.reflect.api.Types 上的 typeRefpolyType(不建議)。

使用 typeOf 建立類型實例

要建立類型實例,大多數時候可以使用 scala.reflect.api.TypeTags#typeOf 方法。它會接收類型引數並產生表示該引數的 Type 實例。例如

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

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

在此範例中,會傳回 scala.reflect.api.Types$TypeRef,它對應於類型建構函式 List,套用至類型引數 Int

不過,請注意此方法需要手動指定我們嘗試建立實例的類型。如果我們有興趣取得對應於某個任意實例的 Type 實例,該怎麼辦?我們可以簡單定義一個具有類型參數上下文繫結的方法,這會為我們產生一個專門的 TypeTag,我們可以使用它來取得任意實例的類型

scala> def getType[T: TypeTag](obj: T) = typeOf[T]
getType: [T](obj: T)(implicit evidence$1: scala.reflect.runtime.universe.TypeTag[T])scala.reflect.runtime.universe.Type

scala> getType(List(1,2,3))
res1: scala.reflect.runtime.universe.Type = List[Int]

scala> class Animal; class Cat extends Animal
defined class Animal
defined class Cat

scala> val a = new Animal
a: Animal = Animal@21c17f5a

scala> getType(a)
res2: scala.reflect.runtime.universe.Type = Animal

scala> val c = new Cat
c: Cat = Cat@2302d72d

scala> getType(c)
res3: scala.reflect.runtime.universe.Type = Cat

注意:方法 typeOf 不適用於具有類型參數的類型,例如 typeOf[List[A]],其中 A 是類型參數。在此情況下,可以使用 scala.reflect.api.TypeTags#weakTypeOf。有關更多詳細資訊,請參閱本指南的 TypeTags 部分。

標準類型

標準類型,例如 IntBooleanAnyUnit,可透過 universe 的 definitions 成員存取。例如

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

scala> val intTpe = universe.definitions.IntTpe
intTpe: scala.reflect.runtime.universe.Type = Int

標準類型清單在 scala.reflect.api.StandardDefinitions 中的 StandardTypes 特質中指定。

類型上的常見操作

類型通常用於類型符合性測試或查詢成員。在類型上執行的三大類操作為

  1. 檢查兩個類型之間的子類型關係。
  2. 檢查兩個類型之間的相等性。
  3. 查詢給定類型以取得特定成員或內部類型。

子類型關係

給定兩個 Type 執行個體,可以使用 <:<(在特殊情況下,使用 weak_<:<,說明如下)輕鬆測試一個是否為另一個的子類型

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

scala> class A; class B extends A
defined class A
defined class B

scala> typeOf[A] <:< typeOf[B]
res0: Boolean = false

scala> typeOf[B] <:< typeOf[A]
res1: Boolean = true

請注意,方法 weak_<:< 存在於檢查兩個類型之間的弱符合性。在處理數字類型時,這通常很重要。

Scala 的數字類型遵循下列順序(Scala 語言規範的第 3.5.3 節)

在某些情況下,Scala 會使用更通用的符合性關係。如果 S<:T 或 S 和 T 都是基本數字類型,且 S 在下列順序中出現在 T 之前,則類型 S 弱符合類型 T,寫為 S <:w T

弱符合性關係
Byte <:w Short
Short <:w Int
Char <:w Int
Int <:w Long
Long <:w Float
Float <:w Double

例如,弱相容性用於判斷以下 if 表達式的類型

scala> if (true) 1 else 1d
res2: Double = 1.0

在上面顯示的 if 表達式中,結果類型定義為兩個類型的弱最小上界(也就是,相對於弱相容性的最小上界)。

因此,由於 Double 定義為 IntDouble 之間相對於弱相容性的最小上界(根據上面顯示的規格),Double 推論為我們範例 if 表達式的類型。

請注意方法 weak_<:< 檢查弱相容性(相對於 <:<,它檢查相容性而不考慮規格第 3.5.3 節中的弱相容性關係),因此在檢查數字類型 IntDouble 之間的相容性關係時,會傳回正確的結果

scala> typeOf[Int] weak_<:< typeOf[Double]
res3: Boolean = true

scala> typeOf[Double] weak_<:< typeOf[Int]
res4: Boolean = false

而使用 <:< 會錯誤地報告 IntDouble 在任何方面都不相容

scala> typeOf[Int] <:< typeOf[Double]
res5: Boolean = false

scala> typeOf[Double] <:< typeOf[Int]
res6: Boolean = false

類型相等性

類似於類型符合性,可以輕鬆檢查兩個類型的相等性。也就是說,給定兩個任意類型,可以使用方法 =:= 來查看是否都表示完全相同的編譯時類型。

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

scala> def getType[T: TypeTag](obj: T) = typeOf[T]
getType: [T](obj: T)(implicit evidence$1: scala.reflect.runtime.universe.TypeTag[T])scala.reflect.runtime.universe.Type

scala> class A
defined class A

scala> val a1 = new A; val a2 = new A
a1: A = A@cddb2e7
a2: A = A@2f0c624a

scala> getType(a1) =:= getType(a2)
res0: Boolean = true

請注意,兩個實例的精確類型資訊必須相同。例如,在以下程式碼片段中,我們有兩個具有不同類型引數的 List 實例。

scala> getType(List(1,2,3)) =:= getType(List(1.0, 2.0, 3.0))
res1: Boolean = false

scala> getType(List(1,2,3)) =:= getType(List(9,8,7))
res2: Boolean = true

還要特別注意,=:=始終用於比較類型的相等性。也就是說,切勿使用 ==,因為它無法在存在類型別名的情況下檢查類型相等性,而 =:= 可以

scala> type Histogram = List[Int]
defined type alias Histogram

scala> typeOf[Histogram] =:= getType(List(4,5,6))
res3: Boolean = true

scala> typeOf[Histogram] == getType(List(4,5,6))
res4: Boolean = false

正如我們所見,== 錯誤地報告 HistogramList[Int] 具有不同的類型。

查詢類型的成員和宣告

給定一個 Type,也可以查詢特定成員或宣告。 Type成員包括所有欄位、方法、類型別名、抽象類型、巢狀類別/物件/特質等。 Type宣告僅為在給定 Type 所代表的類別/特質/物件定義中宣告(未繼承)的成員。

若要取得特定成員或宣告的 Symbol,只需使用提供與該類型關聯的定義清單的方法 membersdecls 即可。每個方法也有單數對應項,分別為方法 memberdecl。所有四個方法的簽章如下所示

/** The member with given name, either directly declared or inherited, an
  * OverloadedSymbol if several exist, NoSymbol if none exist. */
def member(name: Universe.Name): Universe.Symbol

/** The defined or declared members with name name in this type; an
  * OverloadedSymbol if several exist, NoSymbol if none exist. */
def decl(name: Universe.Name): Universe.Symbol

/** A Scope containing all members of this type
  * (directly declared or inherited). */
def members: Universe.MemberScope // MemberScope is a type of
                                  // Traversable, use higher-order
                                  // functions such as map,
                                  // filter, foreach to query!

/** A Scope containing the members declared directly on this type. */
def decls: Universe.MemberScope // MemberScope is a type of
                                       // Traversable, use higher-order
                                       // functions such as map,
                                       // filter, foreach to query!

例如,要查詢 Listmap 方法,可以執行

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

scala> typeOf[List[_]].member(TermName("map"))
res0: scala.reflect.runtime.universe.Symbol = method map

請注意,我們傳遞方法 member 一個 TermName,因為我們正在查詢一個方法。如果我們要查詢一個類型成員,例如 List 的自我類型 Self,我們會傳遞一個 TypeName

scala> typeOf[List[_]].member(TypeName("Self"))
res1: scala.reflect.runtime.universe.Symbol = type Self

我們也可以用有趣的方式查詢類型上的所有成員或宣告。我們可以使用方法 members 取得一個 Traversable (MemberScopeApi 延伸 Traversable) 的 Symbol,代表給定類型上所有繼承或宣告的成員,這表示我們可以使用集合上的常見高階函數,例如 foreachfiltermap 等,來探索我們的類型成員。例如,要列印 List 中私有的成員,只需執行

scala> typeOf[List[Int]].members.filter(_.isPrivate).foreach(println _)
method super$sameElements
method occCounts
class CombinationsItr
class PermutationsItr
method sequential
method iterateUntilEmpty

樹狀結構

樹狀結構是 Scala 抽象語法的基礎,用於表示程式。它們也稱為抽象語法樹,通常縮寫為 AST。

在 Scala 反射中,產生或使用樹狀結構的 API 如下

  1. Scala 注解,使用樹狀結構表示其引數,顯示在 Annotation.scalaArgs 中(更多資訊,請參閱本指南的 註解 部分)。
  2. reify,一個特殊方法,它會取得一個表達式並傳回一個表示此表達式的 AST。
  3. 使用巨集的編譯時期反射(在巨集指南中有概述)和使用工具箱的執行時期編譯都使用樹作為其程式表示媒介。

請務必注意,樹是不可變的,但有三個欄位例外:pos (位置)、symbol (符號) 和 tpe (類型),這些欄位會在樹進行類型檢查時指派。

Tree 的種類

樹有三大類

  1. TermTree 的子類別,用於表示項,例如,方法呼叫會以 Apply 節點表示,物件實例化會使用 New 節點來達成,等等。
  2. TypTree 的子類別,用於表示程式碼來源中明確指定的類型,例如List[Int] 會解析為 AppliedTypeTree注意TypTree 沒有拼錯,而且在概念上也不同於 TypeTreeTypeTree 是不同的東西。也就是說,在編譯器建構 Type 的情況下(例如,在類型推論期間),它們可以包裝在 TypeTree 樹中,並整合到程式的 AST 中。
  3. SymTree 的子類別,用於引入或參照定義。引入新定義的範例包括表示類別和特徵定義的 ClassDef,或表示欄位和參數定義的 ValDef。參照現有定義的範例包括參照目前範圍內現有定義的 Ident,例如局部變數或方法。

任何其他類型的人們可能會遇到的樹通常是語法或短暫的建構。例如,包裝個別比對案例的 CaseDef;此類節點既不是術語也不是類型,而且不會帶有符號。

檢查樹

Scala Reflection 提供了許多視覺化樹的方法,所有方法都透過 universe 提供。給定一棵樹,人們可以

  • 使用 showtoString 方法,這些方法會列印樹表示的偽 Scala 程式碼。
  • 使用 showRaw 方法,以查看類型檢查器運作的原始內部樹。

例如,給定以下樹

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

scala> val tree = Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2))))
tree: scala.reflect.runtime.universe.Apply = x.$plus(2)

我們可以使用 show 方法(或等效的 toString)來查看該樹表示的內容。

scala> show(tree)
res0: String = x.$plus(2)

正如我們所見,tree 僅將 2 加到術語 x

我們也可以反過來進行。給定一些 Scala 表達式,我們可以先取得一個樹狀結構,然後使用 showRaw 方法來查看編譯器和類型檢查器運作的原始內部樹狀結構。例如,給定表達式

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

scala> val expr = reify { class Flower { def name = "Rose" } }
expr: scala.reflect.runtime.universe.Expr[Unit] = ...

這裡,reify 僅取得傳遞給它的 Scala 表達式,並傳回一個 Scala Expr,它只會包裝一個 Tree 和一個 TypeTag(請參閱本指南的 Expr 區段,以取得有關 Expr 的更多資訊)。我們可以透過以下方式取得 expr 所包含的樹狀結構

scala> val tree = expr.tree
tree: scala.reflect.runtime.universe.Tree =
{
  class Flower extends AnyRef {
    def <init>() = {
      super.<init>();
      ()
    };
    def name = "Rose"
  };
  ()
}

我們可以透過簡單執行以下動作來檢查原始樹狀結構

scala> showRaw(tree)
res1: String = Block(List(ClassDef(Modifiers(), TypeName("Flower"), 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("name"), List(), List(), TypeTree(), Literal(Constant("Rose"))))))), Literal(Constant(())))

遍歷樹狀結構

在了解給定樹狀結構的結構後,通常下一步是從中擷取資訊。這可透過遍歷樹狀結構來完成,而且有兩種方法可以執行此操作

  • 透過樣式比對進行遍歷。
  • 使用 Traverser 的子類別

透過樣式比對進行遍歷

透過樣式比對進行遍歷是最簡單且最常見的樹狀結構遍歷方式。通常,當對單一節點的給定樹狀結構狀態感興趣時,會透過樣式比對來遍歷樹狀結構。例如,假設我們只想取得下列樹狀結構中唯一 Apply 節點的函式和引數

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

scala> val tree = Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2))))
tree: scala.reflect.runtime.universe.Apply = x.$plus(2)

我們可以簡單地對我們的 tree 進行比對,如果我們有一個 Apply 節點,只要傳回 Apply 的函式和引數即可

scala> val (fun, arg) = tree match {
     |     case Apply(fn, a :: Nil) => (fn, a)
     | }
fun: scala.reflect.runtime.universe.Tree = x.$plus
arg: scala.reflect.runtime.universe.Tree = 2

我們可以透過將樣式比對放在左側,來更簡潔地達成完全相同的效果

scala> val Apply(fun, arg :: Nil) = tree
fun: scala.reflect.runtime.universe.Tree = x.$plus
arg: scala.reflect.runtime.universe.Tree = 2

請注意,Tree 通常可以非常複雜,節點可以任意深度嵌套在其他節點中。一個簡單的說明是,如果我們要將第二個 Apply 節點新增到上述樹狀結構中,它用於將 3 新增到我們的總和中

scala> val tree = Apply(Select(Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2)))), TermName("$plus")), List(Literal(Constant(3))))
tree: scala.reflect.runtime.universe.Apply = x.$plus(2).$plus(3)

如果我們套用與上述相同的模式比對,我們會取得外部 Apply 節點,其函式包含表示 x.$plus(2) 的完整樹狀結構,我們在上面看過

scala> val Apply(fun, arg :: Nil) = tree
fun: scala.reflect.runtime.universe.Tree = x.$plus(2).$plus
arg: scala.reflect.runtime.universe.Tree = 3

scala> showRaw(fun)
res3: String = Select(Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2)))), TermName("$plus"))

在必須執行更豐富的任務時,例如在特定節點停止前遍歷整個樹狀結構,或收集並檢查特定類型的所有節點,使用 Traverser 進行遍歷可能會更有利。

透過 Traverser 進行遍歷

在需要從上到下遍歷整個樹狀結構的情況下,透過模式比對進行遍歷將不可行 - 要這樣做,必須個別處理我們在模式比對中可能遇到的每種類型的節點。因此,在這些情況下,通常會使用類別 Traverser

Traverser 會確保深度優先搜尋中拜訪給定樹狀結構中的每個節點。

若要使用 Traverser,只需建立 Traverser 的子類別,並覆寫方法 traverse。這樣做時,您可以僅提供自訂邏輯來處理您有興趣的案例。例如,如果針對前一節中的 x.$plus(2).$plus(3) 樹狀結構,我們想要收集所有 Apply 節點,我們可以這樣做

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

scala> val tree = Apply(Select(Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2)))), TermName("$plus")), List(Literal(Constant(3))))
tree: scala.reflect.runtime.universe.Apply = x.$plus(2).$plus(3)

scala> object traverser extends Traverser {
     |   var applies = List[Apply]()
     |   override def traverse(tree: Tree): Unit = tree match {
     |     case app @ Apply(fun, args) =>
     |       applies = app :: applies
     |       super.traverse(fun)
     |       super.traverseTrees(args)
     |     case _ => super.traverse(tree)
     |   }
     | }
defined module traverser

在上述範例中,我們打算建立一個 Apply 節點清單,我們在給定的樹狀結構中找到這些節點。

我們透過實際上新增一個特殊案例到超類別 Traverser 中已定義的深度優先 traverse 方法來達成這個目標,透過子類別 traverser 的覆寫 traverse 方法。我們的特殊案例只影響符合 Apply(fun, args) 樣式的節點,其中 fun 是某個函數(由 Tree 表示),而 args 是引數清單(由 Tree 清單表示)。

當樹符合樣式(例如,當我們有一個 Apply 節點時),我們會將它新增到我們的 List[Apply]applies 中,並繼續我們的遍歷。

請注意,在我們的配對中,我們在 Apply 中封裝的函式 fun 上呼叫 super.traverse,而我們在我們的引數清單 args 上呼叫 super.traverseTrees(基本上與 super.traverse 相同,但適用於 List[Tree],而不是單一的 Tree)。在這些呼叫中,我們的目標很簡單——我們希望確保我們在 Traverser 中使用預設的 traverse 方法,因為我們不知道表示 fun 的 Tree 是否包含我們的 Apply 模式——也就是說,我們希望遍歷整個子樹。由於 Traverser 超類別呼叫 this.traverse,傳入每個巢狀子樹,最終我們的自訂 traverse 方法保證會針對與我們的 Apply 模式相符的每個子樹呼叫。

若要觸發 traverse 並查看相符的 Apply 節點的結果 List,只需執行

scala> traverser.traverse(tree)

scala> traverser.applies
res0: List[scala.reflect.runtime.universe.Apply] = List(x.$plus(2), x.$plus(2).$plus(3))

建立樹狀結構

在使用執行時期反射時,不需要手動建立樹狀結構。但是,使用工具箱的執行時期編譯和使用巨集的編譯時期反射都使用樹狀結構作為其程式表示媒介。在這些情況下,有三個建議的建立樹狀結構方法

  1. 透過方法 reify(應盡可能優先使用)。
  2. 透過 ToolBoxes 上的方法 parse
  3. 手動建構(不建議使用)。

透過 reify 建立樹狀結構

方法 reify 僅將 Scala 表達式作為引數,並產生該引數的類型化 Tree 表示形式作為結果。

透過方法 reify 建立樹狀結構是 Scala Reflection 中建立樹狀結構的建議方法。讓我們從一個小範例開始了解原因

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

scala> { val tree = reify(println(2)).tree; showRaw(tree) }
res0: String = Apply(Select(Select(This(TypeName("scala")), TermName("Predef")), TermName("println")), List(Literal(Constant(2))))

在這裡,我們僅對呼叫 println(2) 執行 reify,也就是說,我們將表達式 println(2) 轉換為其對應的樹狀結構表示形式。然後,我們輸出原始樹狀結構。請注意,println 方法已轉換為 scala.Predef.println。此類轉換可確保不論 reify 的結果用於何處,它都不會意外變更其意義。例如,即使這個 println(2) 片段稍後插入定義其自己的 println 的程式碼區塊中,它也不會影響該片段的行為。

因此,這種建立樹狀結構的方式是衛生的,因為它保留識別碼的繫結。

拼接樹狀結構

使用 reify 也允許使用者從較小的樹狀結構組成樹狀結構。這是透過 Expr.splice 執行的。

注意: Exprreify 的回傳類型。它可以視為一個簡單的包裝器,包含一個類型化Tree、一個 TypeTag,以及一些與具象化相關的方法,例如 splice。有關 Expr 的更多資訊,請參閱 本指南中的相關部分

例如,讓我們嘗試使用 splice 建立一個代表 println(2) 的樹

scala> val x = reify(2)
x: scala.reflect.runtime.universe.Expr[Int(2)] = Expr[Int(2)](2)

scala> reify(println(x.splice))
res1: scala.reflect.runtime.universe.Expr[Unit] = Expr[Unit](scala.this.Predef.println(2))

在此,我們分別 reify 2println,然後簡單地將一個 splice 到另一個。

不過,請注意,reify 的參數必須是有效的且可輸入 Scala 程式碼。如果我們想要抽象 println 本身,而不是 println 的參數,那是不可能的

scala> val fn = reify(println)
fn: scala.reflect.runtime.universe.Expr[Unit] = Expr[Unit](scala.this.Predef.println())

scala> reify(fn.splice(2))
<console>:12: error: Unit does not take parameters
            reify(fn.splice(2))
                            ^

正如我們所見,編譯器假設我們想要具象化一個沒有參數的 println 呼叫,而我們實際上想要擷取要呼叫的函數名稱。

當使用 reify 時,目前無法表達這些類型的使用案例。

透過 ToolBox 上的 parse 建立樹

工具箱可用於類型檢查、編譯和執行抽象語法樹。工具箱也可可用於將字串解析為 AST。

注意:使用工具箱需要在類別路徑中包含 scala-compiler.jar

讓我們看看 parse 如何處理上一節中的 println 範例

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

scala> import scala.tools.reflect.ToolBox
import scala.tools.reflect.ToolBox

scala> val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()
tb: scala.tools.reflect.ToolBox[scala.reflect.runtime.universe.type] = scala.tools.reflect.ToolBoxFactory$ToolBoxImpl@7bc979dd

scala> showRaw(tb.parse("println(2)"))
res2: String = Apply(Ident(TermName("println")), List(Literal(Constant(2))))

重要的是要注意,與 reify 不同,工具箱不受類型化需求的限制 - 儘管這種靈活性是透過犧牲健全性來達成的。也就是說,我們可以看到 parsereify 不同,它不會反映 println 應該繫結到標準 println 方法的事實。

注意:在使用巨集時,不應該使用 ToolBox.parse。這是因為巨集內容中已經內建 parse 方法。例如

bash$ scala -Yrepl-class-based:false

scala> import scala.language.experimental.macros
import scala.language.experimental.macros

scala> def impl(c: scala.reflect.macros.whitebox.Context) = c.Expr[Unit](c.parse("println(2)"))
def impl(c: scala.reflect.macros.whitebox.Context): c.Expr[Unit]

scala> def test: Unit = macro impl
def test: Unit

scala> test
2

您可以在 這篇巨集文章 中找到更多關於兩個 Context 的資訊。

使用工具箱進行類型檢查

正如前面提到的,工具箱不僅能從字串建構樹。它們也可可用於類型檢查、編譯和執行樹。

除了概述程式的結構外,樹狀結構還包含程式語意中編碼在 symbol(指派給引入或參考定義的樹狀結構的符號)和 tpe(樹狀結構的類型)中的重要資訊。預設情況下,這些欄位為空,但類型檢查會填入這些欄位。

使用執行時期反射架構時,類型檢查由 ToolBox.typeCheck 實作。使用巨集時,編譯時可以使用 Context.typeCheck 方法。

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

scala> val tree = reify { "test".length }.tree
tree: scala.reflect.runtime.universe.Tree = "test".length()

scala> import scala.tools.reflect.ToolBox
import scala.tools.reflect.ToolBox

scala> val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()
tb: scala.tools.reflect.ToolBox[scala.reflect.runtime.universe.type] = ...

scala> val ttree = tb.typeCheck(tree)
ttree: tb.u.Tree = "test".length()

scala> ttree.tpe
res5: tb.u.Type = Int

scala> ttree.symbol
res6: tb.u.Symbol = method length

在此,我們僅建立一個代表呼叫 "test".length 的樹狀結構,並使用 ToolBox tbtypeCheck 方法對樹狀結構進行類型檢查。正如我們所見,ttree 取得正確的類型 Int,且其 Symbol 已正確設定。

透過手動建構建立樹狀結構

如果其他方法都失敗,可以手動建構樹狀結構。這是建立樹狀結構的最低階層方法,且僅應在其他方法都無法運作時嘗試使用。與 parse 相較,它有時提供更大的彈性,但這種彈性是以過度冗長且脆弱為代價。

我們先前的範例包含 println(2),可以手動建構如下

scala> Apply(Ident(TermName("println")), List(Literal(Constant(2))))
res0: scala.reflect.runtime.universe.Apply = println(2)

此技術的標準使用案例是在目標樹需要從動態建立的部分組裝時,這些部分彼此獨立時沒有意義。在這種情況下,reify 很可能不適用,因為它要求其參數可輸入。 parse 可能也不適用,因為通常樹是在子表達式層級組裝,而個別部分無法表示為 Scala 來源。

此頁面的貢獻者