【Scala反射】反射概述

概述

Reflection 是一種程序檢查,甚至多是自我修改的能力。 它在面向對象、函數式和邏輯編程範例方面有着悠久的歷史。雖然只有一些語言是以反射爲指導原則,但隨着時間的推移,許多語言逐漸發展出反射能力。html

反射涉及到對程序的其餘隱含元素進行具體化(即明確表達)的能力。 這些元素能夠是靜態程序元素,如類、方法或表達式,也能夠是動態元素,如當前的延續或完成事件,如方法調用和字段訪問。 一般從根據執行反射過程的時間區分編譯時和運行時反射。 編譯時反射是開發程序轉換器和生成器的強大方式,而運行時反射一般用於調整語言語義或支持軟件組件之間的後期綁定。java

Scala直到2.10這個版本也尚未任何反射工具。而代替方式是,使用Java反射API的一部分,即處理提供動態檢查類和對象並訪問其成員的能力。然而,在獨立Java反射下,許多Scala特定的元素是不可恢復的,它僅公開Java元素(沒有函數,沒有特質)和類型(沒有存在判別、高階特性、路徑依賴和抽象類型)。另外,Java反射也沒法恢復編譯時通用的Java類型的運行時類型信息、經過運行時反射到Scala中的泛型類型的限制。es6

在Scala 2.10中,引入了一個新的反射庫,不只解決了Java在Scala特定類型和泛型類型上運行時反射的缺點,並且爲Scala增長了一個更強大的通用反射功能工具箱。除了Scala類型和泛型的全功能運行時反射以外,Scala 2.10還提供了以的形式的編譯時反射功能,以及將Scala表達式變爲抽象語法樹的功能。編程

運行時反射

什麼是運行時反射? 在運行時給定某個對象的類型或實例,反射就是可以:api

  • 檢查該對象的類型,包括泛型類型,
  • 實例化新的對象,
  • 或訪問或調用該對象的成員。

讓咱們來看看如何經過幾個例子來完成上述每一步。閉包

檢查運行時類型(包括運行時的通用類型)

與其餘JVM語言同樣,Scala的類型在編譯時被擦除。這意味着若是您要檢查某個實例的運行時類型,則可能沒法訪問Scala編譯器在編譯時可用的全部類型信息。ide

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 做爲參數調用咱們的方法,並調用返回 TypeTag 中包含的類型的 tpe。 正如咱們所看到的,咱們獲得了正確的完整類型(包括 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 ++)

在運行時啓動類型

經過反射得到的類型能夠經過使用適當的 「調用者」 mirror 調用它們的構造函數來實例化(mirror 在下面展開)。 讓咱們經過一個使用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 ...

第一步,咱們得到一個mirror 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 方法獲取 Person 類的 ClassMirrorClassMirror 提供對 Person 類的構造函數的訪問。

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

Persons 構造函數的 symbol 能夠經過在 Person 類型的聲明中查找來僅使用運行時universe  ru 來得到。

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)

訪問和調用運行時類型的成員

一般,運行時類型的成員可使用適當的「調用者」 mirror 來訪問(mirror 在下面展開)。讓咱們經過一個使用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 ...

正如咱們在前面的例子中所作的那樣,咱們首先得到一個mirror m,它使得全部可用的類和類型均可用,這些類和類型由類加載器加載,該類也加載了類 pPurchase),咱們須要它才能訪問成員 shipped

cala> 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 成員,咱們須要一個 mirror 用於咱們的特定實例,p 的實例鏡像 im。 考慮到咱們的實例 mirror,咱們能夠爲表示 p 類型字段的任意 TermSymbol 獲取 FieldMirror

如今咱們爲特定領域提供了一個 FieldMirror,咱們可使用方法 getset 來獲取/設置特定實例的 shipped 成員。 讓咱們將 shipped 狀態更改成 true

scala> shippingFieldMirror.get
res7: Any = false

scala> shippingFieldMirror.set(true)

scala> shippingFieldMirror.get
res9: Any = true

Java中的運行時類 vs. Scala中的運行時類型

那些習慣使用Java反射在運行時得到Java類實例的人可能已經注意到,在Scala中,咱們改成得到運行時類型。

下面的REPL運行顯示了一個很是簡單的場景,在Scala類上使用Java反射可能會返回使人驚訝或不正確的結果。

首先,咱們用一個抽象類型成員 T 定義一個基類 E,並從中得出兩個子類 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 Reflection 的 getClassisAssignableFrom 獲取表示 cd 運行時類的 java.lang.Class 實例,而後測試運行時類 d 是否爲 c 的運行時表示的子類。

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

在上面的代碼中,咱們看到 D 擴展 C,但最後的結果有點使人驚訝。在執行這個簡單的運行時類型檢查時,人們會認爲問題「c的子類是否爲d」的結果是 true 。可是,正如你可能在上面已經注意到的那樣,當 cd 被實例化時,Scala編譯器實際上分別建立了 CD 的匿名子類。這是因爲 Scala 編譯器必須將 Scala 特定的(即非Java)語言特性翻譯成 Java 字節碼中的某些等價物以便可以在JVM上運行。所以,Scala編譯器常常在運行時建立用來代替用戶定義的類的合成類(即自動生成的類)。這在Scala中至關廣泛,而且在使用具備多個Scala功能的Java反射時能夠觀察到,例如,閉包、類型成員、類型優化、本地類等。

在這些狀況下,咱們可使用 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 中。反射環境的另外一個重要方面是咱們能夠反射訪問的一組實體。這組實體由所謂的 mirror 肯定。

mirror 不只肯定可反射訪問的一組實體。 他們還提供對這些實體進行反射的操做。 例如,在運行時反射中,調用者 mirror 可用於調用類的方法或構造函數。

Universe

Universe 是 Scala 反射的入口點。Universe 爲反射中使用的全部主要概念(如 typeTreeAnnotation)提供了一個接口。 有關更多詳細信息,請參閱 Universe 上的本指南部分或包 scala.reflect.api 中的Universe API文檔

要使用 Scala 反射的大部份內容(包括本指南中提供的大多數代碼示例),須要確保導入 UniverseUniverse 成員。 一般,要使用運行時反射,可使用通配符導入來導入 scala.reflect.runtime.universe 的全部成員:

import scala.reflect.runtime.universe._

Mirror

Mirror 是 Scala Reflection 的核心部分。經過反射提供的全部信息均可以經過這些所謂的 mirror 訪問。根據要獲取的信息類型或要採起的反射行爲,必須使用不一樣的 mirror 風格。

有關更多詳細信息,請參閱本指南有關 mirror 的部分或包 scala.reflect.api 中的 Mirror API文檔

相關文章
相關標籤/搜索