本篇文章主要讓大家理解什么是Scala的反射, 以及反射的分類, 反射的一些術語概念和一些簡單的反射例子.
什么是反射
我們知道, Scala是基於JVM的語言, Scala編譯器會將Scala代碼編譯成JVM字節碼, 而JVM編譯過程中會擦除一些泛型
信息, 這就叫類型擦除(type-erasure ).
而我們開發過程中, 可能需要在某一時刻獲得類中的詳細泛型
信息. 並進行邏輯處理. 這時就需要用的 這個概念. 通過反射我們可以做到
- 獲取運行時類型信息
- 通過類型信息實例化新對象
- 訪問或調用對象的方法和屬性等
在Scala-2.10以前, 只能在Scala中利用Java的反射機制, 但是通過Java反射機制得到的是只是擦除后的類型信息, 並不包括Scala的一些特定類型信息. 從Scala-2.10起, Scala實現了自己的反射機制, 我們可以通過Scala的反射機制得到Scala的類型信息。
Scala 反射的分類
Scala 的反射分為兩個范疇:
- 運行時反射
- 編譯時反射
這兩者之間的區別在於Environment
, 而Environment
又是由universe
決定的. 反射的另一個重要的部分就是一個實體集合,而這個實體集合被稱為mirror
,有了這個實體集合我們就可以實現對需要反射的類進行對應的操作,如屬性的獲取,屬性值得設置,以及對反射類方法的調用(其實就是成員函數的入口地址, 但請注意, 這只是個地址
)!
可能有點繞, 說的直白點就是要操作類方法或者屬性就需要獲得指定的mirror
,而mirror
又是從Environment
中得來的,而Environment
又是Universes
中引入的,而Universes
根據運行時和編譯時又可以分為兩個領域的.
對於不同的反射, 我們需要引入不同的Universes
import scala.reflect.runtime.universe._ // for runtime reflection import scala.reflect.macros.Universe._ // for compile-time reflection
編譯過程
因為反射需要編譯原理基礎, 而我學的其實也不好, 所以不做過多深入的探討, 這里主要說一下Scala 在 .scala
到 .class
再到裝入 JVM 的這個過程.
類Java程序之所以能實現跨平台, 主要得益於JVM(Java Virtual Machine)的強大. JVM為什么能實現讓類Java代碼可以跨平台呢? 那就要從類Java程序的整個編譯、運行的過程說起.
我們平時所寫的程序, 都是基於語言(第三代編程語言)范疇的. 它只能是開發者理解, 但底層硬件(如內存和cpu)並不能讀懂並執行. 因此需要經歷一系列的轉化. 類Java的代碼, 首先會經過自己特有的編輯器, 將代碼轉為.class
, 再由ClassLoader將.class
文件加載到JVM運行時數據區, 此時JVM就可以讀懂.class
的二進制文件, 並調用C/C++
來間接操作底層硬件, 實現代碼功能.
.scala => .class 過程
整體流程如下
詞法分析
首先Scala編譯器要讀取源代碼, 一個字節一個字節地讀進來, 找到關鍵詞如if
、for
、while
等, 這就是詞法分析的過程. 這個過程結束以后.Scala
代碼就變成了規范的Token流, 就像我們把一句話中的名詞、動詞、標點符號分辨出來.
語法分析
接着Scala編譯器就是對Token流進行語法分析了, 比如if后面跟着的是不是一個布爾型的變量, 並將符合規范的語法使用語法樹存貯起來. 之所以要用語法樹來存儲, 是因為這樣做可以方便以后對這棵樹按照新的規則重新組織, 這也是編譯器的關鍵所在.
語義分析
之后Scala編譯器會進行語義分析. 因為可以保證形成語法樹以后不存在語法錯誤, 但語義是否正確則無法保證. 還有就是Scala會有一些相對復雜的語法, 語義分析器的作用就是將這些復雜的語法翻譯成更簡單的語法, 比如將foreach
翻譯成簡單的for循環, 使它更接近目標語言的語法規則.
字節碼生成
最后就是由代碼生成器將將語義分析的結果生成符合JVM規范的字節碼了.
符號 Symbol
Symbol Table, 簡單的來說是用於編譯器或者解釋器的一種數據結構, 通常是用HashTable實現. 它所記載的信息通常是標識符(identifier)的相關信息,如類型,作用域等。那么,它通常會在語義分析(Semantic Analysis)階段運用到.
抽象語法樹 AST
語法樹通過樹結構來描述開始符到產生式的推導過程.
在計算機科學中,抽象語法樹(abstract syntax tree 或者縮寫為 AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這里特指編程語言的源代碼。樹上的每個節點都表示源代碼中的一種結構。之所以說語法是「抽象」的,是因為這里的語法並不會表示出真實語法中出現的每個細節。
運行時反射
Scala運行時類型信息是保存在TypeTag對象中, 編譯器在編譯過程中將類型信息保存到TypeTag中, 並將其攜帶到運行期. 我們可以通過typeTag方法獲取TypeTag類型信息。
TypeTag
舉例如下:
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> typeTag[List[Int]] res0: reflect.runtime.universe.TypeTag[List[Int]] = TypeTag[scala.List[Int]] scala> res0.tpe res1: reflect.runtime.universe.Type = scala.List[Int] scala> typeOf[List[Int]] res2: reflect.runtime.universe.Type = scala.List[Int]
但是,上面的例子不實用, 因為我們要在事先知道它的類型信息. 不過沒關系, 下面我們寫個方法, 來事先任意變量的類型獲取:
scala> val ru = scala.reflect.runtime.universe
ru: scala.reflect.api.JavaUniverse = scala.reflect.runtime.JavaUniverse@6dae22e7
scala> def getTypeTag[T : ru.TypeTag](obj: T) = ru.typeOf[T] getTypeTag: [T](obj: T)(implicit evidence$1: ru.TypeTag[T])ru.TypeTag[T] scala> val list = List(1, 2, 3) list: List[Int] = List(1, 2, 3) scala> val theType = getTypeTag(list) theType: ru.Type = List[Int]
上面代碼就是一個簡單的方法定義, 但是用到了兩個關鍵的技術, 分別是泛型[T]
和上下文界定[T : ru.TypeTag]
, 下面我來解讀一下:
泛型
scala中的泛型稱為類型參數化(type parameterlization).
通過名字, 應該有個模糊的概念了, 好像是要傳一個類型當做參數. 舉個例子:
scala> def getList[T](value: T) = List(value) getList: [T](value: T)List[T] scala> getList[Int](1) res4: List[Int] = List(1) scala> getList("1") res5: List[String] = List(1)
看例子, 我們定義了一個方法, 根據傳入的一個參數, 來生成一個List, 但為了方法的更加通用(可以處理任何類型), 我們的參數類型(value: T)
使用了泛型T
, 可以使任意字母, 但推薦大寫(約定俗成).
這樣, 我們調用方法, 大家可以看到. 這個方法既可以傳入Int
類型也可以傳入String
類型. 而類型參數[]中的類型, 我們可以手動填寫, 也可以不填, Scala編譯器會為我們自動推導, 如果填寫了, 編譯器就不會進行類型推導, 而是進行類型檢查, 看我們的入參, 是否符合類型參數.
scala> getList[String](1) <console>:16: error: type mismatch; found : Int(1) required: String getList[String](1)
知識延伸 上界
<:
, 下界>:
, 視界<%
, 邊界:
, 協變+T
, 逆變-T
trait Function1[-T, +U] { def apply(x: T): U } 等價於: fun: -T => +U
邊界
了解了泛型, 下面我們說一下上下文界定[T : ru.TypeTag]
, 也叫做邊界.
了解邊界, 我們先說一下視界<%
. 先舉例子:
scala> case class Fruits(name: String)
defined class Fruits
scala> def getFruits[T <% Fruits](value: T): Fruits = value getFruits: [T](value: T)(implicit evidence$1: T => Fruits)Fruits scala> implicit val border: String => Fruits = str => Fruits(str) border: String => Fruits = <function1> scala> getFruits("apple") res11: Fruits = Fruits(apple)
可以看到, 在代碼最后, 我們調用方法getFruits
的時候,傳入的是一個String
, 而返回給我們的是一個Fruits(apple)
, 這是怎么做的的呢?
這就依賴於視界T <% Fruits
, 這個符號, 可以理解成, 當前名稱空間, 必須存在一個implicit
可以將非繼承關系的兩個實體(String
到 Fruits
)的轉換. 也就是我們上面的那個隱式函數.
理解了視界, 那么邊界就很簡單了. 還是先寫個例子:
scala> case class Fruits[T](name: T) defined class Fruits scala> def getFruits[T](value: T)(implicit fun: T => Fruits[T]): Fruits[T] = value getFruits: [T](value: T)(implicit fun: T => Fruits[T])Fruits[T] scala> implicit val border: String => Fruits[String] = str => Fruits(str) border: String => Fruits[String] = <function1> scala> getFruits("apple") res0: Fruits[String] = Fruits(apple)
這個應該可以看懂吧, 有個T => Fruits[T]
的隱式轉換. 而Scala有個語法糖可以簡化上面的函數定義
def getFruits[T : Fruits](value: T): Fruits[T] = value
作用一樣, 需要一個T => Fruits[T]
的隱式轉換.
typeTag 總結
現在我們在回頭看一下最初的例子:
scala> def getTypeTag[T : ru.TypeTag](obj: T) = ru.typeOf[T]
這里, 我們把Fruits
換成了 ru.TypeTag
, 但是我們並沒有看到有T => TypeTag[T]
的隱式轉換啊!
不要急, 聽我說, 因為前面已經講過了, Scala運行時類型信息是保存在TypeTag對象中, 編譯器在編譯過程中將類型信息保存到TypeTag中, 並將其攜帶到運行期. 也就是說, Scala編譯器會自動為我們生成一個隱式, 只要我們在方法中定義了這個邊界.
ClassTag
一旦我們獲取到了類型信息(Type instance),我們就可以通過該Type對象查詢更詳盡的類型信息.
scala> val decls = theType.declarations.take(10) decls: Iterable[ru.Symbol] = List(constructor List, method companion, method isEmpty, method head, method tail, method ::, method :::, method reverse_:::, method mapConserve, method ++)
而如果想要獲得擦除后的類型信息, 可以使用ClassTag
,
注意,
classTag
在包scala.reflect._
下
scala> import scala.reflect._
import scala.reflect._
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> val tpeTag = typeTag[List[Int]]
tpeTag: reflect.runtime.universe.TypeTag[List[Int]] = TypeTag[scala.List[Int]]
scala> val clsTag = classTag[List[Int]]
clsTag: scala.reflect.ClassTag[List[Int]] = scala.collection.immutable.List
scala> clsTag.runtimeClass
res12: Class[_] = class scala.collection.immutable.List scala> classOf[List[Int]] res0: Class[List[Int]] = class scala.collection.immutable.List
運行時類型實例化
反射是比Scala本身更接近底層的一種技術, 所以當然比本身可以做更多的事情, 我們試着使用反射, 在運行期生成一個實例:
scala> case class Fruits(id: Int, name: String) defined class Fruits scala> import scala.reflect.runtime.universe._ import scala.reflect.runtime.universe._ // 獲得當前JVM中的所有類鏡像 scala> val rm = runtimeMirror(getClass.getClassLoader) rm: reflect.runtime.universe.Mirror = JavaMirror with scala.tools.nsc.interpreter.IMain$TranslatingClassLoader@566edb2e of ..... // 獲得`Fruits`的類型符號, 並指定為class類型 scala> val classFruits = typeOf[Fruits].typeSymbol.asClass classFruits: reflect.runtime.universe.ClassSymbol = class Fruits // 根據上一步的符號, 從所有的類鏡像中, 取出`Fruits`的類鏡像 val cm = rm.reflectClass(classFruits) cm: reflect.runtime.universe.ClassMirror = class mirror for Fruits (bound to null) // 獲得`Fruits`的構造函數, 並指定為asMethod類型 scala> val ctor = typeOf[Fruits].declaration(nme.CONSTRUCTOR).asMethod ctor: reflect.runtime.universe.MethodSymbol = constructor Fruits // 根據上一步的符號, 從`Fruits`的類鏡像中, 取出一個方法(也就是構造函數) scala> val ctorm = cm.reflectConstructor(ctor) // 調用構造函數, 反射生成類實例, 完成 scala> ctorm(1, "apple") res2: Any = Fruits(1,apple)
Mirror
Mirror是按層級划分的,有
- ClassLoaderMirror
- ClassMirror ( => 類)
- MethodMirror ( => 方法)
- FieldMirror ( => 成員)
- InstanceMirror ( => 實例)
- MethodMirror
- FieldMirror
- ModuleMirror ( => Object)
- MethodMirror
- FieldMirror
- ClassMirror ( => 類)
運行時類成員訪問
話不多說, 舉例來看:
scala> case class Fruits(id: Int, name: String) defined class Fruits scala> import scala.reflect.runtime.universe._ import scala.reflect.runtime.universe._ // 獲得當前JVM中的所有類鏡像 scala> val rm = runtimeMirror(getClass.getClassLoader) rm: reflect.runtime.universe.Mirror = JavaMirror with scala.tools.nsc.interpreter.IMain$TranslatingClassLoader@566edb2e of ..... // 生成一個`Fruits`的實例 scala> val fruits = Fruits(2, "banana") fruits: Fruits = Fruits(2,banana) // 根據`Fruits`的實例生成實例鏡像 val instm = rm.reflect(fruits) instm: reflect.runtime.universe.InstanceMirror = instance mirror for Fruits(2,banana) // 獲得`Fruits`中, 名字為name的成員信息, 並指定為asTerm類型符號 scala> val nameTermSymbol = typeOf[Fruits].declaration(newTermName("name")).asTerm nameTermSymbol: reflect.runtime.universe.TermSymbol = value name // 根據上一步的符號, 從`Fruits`的實例鏡像中, 取出一個成員的指針 scala> val nameFieldMirror = instm.reflectField(nameTermSymbol) nameFieldMirror: reflect.runtime.universe.FieldMirror = field mirror for private[this] val name: String (bound to Fruits(2,banana)) // 通過get方法訪問成員信息 scala> nameFieldMirror.get res3: Any = banana // 通過set方法, 改變成員信息 scala> nameFieldMirror.set("apple") // 再次查詢, 發現成員的值已經改變, 即便是val, 在反射中也可以改變 scala> nameFieldMirror.get res6: Any = apple
(補)關於Manifest
的路徑依賴問題
Scala 在2.10之前的反射中, 使用的是 Manifest
和 ClassManifest
.
不過scala在2.10里卻用TypeTag替代了Manifest,用ClassTag替代了ClassManifest.
原因是在路徑依賴類型中,Manifest存在問題:
scala> class Foo{class Bar} defined class Foo scala> val f1 = new Foo;val b1 = new f1.Bar f1: Foo = Foo@994f7fd b1: f1.Bar = Foo$Bar@1fc0e258 scala> val f2 = new Foo;val b2 = new f2.Bar f2: Foo = Foo@ecd59a3 b2: f2.Bar = Foo$Bar@15c882e8 scala> def mfun(f: Foo)(b: f.Bar)(implicit ev: Manifest[f.Bar]) = ev mfun: (f: Foo)(b: f.Bar)(implicit ev: scala.reflect.Manifest[f.Bar])scala.reflect.Manifest[f.Bar] scala> def tfun(f: Foo)(b: f.Bar)(implicit ev: TypeTag[f.Bar]) = ev tfun: (f: Foo)(b: f.Bar)(implicit ev: reflect.runtime.universe.TypeTag[f.Bar])reflect.runtime.universe.TypeTag[f.Bar] scala> mfun(f1)(b1) == mfun(f2)(b2) res14: Boolean = true scala> tfun(f1)(b1) == tfun(f2)(b2) res15: Boolean = false
編譯時反射
請參考: