反射

概觀

語言
此文件頁面專門針對 Scala 2 中發布的功能,這些功能已在 Scala 3 中移除或已由其他功能取代。除非另有說明,此頁面中的所有程式碼範例都假設您使用 Scala 2。

實驗性

Heather Miller、Eugene Burmako、Philipp Haller

反射是程式檢查甚至修改自己的能力。它在物件導向、函數式和邏輯程式設計範例中擁有悠久的歷史。雖然有些語言是以反射作為指導原則而建構的,但許多語言會隨著時間推移而逐步發展其反射能力。

反射涉及具體化(即明確化)程式中其他隱含元素的能力。這些元素可以是靜態程式元素,例如類別、方法或表達式,或動態元素,例如目前延續或執行事件,例如方法呼叫和欄位存取。通常會根據執行反射程序的時間區分編譯時期和執行時期反射。編譯時期反射是開發程式轉換器和產生器的強大方式,而執行時期反射通常用於調整語言語意或支援軟體元件之間非常後期繫結。

在 2.10 之前,Scala 沒有任何自己的反射功能。相反地,可以利用 Java 反射 API 的一部分,也就是處理提供動態檢查類別和物件以及存取其成員的能力。然而,許多 Scala 特定元素在獨立的 Java 反射下無法復原,它只公開 Java 元素(沒有函數、沒有特質)和類型(沒有存在、高階、路徑相依和抽象類型)。此外,Java 反射也無法復原編譯時期為泛型的 Java 類型的執行時期類型資訊;這項限制會傳遞到 Scala 中泛型類型的執行時期反射。

在 Scala 2.10 中,引進了一個新的反射函式庫,不僅要解決 Java 的執行時期反射在 Scala 特定和泛型類型上的缺點,還要新增更強大的通用反射功能工具組到 Scala。除了針對 Scala 類型和泛型的全功能執行時期反射之外,Scala 2.10 也隨附編譯時期反射功能,以巨集的形式,以及將 Scala 表達式具體化成抽象語法樹的能力。

執行時期反射

什麼是執行時期反射?給定某個物件在執行時期的類型或實例,反射就是能夠

  • 檢查該物件的類型,包括泛型類型,
  • 實例化新物件,
  • 或存取或呼叫該物件的成員。

讓我們開始,看看如何透過一些範例來執行上述各項操作。

範例

檢查執行時期類型(包括執行時期的泛型類型)

與其他 JVM 語言一樣,Scala 的類型會在編譯時期清除。這表示如果你要檢查某個實例的執行時期類型,你可能無法存取 Scala 編譯器在編譯時期可用的所有類型資訊。

TypeTag 可以視為在編譯時期攜帶所有類型資訊到執行時期的物件。不過,請務必注意 TypeTag 始終是由編譯器產生的。只要使用需要 TypeTag 的隱式參數或內容限制,就會觸發此產生。這表示通常只能使用隱式參數或內容限制取得 TypeTag

例如,使用內容限制

scala> import scala.reflect.runtime.{universe => ru}
import scala.reflect.runtime.{universe=>ru}

scala> val l = List(1,2,3)
l: List[Int] = List(1, 2, 3)

scala> def getTypeTag[T: ru.TypeTag](obj: T) = ru.typeTag[T]
getTypeTag: [T](obj: T)(implicit evidence$1: ru.TypeTag[T])ru.TypeTag[T]

scala> val theType = getTypeTag(l).tpe
theType: ru.Type = List[Int]

在上述範例中,我們首先匯入 scala.reflect.runtime.universe(必須匯入才能使用 TypeTag),並建立一個名為 lList[Int]。然後,我們定義一個方法 getTypeTag,它有一個類型參數 T,其中包含內容限制(REPL 顯示,這等於定義一個隱式的「證據」參數,這會導致編譯器為 T 產生一個 TypeTag)。最後,我們以 l 為其參數呼叫我們的 method,並呼叫 tpe,它會傳回包含在 TypeTag 中的類型。正如我們所見,我們取得正確的完整類型(包括 List 的具體類型引數),List[Int]

一旦我們取得所需的 Type 執行個體,我們可以檢查它,例如。

scala> val decls = theType.decls.take(10)
decls: Iterable[ru.Symbol] = List(constructor List, method companion, method isEmpty, method head, method tail, method ::, method :::, method reverse_:::, method mapConserve, method ++)

在執行階段實例化 Type

透過反射取得的 Type 可以使用適當的「呼叫者」鏡像 (鏡像在 下方 有更進一步的說明) 來呼叫其建構函數,進而實例化。讓我們使用 REPL 逐步示範一個範例

scala> case class Person(name: String)
defined class Person

scala> val m = ru.runtimeMirror(getClass.getClassLoader)
m: scala.reflect.runtime.universe.Mirror = JavaMirror with ...

在第一步中,我們取得一個鏡像 m,它讓目前類別載入器載入的所有類別和類型都可用,包括類別 Person

scala> val classPerson = ru.typeOf[Person].typeSymbol.asClass
classPerson: scala.reflect.runtime.universe.ClassSymbol = class Person

scala> val cm = m.reflectClass(classPerson)
cm: scala.reflect.runtime.universe.ClassMirror = class mirror for Person (bound to null)

第二步包括使用 reflectClass 方法,取得類別 PersonClassMirrorClassMirror 提供存取類別 Person 建構函數的方式。(如果這個步驟導致例外狀況,最簡單的解決方法是在啟動 REPL 時使用這些旗標。 scala -Yrepl-class-based:false)

scala> val ctor = ru.typeOf[Person].decl(ru.termNames.CONSTRUCTOR).asMethod
ctor: scala.reflect.runtime.universe.MethodSymbol = constructor Person

Person 建構函數的符號可以使用僅有執行階段宇宙 ru 取得,方法是在類型 Person 的宣告中查詢。

scala> val ctorm = cm.reflectConstructor(ctor)
ctorm: scala.reflect.runtime.universe.MethodMirror = constructor mirror for Person.<init>(name: String): Person (bound to null)

scala> val p = ctorm("Mike")
p: Any = Person(Mike)

存取和呼叫執行階段類型的成員

一般來說,執行階段類型的成員會使用適當的「呼叫者」鏡像來存取 (鏡像在 下方 有更進一步的說明)。讓我們使用 REPL 逐步示範一個範例

scala> case class Purchase(name: String, orderNumber: Int, var shipped: Boolean)
defined class Purchase

scala> val p = Purchase("Jeff Lebowski", 23819, false)
p: Purchase = Purchase(Jeff Lebowski,23819,false)

在這個範例中,我們將嘗試透過反射取得並設定 Purchase pshipped 欄位。

scala> import scala.reflect.runtime.{universe => ru}
import scala.reflect.runtime.{universe=>ru}

scala> val m = ru.runtimeMirror(p.getClass.getClassLoader)
m: scala.reflect.runtime.universe.Mirror = JavaMirror with ...

就像我們在先前的範例中所做的那樣,我們會從取得一個鏡像 m 開始,它讓載入 p (Purchase) 類別的類別載入器載入的所有類別和類型都可用,這是我們存取成員 shipped 所需的。

scala> val shippingTermSymb = ru.typeOf[Purchase].decl(ru.TermName("shipped")).asTerm
shippingTermSymb: scala.reflect.runtime.universe.TermSymbol = method shipped

我們現在查詢 shipped 欄位的宣告,它會提供我們一個 TermSymbol(一種 Symbol 類型)。我們稍後需要使用這個 Symbol 來取得一個鏡像,讓我們可以存取這個欄位的值(對某些執行個體而言)。

scala> val im = m.reflect(p)
im: scala.reflect.runtime.universe.InstanceMirror = instance mirror for Purchase(Jeff Lebowski,23819,false)

scala> val shippingFieldMirror = im.reflectField(shippingTermSymb)
shippingFieldMirror: scala.reflect.runtime.universe.FieldMirror = field mirror for Purchase.shipped (bound to Purchase(Jeff Lebowski,23819,false))

為了存取特定執行個體的 shipped 成員,我們需要一個鏡像給我們的特定執行個體,p 的執行個體鏡像 im。有了我們的執行個體鏡像,我們可以取得一個 FieldMirror,對應到任何代表 p 類型欄位的 TermSymbol

現在我們有一個特定欄位的 FieldMirror,我們可以使用方法 getset 來取得/設定我們特定執行個體的 shipped 成員。讓我們將 shipped 的狀態變更為 true

scala> shippingFieldMirror.get
res7: Any = false

scala> shippingFieldMirror.set(true)

scala> shippingFieldMirror.get
res9: Any = true

Java 中的執行時期類別與 Scala 中的執行時期類型

習慣使用 Java 反射在執行時期取得 Java 類別執行個體的人可能會注意到,在 Scala 中,我們取得的是執行時期類型

以下在 REPL 中執行的內容顯示了一個非常簡單的範例,說明在 Scala 類別中使用 Java 反射可能會傳回令人驚訝或不正確的結果。

首先,我們定義一個基礎類別 E,其中包含一個抽象類型成員 T,並從中衍生出兩個子類別 CD

scala> class E {
     |   type T
     |   val x: Option[T] = None
     | }
defined class E

scala> class C extends E
defined class C

scala> class D extends C
defined class D

接著,我們建立 CD 的實例,同時讓型別成員 T 具體化(在兩種情況下,String

scala> val c = new C { type T = String }
c: C{type T = String} = $anon$1@7113bc51

scala> val d = new D { type T = String }
d: D{type T = String} = $anon$1@46364879

現在,我們使用 Java 反射中的方法 getClassisAssignableFromjava.lang.Class 的實例取得 cd 的執行時期類別,然後我們測試以查看 d 的執行時期類別是否是 c 的執行時期表示的子類別。

scala> c.getClass.isAssignableFrom(d.getClass)
res6: Boolean = false

從上面,我們看到 D 擴充 C,這個結果有點令人驚訝。在執行這個簡單的執行時期類型檢查時,您會預期「d 的類別是 c 的類別的子類別嗎?」問題的結果為 true。不過,正如您在上面可能注意到的,當 cd 被實例化時,Scala 編譯器實際上會分別建立 CD 的匿名子類別。這是因為 Scala 編譯器必須將 Scala 特有的(非 Java)語言功能轉換為 Java 位元組碼中的某些等效功能,才能在 JVM 上執行。因此,Scala 編譯器經常會建立合成類別(自動產生的類別),在執行時期用於取代使用者定義的類別。這在 Scala 中相當普遍,當使用 Java 反射與多個 Scala 功能(例如封閉、類型成員、類型精緻化、區域類別等)時,可以觀察到這種情況。

在這種情況下,我們可以改用 Scala 反射來取得這些 Scala 物件精確的執行時期類型。Scala 執行時期類型會攜帶編譯時期的所有類型資訊,避免編譯時期和執行時期之間的這些類型不符。

在下面,我們定義一個方法,它使用 Scala 反射來取得其引數的執行時期類型,然後檢查兩個類型之間的子類型關係。如果其第一個引數的類型是其第二個引數類型的子類型,它會傳回 true

scala> import scala.reflect.runtime.{universe => ru}
import scala.reflect.runtime.{universe=>ru}

scala> def m[T: ru.TypeTag, S: ru.TypeTag](x: T, y: S): Boolean = {
    |   val leftTag = ru.typeTag[T]
    |   val rightTag = ru.typeTag[S]
    |   leftTag.tpe <:< rightTag.tpe
    | }
m: [T, S](x: T, y: S)(implicit evidence$1: scala.reflect.runtime.universe.TypeTag[T], implicit evidence$2: scala.reflect.runtime.universe.TypeTag[S])Boolean

scala> m(d, c)
res9: Boolean = true

正如我們所見,我們現在取得預期的結果– d 的執行時期類型確實是 c 的執行時期類型的子類型。

編譯時期反射

Scala 反射啟用一種元程式設計,讓程式在編譯時期修改它們自己。這種編譯時期反射以巨集的形式實現,提供在編譯時期執行操縱抽象語法樹的方法的能力。

巨集特別有趣的方面在於它們基於與 Scala 的執行時期反射所使用的相同 API,提供在套件 scala.reflect.api 中。這讓巨集和使用執行時期反射的實作之間能夠共用通用程式碼。

請注意,巨集指南專注於巨集的具體事項,而本指南則專注於反射 API 的一般面向。儘管如此,許多概念直接套用於巨集,例如在 符號、樹狀結構和類型 的區段中更詳細討論的抽象語法樹。

環境

所有反射任務都需要設定適當的環境。此環境會根據反射任務是在執行時期或編譯時期執行而有所不同。在執行時期或編譯時期使用的環境之間的區別封裝在所謂的宇宙中。反射環境的另一個重要面向是我們具有反射存取權的實體集合。此實體集合由所謂的鏡像決定。

鏡像不僅決定可以透過反射存取的實體集合。它們也提供在那些實體上執行的反射操作。例如,在執行時期反射中,呼叫者鏡像可用於呼叫類別的方法或建構函式。

宇宙

Universe 是 Scala 反射的入口點。Universe 提供介面,可使用反射中所有主要概念,例如 TypesTreesAnnotations。如需更多詳細資料,請參閱本指南中關於 Universes 的區段,或套件 scala.reflect.api 中的 Universes API 文件

若要使用 Scala 反射的大部分面向,包括本指南中提供的多數程式碼範例,您需要確定匯入 UniverseUniverse 的成員。通常,若要使用執行時期反射,可以使用萬用字元匯入匯入 scala.reflect.runtime.universe 的所有成員

import scala.reflect.runtime.universe._

Mirrors

Mirror 是 Scala 反射的核心部分。反射提供的所有資訊都是透過這些所謂的 mirrors 存取的。必須根據要取得的資訊類型或要執行的反射動作,使用不同類型的 mirrors。

如需更多詳細資料,請參閱本指南中關於 Mirrors 的區段,或套件 scala.reflect.api 中的 Mirrors API 文件

此頁面的貢獻者