反射機制 小小談

Oberon發表於2019-06-25

反射機制(Reflection)

何為反射

反射是在兩種物質分介面上改變傳播方向返回原來物質中的現象
反射是生物體對外界刺激做出應激行為的過程,根據產生的原因分為條件反射非條件反射等,典型的實驗案例包括巴甫洛夫的狗……
反射是一些物件導向程式設計語言提供的針對物件後設資料(Metadata)的一種訪問機制

元……資料??什麼高深莫測的武功??

啊,誠然,一旦涉及到“元XXX”事情通常就開始變得無比抽象,以至於我不禁唸叨起那句訣

太極生兩儀,兩儀生四象,四象生八卦……

不過後設資料這個概念在資料庫裡還是比較常見的,比如,某個關係型資料庫裡有張表:

水果

編號 名字 數量
1 蘋果 6
2 香蕉 3
3 5
4 橘子 3
5 菠蘿 2

資料,就是存在表裡的一條一條的記錄,(1,蘋果,6),(3,梨,5)都是資料,那麼,後設資料就是凌駕於這些資料之上的用於描述資料資料,對於這張表而言,也就是這張表的表頭(關係資料理論裡稱之為關係模式):(編號,名稱,資料)

劃重點
後設資料(Metadata):用於描述資料的資料

好像有些明朗了,但那關物件導向什麼事呢

眾所周知,類(Class)是物件導向的一個重要概念,儘管,針對於資料庫來說,物件模型和關係模型是不同的概念(上文提到的是關係模型的一個例子),但是,物件模型中的物件和關係模型中的關係,其級別是等同的。

關係……又物件……越來越聽不懂了

好吧,我們先把關係放在一邊,我們只把上邊的東西看做一張表。

難道你就沒有把它改寫成如下形式的衝動嗎??

public class Fruit
{
    public int no;
    public string name;
    public int count;
    
    public Fruit(int no, string name, int count)
    {
        // ...
    }
}

好了,上面的類定義的語義就是

有這樣一類東西,我們稱呼這類東西為水果,結構如下……

那麼,這樣一來,我們就可以定義一個no為9,name叫做“西瓜”,count為5的一個物件,這個物件具有具體的資料。

而上面的類定義程式碼,包含的就是這個類的後設資料

說的再直白點吧

以人為例,資料注重的是這人的臉長啥樣,而後設資料注重的是這人有沒有臉(好像不太對……)

好吧差不多瞭解了,但後設資料和反射有什麼關係呢

反射是一些物件導向程式設計語言提供的針對物件後設資料(Metadata)的一種訪問機制

本文一開始就說了,罰站20年

不過在此之前先解釋一件事,後設資料在哪

任何一個物件導向的程式設計語言,其類型別都具備一個後設資料的儲存,至少程式會使用這個後設資料能夠動態地構造此類的物件。但不同的語言機制不同,比如C++這種的,因為直接和系統進行愉♂快的互♂動,因此後設資料就直接使用系統的記憶體地址了,這種資料使用是很不直觀的,同時也不使用任何託管機制做後援(巨硬魔改的C++/CLI不在討論範圍內),因此這種貼近底層的語言不支援反射機制,雖然可以通過強行向程式程式碼中通過工廠類模式強行注入可讀的元資訊(方法參見這位大佬的文章)。

但是,正如前面所說的,如果後設資料在託管編譯或解釋的狀態下會保留一份可讀的版本,這是提供給直譯器或者託管平臺用的,當然,這種情況下語言一般會提供一個較為完善的後設資料訪問機制,這就是反射。這類語言典型的代表就是C#(.NET託管)、Java(JVM虛擬機器)、Python(直譯器提供)等。

那……反射是如何運作的呢??

反射嘛。那還不容易,拿個鏡子就可以了呀!
或者用羊角錘偷襲的方式砸膝蓋什麼的也是很容易的呀!
不過這麼說來,拿羊角錘偷襲鏡子豈不是更棒!!

正如之前所說,反射機制是對類的後設資料的獲取和操縱,因此,一個重要的前提就是:

這個程式設計語言的運作機制當中,類的後設資料必須是可見的,如果可讀的話那更好

只有當類的後設資料是可見的,反射機制才有訪問它們的可能,但是後設資料的可讀性會決定反射機制訪問它們的難易程度。

這裡補充一句,有人會說,在使用IDE或者程式碼編輯器的時候,我們寫object.property這種訪問方式的時候編譯器不就直接告訴我們了麼??
關於這一點,這裡暫時只說一個前提:

反射機制的實際動作是聚焦於執行時(Runtime)的。

在程式程式碼編譯之前我們恣意地書寫這MyObject.id.hashCode.getFlush().balabala的時候,這是預編譯的過程,預編譯的時候當然這些後設資料都是以字面形式給出的(因為你的程式碼裡寫了這個類的定義),你可以非常愉悅地Ctrl+C Ctrl+V或者享受著IntelliSense帶給你的N倍快樂,這個時候再談反射就沒什麼意義了,因此,反射機制訪問後設資料都是在編譯後執行時發生的。

明明都是物件導向,為什麼偏偏C++不支援這個東西呢

以C++為例,這些後設資料是否可見?答案是肯定的,那為什麼不支援反射機制呢,因為這些後設資料是以指標的方式給出的,指標在已編譯的C++程式中的存在形式就是地址,說的再粗暴點,就是4或8位元組的二進位制數……
也就是說,在已經編譯完成的C++程式的眼裡,類的後設資料已經變成二進位制的地址碼了,如果某人在沒有原始碼的情況下想給這個專案寫一個反射機制,那麼他將不得不面對一大堆的:

0xb08dfe231a1c002e
0xb08dfe231bc128f6
0xb08dfe2417a90f5d
......

看到這些,他長舒了一口氣,優雅地點燃了一根香菸,然後毫不猶豫地戳到電腦螢幕上:

鬼知道這是什麼玩意啊!!

如果原專案加個殼、模板元編一下再做個混淆加密的話那更沒法看了,因此如果一定要實現反射機制,一般都是把反射機制直接囊括到專案開發過程當中(就像上面那位大佬的文章中提到的那樣,原專案的作者也是反射機制的構造者)。
這樣的話就會存在一個上上上個世紀汽車行業出現的問題:

這輛車的件無法用到另外一輛車上!
這個反射機制無法用到別的專案上!

當然,這樣說可能有些絕對,但以C++的方式實現一個能夠廣泛用於所有專案的反射機制應該是極端困難的。
上面大佬的文章當中,這個C++的專案要使用反射機制,是藉助工廠模式實現的,關於這些的實現方法,詳見大佬文章(當然我自己也沒完全看懂)

那託管語言又如何呢

C#、Java,這兩種語言都是託管程式碼的(C#使用.NET進行託管,Java則交給了JVM虛擬機器)。

與C++不同的是,他們並不直接接觸系統底層,而是通過中間程式碼訪問底層的。

中間程式碼由誰處理呢,C#是通過.NET提供的CLR,而Java靠的是JVM。

這就好像,一群孩子進了幼兒園,一個託管老師全程進行看護。

把拔碼麻區上辦,我區悠貳園吶

當然,託管老師肯定是知道孩子叫什麼名的,訪問他們自然也是很容易的。同理,託管環境(或虛擬環境)也是一樣的,因為銜接上下兩層,因此把底層的後設資料和上層的可讀文字構造反射的橋樑是很容易辦到的,因此,C#和Java都提供了一套非常完善的反射庫,他們可以被用於使用這兩種語言寫的任意一個類當中。

好了,道理我都懂,但為什麼要反射呢?

反射能幹什麼呢

舉個最簡單的例子
我……我有一個夢想,我想要這樣一個函式,能夠返回Person類是否有我所說的方法,但是我不知道Person類裡有什麼,比如我想問他有沒有Eat()方法,它返回true,我問他有沒有Fly()方法,它能返回false

好了,換作是你,你會怎麼實現這樣一個函式呢??

而反射機制恰恰做到了!
你提供給反射機制一個字串形式的函式名,反射機制不僅可以得知這個函式是否存在,甚至能幫助你去執行這個函式(Invoke)。

什麼,你不好問它有沒有某個函式??好啊,反射機制甚至可以告訴你這個類都有哪些屬性哪些函式,繼承自誰,可見性如何,是否抽象等等。

那反射在什麼時候比較好用呢

上面那個例子其實就是一個經典的用途。

或者,我們可以考慮另外一個場景。

你寫了某個函式接受了一個抽象為Object的物件,你希望,如果Object的物件存在方法Grow則呼叫之,否則什麼也不做。

這個時候首先可以通過反射機制確定方法是否存在,但即便方法已經存在,我們是無法直接呼叫的,因為物件已經抽象為Object,而Object並不存在方法Grow,所以直接呼叫就洗洗睡了。

我們不能具象回來麼??

如果我們知道類在抽象之前是什麼型別的時候,那當然可以具象化回來。
但是抽象雖然發生於編譯時或執行時(動態建立的物件),但具象型別的獲知卻是在編譯之前的程式碼原始檔,而且還有些時候你根本無法知道原型別,那也沒辦法拆箱。


這裡面我為了方便,也是想不出啥更好的詞
這裡我稱派生類基類的多型轉化為抽象
反過來的過程稱為具象

那我還怎麼呼叫Grow

反射機制可以獲取到完整的可用方法的列表,我們在列表中找到了Grow,存在形式為Method/MethodInfo物件或乾脆就是個字串。

但無論是哪種,obj.Grow();是不可能了,好在反射機制連這件事都考慮在內了——Invoke呼叫!!

反射機制不僅知道你想要什麼方法,還可以幫助你呼叫這個方法,這個呼叫就通過一個叫做Invoke的方法完成。

不同語言對Invoke的定義不盡相同但功能上大同小異,通過Invoke呼叫某方法的過程實質上是轉調回撥(或者是間接呼叫)。
間接呼叫比直接呼叫更加的強大靈活,但繞了遠路。

當然,以上都是反射機制用途中小的不能再小的冰山一角,比如我還可以通過反射機制根據我的輸入建立我想要的型別的物件等等。

哇,反射這麼強大??我要滿地反射!!

冷靜點!任何事物都有多面性,反射也不例外,我們看看反射機制有什麼特點,它到底是否適合所有情形。

極致靈動(Flexi Frenzy) 稀有屬性

反射機制可以讓你的程式碼非常靈活,以不變應萬變。
這也正是反射機制帶來的最大的好處。

未卜先知(Fortune Tell) 普通屬性

反射機制是在執行時起作用,當然,執行期間發生什麼,編譯之前是無法獲知的,反射就是處理這件事的。

效率捉急(Emaciated Efficiency) 糟糕屬性

反射機制最大的問題!

反射機制的效率是十分低下的,首先在執行時獲取後設資料再轉化成可讀形式就不是一個很快的過程,而反射的Invoke呼叫是個不折不扣的間接呼叫。

不當地使用大量反射會導致程式效率的急劇下降。

程式碼膨脹(Code Expansion)

顯然,用反射進行呼叫的程式碼往往比直接呼叫寫起來複雜,所以除非你寫程式碼是按行數計工資,否則能直接呼叫就不要反射。

健壯風險(Robustness Risk)

反射機制一般允許使用者傳入字串……

然後就是萬劫不復深淵之伊始

這時候使用者傳的字串就可以非常的五花八門了,就好像一個動物園裡,反射機制是一個可愛的小動物,而遊客開始不分青紅皁白地對它投各種食,良莠不齊,可是你的反射機制很脆弱,它可禁不起這折騰,吃到不好的東西就會生病罷工(拋異常,然後中止),因此你這當奶媽奶爸就要多操心,幫它收拾(捕獲),告訴他如何分辨食物(預先判斷)……

不過呢,有些時候引入反射機制恰恰就是出於健壯性的考慮……

如果我養的不是個反射機制而是一隻熊貓的話我會上天的!!

總結

反射是個強大的武器,但使用應多加謹慎!

以上

相關文章