動態與彈性 細看編程語言的反射機制
因為可以在程序執行期改變自身的動態語言近年越來越流行,在新一期的編程語言排行榜中,動態語言Ruby得到了穩步提升。這篇文章將向您介紹動態程序設計語言的一個關鍵特性——反射機制。
在不更動已編譯好程序代碼的情況下,卻能大幅地影響程序的行為,這便是反射機制的動態威力所在許多接觸過像Java及C#這類程序語言的程序人,或許對"Reflection(反射)" 這個名詞不陌生,但實際上將這個技巧運作在日常開發工作的程序設計者,可能就并不那么多了。
"Reflection"這個名詞,在維基百科的解釋是“計算機程序用以觀察自身,及修改自身結構和行為的過程”。事實上,透過反射技巧,程序在執行時期本身便能夠得知自己的外觀長相,并且自我修改,甚至自我復制。
反射的作用:得知自己的外觀,甚至自我修改與復制
支持反射機制的程序語言眾多,大多數都是腳本式(Scripting Language)或是以虛擬機器為基礎的程序語言,例如Java、C#、Smalltalk、Python、Ruby、PHP、Perl等。甚至JavaScript也支持Reflection。
反射機制究竟能為程序提供什么樣的作用?為什么程序設計者需要動用到Reflection?針對諸如此類問題的答案,還是要回到為什么程序需要在執行時期得知自己的外觀長相,甚至進一步自我修改、復制。
相對于“執行時期”,未使用反射機制的程序代碼,在編譯時期便已為編譯器所見。對這樣的對象導向程序而言,當某個類別A存在與另一個類別B的互動時,類別B在編譯時期的長相,勢必已經已為類別A所了解。
舉例來說,對于C++程序而言,類別A欲與類別B互動(例如呼叫它的函數),編譯器在編譯類別A的程序代碼時,必須也要能夠得知類別B的宣告及定義。相較于這樣的限制,Reflection則讓你的程序不必在編譯時期便確定此事,而是讓程序得以在執行時期,根據一些外在的信息,決定操作的對象以及操作的方式,毋需于編譯時期便確定、同時寫死這些事情。
由此可以推想,反射機制是一個十分動態的特性,而且看起來可以為程序注入許多的彈性。
運用反射機制審視自身的特性
在解釋究竟反射機制能夠帶來什么好處之前,先來看看具體的Reflection機制,以明白透過常見的Reflection支持,在程序中究竟能做到那些事情。我以Java為例介紹,目的不在介紹Java完整的Reflection API,而是透過Java,幫助大家了解Reflection的一般性概念。
在Java中反射機制的源頭,就是一個叫“Class”的class(在C#中有一個相似的類別,則叫做Type)。這個類別有點特殊,原因在于此類別的每一個對象都用來表示系統中的每一個類別。
具體來說,每個Class對象都描述了每個類別的相關信息,也提供你透過它可以進行的一些操作。想要開始Reflection的動作,就必須先取得Class類別的對象。最常被運用到的兩個途徑,一個便是Object(所有對象皆繼承的類別)所提供的getClass()函數,另一個則是Class類別所提供的forName()靜態函數。
前者讓你得以取得一個對象(尤其是類型未知的對象)所屬的類別,而后者則讓你得以指定一個類別的名稱后,直接得到該類別對應的Class對象。
有了Class對象之后,便能“審視”自身的特性,這些特性包括了它隸屬于那個Package、類別本身究竟是Public還是Private、繼承自那一類別、實作了那些接口等。更重要的是,你可以得知它究竟有那些成員變量以及成員函數(包括建構式)
透過反射,不需在程序中明定函數名稱、自變量個數和類型
透過這個自我審視的過程,程序便能夠了解它所要處理的對象(尤其是類型未知的對象),究竟具備了什么特質。對運用反射機制的程序而言,所了解到的這些特質,便會影響到該程序的運作行為。
取得了某類別的成員變量后(在Java中是以Field類別的對象表示),便可以取得該類別對象的成員變量值,也可以設定其值。同樣的,取得了某類別的成員函數后(在Java中是以Method類別的對象表示),便可取得該成員函數的回傳類型、傳入的自變量列表類型,當然更重要的是,Method類別的對象,可被用以呼叫類別對象的相對應成員函數。
所以假想一個情境,你的程序面臨了一個待處理的對象,但你完全不知道它是那個類型,有什么成員變量、有什么成員函數,但你還是可以察覺出這一切,你會知道每個成員變量的名稱,每個成員函數的名稱、甚至你還可以取得每個成員函數的值、設定它們的值、還可以呼叫每個成員函數,同時傳入正確的自變量、正確地取得回傳值。
除此之外,Java還允許程序人透過Class類別的newInstance()函數,產生該類別的對象,或許是透過Constructor類別對象取得建構式并呼叫、藉以執行不同建構式,以不同方式產生類別的對象。
從以上簡短的描述中,你應當能夠明白,Reflection讓你得以在執行時期處理一些原先在編譯時期才能夠達成的動作。例如在Java中,你想要產生某個類別的對象,你得在程序中這么寫:
Foo obj = new Foo();
編譯時期就得將類別的名稱明確寫在程序中,也就是說,編譯時期就必須讓程序知道這件事。如果你想呼叫某個函數,你得這么寫:
obj->bar(arg);
函數名稱、自變量個數和類型,都必須在程序代碼中明確指定
但有了反射,便不再需要在程序代碼中明確指定這些東西。例如,程序可以動態地決定究竟要產生那個類別的對象,你可以從設定檔中讀取類別的名稱、根據使用者的輸入值,經過一段邏輯運算之后,決定要產生的類別名稱,接著再利用反射機制,產生類別的對象。你也可以動態地得知產生出來的對象擁有那些成員函數,甚至是否具有特定名稱的成員函數,接著呼叫這些函數。
有了反射,程序代碼在撰寫及編譯的時間點,毋需明白實際在運行時,究竟會涉及那些類別以及它們各自的行為。你所寫下的程序代碼,可以完全是對要處理的類別一無所知,也可以是對他們有一點基本的假設(例如要處理的類別都具有相同名稱的函數,卻沒有實作相同的接口,或是繼承同樣的類別),一切都可以等到執行時期,透過自我審視的能力,了解要面對的對象究竟具備什么特性,再依據相對應的邏輯,動態利用程序代碼控制。 當程序毋需將行為寫死,便消除了相依性
有了如此動態的能力,程序代碼在撰寫時毋需將行為寫死,包括要處理的類別、要存取的成員變量、要呼叫的函數等。這大大增加了程序彈性,同時也增加了程序的擴充性。
舉例來說,一個連接數據庫的Java系統而言,在編譯時期是不需要知道究竟運作時會使用那一個JDBC驅動程序,系統只需要透過某種方式,例如在設定檔中指定類別名稱,那么程序便可以依據這類別名稱,加載相對應的JDBC驅動程序,程序代碼中完全可以不涉及具體的JDBC驅動程序究竟為何。
這不僅消除了一定程度的相依性,相較于那些將數據庫連接程序代碼以靜態的方式附屬在程序代碼中的做法,一旦遇上了必須變更的時候,上述的作法只需更動JDBC驅動程序在設定檔中的名稱,毋需改變任何已經編譯出來的程序代碼。
在不更動已編譯好程序代碼的情況下,大幅地影響程序的行為,便是反射機制的動態威力所在。
【編輯推薦】