此文件頁面專門針對 Scala 3,並可能涵蓋 Scala 2 中沒有的新概念。除非另有說明,否則此頁面中的所有程式碼範例假設您使用的是 Scala 3。
反射 API 提供了更複雜且全面的程式碼結構檢視。它提供了型別抽象語法樹及其屬性的檢視,例如型別、符號、位置和註解。
API 可用於巨集中,也可用于 檢查 TASTy 檔案。
如何使用 API
反射 API 在型別 Quotes
中定義為 reflect
。實際執行個體取決於目前的範圍,其中使用引號或引號模式配對。因此,每個巨集方法都會收到 Quotes 作為額外的引數。由於 Quotes
是上下文相關的,因此要存取其成員,我們需要命名參數或呼叫它。標準函式庫中的下列定義詳細說明了存取它的正規方法
package scala.quoted
transparent inline def quotes(using inline q: Quotes): q.type = q
我們可以使用 scala.quoted.quotes
來匯入範圍內目前的 Quotes
import scala.quoted.* // Import `quotes`, `Quotes`, and `Expr`
def f(x: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.* // Import `Tree`, `TypeRepr`, `Symbol`, `Position`, .....
val tree: Tree = ...
...
這將匯入 API 的所有類型和模組(包含擴充方法)。
如何瀏覽 API
完整的 API 可以從 scala.quoted.Quotes.reflectModule
的 API 文件 中找到。遺憾的是,在這個階段,這個自動產生的文件並不容易瀏覽。
頁面上最重要的元素是階層樹,它提供了 API 中類型的子類型關係的綜合概觀。對於樹中的每個類型 Foo
- 特徵
FooMethods
包含類型Foo
上可用的方法 - 特徵
FooModule
包含物件Foo
上可用的靜態方法。最值得注意的是,建構函式(apply/copy
)和unapply
方法,它提供了模式比對所需的萃取器 - 對於所有類型
Upper
,使得Foo <: Upper
,在UpperMethods
中定義的方法也可用於Foo
例如,TypeBounds
是 TypeRepr
的子類型,表示形式為 T >: L <: U
的類型樹:類型 T
是 L
的超類型,也是 U
的子類型。在 TypeBoundsMethods
中,您將找到方法 low
和 hi
,它們允許您存取 L
和 U
的表示。在 TypeBoundsModule
中,您將找到 unapply
方法,它允許您撰寫
def f(tpe: TypeRepr) =
tpe match
case TypeBounds(l, u) =>
因為 TypeBounds <: TypeRepr
,所以 TypeReprMethods
中定義的所有方法都可用於 TypeBounds
值
def f(tpe: TypeRepr) =
tpe match
case tpe: TypeBounds =>
val low = tpe.low
val hi = tpe.hi
與 Expr/Type 的關係
Expr 和 Term
表達式 (Expr[T]
) 可以視為 Term
的包裝器,其中 T
是該項目的靜態已知類型。以下,我們使用擴充方法 asTerm
將表達式轉換為項目。此擴充方法僅在匯入 quotes.reflect.asTerm
後才可用。接著我們使用 asExprOf[Int]
將項目轉換回 Expr[Int]
。如果項目的類型不符 (在此情況下為 Int
) 或項目不是有效的表達式,此操作將會失敗。例如,如果方法 fn
採用類型參數,則 Ident(fn)
會是無效的項目,在這種情況下我們需要 Apply(Ident(fn), args)
。
def f(x: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.*
val tree: Term = x.asTerm
val expr: Expr[Int] = tree.asExprOf[Int]
expr
類型和 TypeRepr
類似地,我們也可以將 Type[T]
視為 TypeRepr
的包裝器,其中 T
是靜態已知類型。若要取得 TypeRepr
,我們使用 TypeRepr.of[T]
,它預期在範圍內有指定的 Type[T]
(類似於 Type.of[T]
)。我們也可以使用 asType
方法將其轉換回 Type[?]
。由於 Type[?]
的類型並非靜態已知,因此我們需要使用存在類型為其命名才能使用它。這可以使用 '[t]
模式來達成。
def g[T: Type](using Quotes) =
import quotes.reflect.*
val tpe: TypeRepr = TypeRepr.of[T]
tpe.asType match
case '[t] => '{ val x: t = ${...} }
...
符號
就方法產生並接受其類型在 API 中定義的值這一點而言,Term
和 TypeRepr
的 API 相對「封閉」。不過,你可能會注意到有識別定義的 Symbol
。
無論是 Term
或 TypeRepr
(因此也包括 Expr
和 Type
),都有一個關聯的符號。透過使用 ==
,Symbol
可以比較兩個定義是否相同。此外,Symbol
會公開許多有用的方法,並由這些方法使用。例如
declaredFields
和declaredMethods
允許您在符號內定義的欄位和成員上進行反覆運算flags
允許您檢查符號的多個屬性companionClass
和companionModule
提供一種跳轉至伴隨物件/類別和從中跳出的方式TypeRepr.baseClasses
傳回由類別延伸的符號清單Symbol.pos
讓您可以存取定義符號的位置、定義的原始程式碼,甚至是定義符號的檔案名稱- 您可以在
SymbolMethods
中找到許多其他方法
轉換為 Symbol 和返回
考慮一個名為 val tpe: TypeRepr = ...
的 TypeRepr
類型的實例。然後
tpe.typeSymbol
傳回TypeRepr
所表示類型的符號。在給定Type[T]
的情況下取得Symbol
的建議方式為TypeRepr.of[T].typeSymbol
- 對於單例類型,
tpe.termSymbol
傳回基礎物件或值的符號 tpe.memberType(symbol)
傳回所提供符號的TypeRepr
- 在物件
t: Tree
上,t.symbol
傳回與樹狀結構相關聯的符號。由於Term <: Tree
,Expr.asTerm.symbol
是取得與Expr[T]
相關聯的符號的最佳方式 - 在物件
sym: Symbol
上,sym.tree
傳回與符號相關聯的Tree
。使用此方法時請小心,因為符號的樹狀結構可能未定義。請閱讀 最佳實務頁面 以取得更多資訊
巨集 API 設計
建立執行巨集一些常見邏輯的輔助方法或萃取器通常會很有用。
最簡單的方法是僅在簽章中提及 Expr
、Type
和 Quotes
。在內部,它們可能會使用反射,但這不會在方法的使用位置看到。
def f(x: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.*
...
在某些情況下,某些方法可能會預期或傳回 Tree
或 quotes.reflect
中的其他類型,這是不可避免的。對於這些情況,最佳做法是遵循下列方法簽章範例
採用 quotes.reflect.Term
參數的方法
def f(using Quotes)(term: quotes.reflect.Term): String =
import quotes.reflect.*
...
傳回 quotes.reflect.Tree
的 quotes.reflect.Term
延伸方法
extension (using Quotes)(term: quotes.reflect.Term)
def g: quotes.reflect.Tree = ...
與 quotes.reflect.Term
相符的萃取器
object MyExtractor:
def unapply(using Quotes)(x: quotes.reflect.Term) =
...
Some(y)
避免將
Quotes
內容儲存在欄位中。欄位中的Quotes
必然會因為造成涉及具有不同路徑的Quotes
的錯誤而讓其使用更為困難。通常,這些模式已在使用 Scala 2 定義延伸方法或情境式 unapply 的程式碼中看到。現在我們有可在其他參數之前加入的
given
參數,所有這些舊的解決方法都不再需要。新的抽象化讓定義位置和使用位置都更為簡單。
偵錯
執行時期檢查
表達式 (Expr[T]
) 可視為 Term
的包裝器,其中 T
是該項目的靜態已知類型。因此,這些檢查將在執行時期執行 (即巨集展開時的編譯時期)。
建議在開發巨集或巨集測試時啟用 -Xcheck-macros
旗標。此旗標將啟用額外的執行時期檢查,這些檢查會嘗試在建立樹狀結構或類型時立即找出格式不正確的樹狀結構或類型。
還有 -Ycheck:all
旗標,它會檢查樹狀結構格式正確性的所有編譯器不變式。這些檢查通常會因斷言錯誤而失敗。
列印樹狀結構
toString
方法在 quotes.reflect
套件中的類型中並非用於除錯,因為它們顯示的是內部表示,而非 quotes.reflect
表示。在許多情況下,這些表示是相似的,但有時它們可能會誤導除錯程序,因此不應依賴它們。
相反地,quotes.reflect.Printers
提供了一組有用的印表機,用於除錯。特別是 TreeStructure
、TypeReprStructure
和 ConstantStructure
類別可能非常有用。這些類別將印出樹狀結構,大致遵循匹配它所需的萃取器。
val tree: Tree = ...
println(tree.show(using Printer.TreeStructure))
可以新增此功能的最有用地方之一是在 Tree
上的樣式比對結束時。
tree match
case Ident(_) =>
case Select(_, _) =>
...
case _ =>
throw new MatchError(tree.show(using Printer.TreeStructure))
這樣一來,如果錯過某個情況,錯誤將會報告一個熟悉的結構,可以複製貼上以開始修正問題。
如果需要,您可以將此印表機設為預設值
import quotes.reflect.*
given Printer[Tree] = Printer.TreeStructure
...
println(tree.show)
更多
即將推出