許多接觸過像Java及C#這類程式語言的程式人,或許對「Reflection(映射)」 這個名詞不陌生,但實際上將這個技巧運作在日常開發工作的程式設計者,可能就並不那麼多了。

「Reflection」這個名詞,在維基百科的解釋是「電腦程式用以觀察自身,及修改自身結構和行為的過程」。事實上,透過Reflection技巧,程式在執行時期本身便能夠得知自己的外觀長相,並且自我修改,甚至自我複製。

Reflection的作用:得知自己的外觀,甚至自我修改與複製
支援Reflection機制的程式語言眾多,大多數都是腳本式(Scripting Language)或是以虛擬機器為基礎的程式語言,例如Java、C#、Smalltalk、Python、Ruby、PHP、Perl等。甚至JavaScript也支援Reflection。

Reflection機制究竟能為程式提供什麼樣的作用?為什麼程式設計者需要動用到Reflection?針對諸如此類問題的答案,還是要回到為什麼程式需要在執行時期得知自己的外觀長相,甚至進一步自我修改、複製。

相對於「執行時期」,未使用Reflection機制的程式碼,在編譯時期便已為編譯器所見。對這樣的物件導向程式而言,當某個類別A存在與另一個類別B的互動時,類別B在編譯時期的長相,勢必已經已為類別A所了解。

舉例來說,對於C++程式而言,類別A欲與類別B互動(例如呼叫它的函式),編譯器在編譯類別A的程式碼時,必須也要能夠得知類別B的宣告及定義。相較於這樣的限制,Reflection則讓你的程式不必在編譯時期便確定此事,而是讓程式得以在執行時期,根據一些外在的資訊,決定操作的對象以及操作的方式,毋需於編譯時期便確定、同時寫死這些事情。

由此可以推想,Reflection是一個十分動態的特性,而且看起來可以為程式注入許多的彈性。

運用Reflection便能審視自身的特性
在解釋究竟Reflection能夠帶來什麼好處之前,先來看看具體的Reflection機制,以明白透過常見的Reflection支援,在程式中究竟能做到那些事情。我以Java為例介紹,目的不在介紹Java完整的Reflection API,而是透過Java,幫助大家了解Reflection的一般性概念。

在Java中Relfection機制的源頭,就是一個叫「Class」的class(在C#中有一個相似的類別,則叫做Type)。這個類別有點特殊,原因在於此類別的每一個物件都用來表示系統中的每一個類別。

具體來說,每個Class物件都描述了每個類別的相關資訊,也提供你透過它可以進行的一些操作。想要開始Reflection的動作,就必須先取得Class類別的物件。最常被運用到的兩個途徑,一個便是Object(所有物件皆繼承的類別)所提供的getClass()函式,另一個則是Class類別所提供的forName()靜態函式。

前者讓你得以取得一個物件(尤其是型別未知的物件)所屬的類別,而後者則讓你得以指定一個類別的名稱後,直接得到該類別對應的Class物件。

有了Class物件之後,便能「審視」自身的特性,這些特性包括了它隸屬於那個Package、類別本身究竟是Public還是Private、繼承自那一類別、實作了那些介面等。更重要的是,你可以得知它究竟有那些成員變數以及成員函式(包括建構式)。

透過Reflection,不需在程式中明定函式名稱、引數個數和型別
透過這個自我審視的過程,程式便能夠了解它所要處理的對象(尤其是型別未知的對象),究竟具備了什麼特質。對運用Reflection的程式而言,所了解到的這些特質,便會影響到該程式的運作行為。

取得了某類別的成員變數後(在Java中是以Field類別的物件表示),便可以取得該類別物件的成員變數值,也可以設定其值。同樣的,取得了某類別的成員函式後(在Java中是以Method類別的物件表示),便可取得該成員函式的回傳型別、傳入的引數列表型別,當然更重要的是,Method類別的物件,可被用以呼叫類別物件的相對應成員函式。

所以假想一個情境,你的程式面臨了一個待處理的物件,但你完全不知道它是那個型別,有什麼成員變數、有什麼成員函式,但你還是可以察覺出這一切,你會知道每個成員變數的名稱,每個成員函式的名稱、甚至你還可以取得每個成員函式的值、設定它們的值、還可以呼叫每個成員函式,同時傳入正確的引數、正確地取得回傳值。

除此之外,Java還允許程式人透過Class類別的newInstance()函式,產生該類別的物件,或許是透過Constructor類別物件取得建構式並呼叫、藉以執行不同建構式,以不同方式產生類別的物件。

從以上簡短的描述中,你應當能夠明白,Reflection讓你得以在執行時期處理一些原先在編譯時期才能夠達成的動作。例如在Java中,你想要產生某個類別的物件,你得在程式中這麼寫:

Foo obj = new Foo();

編譯時期就得將類別的名稱明確寫在程式中,也就是說,編譯時期就必須讓程式知道這件事。如果你想呼叫某個函式,你得這麼寫:

obj->bar(arg);

函式名稱、引數個數和型別,都必須在程式碼中明確指定。

但有了Reflection,便不再需要在程式碼中明確指定這些東西。例如,程式可以動態地決定究竟要產生那個類別的物件,你可以從設定檔中讀取類別的名稱、根據使用者的輸入值,經過一段邏輯運算之後,決定要產生的類別名稱,接著再利用Reflection的機制,產生類別的物件。你也可以動態地得知產生出來的物件擁有那些成員函式,甚至是否具有特定名稱的成員函式,接著呼叫這些函式。

有了Reflection,程式碼在撰寫及編譯的時間點,毋需明白實際在運行時,究竟會涉及那些類別以及它們各自的行為。你所寫下的程式碼,可以完全是對要處理的類別一無所知,也可以是對他們有一點基本的假設(例如要處理的類別都具有相同名稱的函式,卻沒有實作相同的介面,或是繼承同樣的類別),一切都可以等到執行時期,透過自我審視的能力,了解要面對的對象究竟具備什麼特性,再依據相對應的邏輯,動態利用程式碼控制。

當程式毋需將行為寫死,便消除了相依性
有了如此動態的能力,程式碼在撰寫時毋需將行為寫死,包括要處理的類別、要存取的成員變數、要呼叫的函式等。這大大增加了程式彈性,同時也增加了程式的擴充性。

舉例來說,一個連接資料庫的Java系統而言,在編譯時期是不需要知道究竟運作時會使用那一個JDBC驅動程式,系統只需要透過某種方式,例如在設定檔中指定類別名稱,那麼程式便可以依據這類別名稱,載入相對應的JDBC驅動程式,程式碼中完全可以不涉及具體的JDBC驅動程式究竟為何。

這不僅消除了一定程度的相依性,相較於那些將資料庫連接程式碼以靜態的方式附屬在程式碼中的做法,一旦遇上了必須變更的時候,上述的作法只需更動JDBC驅動程式在設定檔中的名稱,毋需改變任何已經編譯出來的程式碼。

在不更動已編譯好程式碼的情況下,大幅地影響程式的行為,便是Reflection的動態威力所在。

作者簡介


Advertisement

更多 iThome相關內容