2010年9月14日 星期二

多型 (polymorphism), 多緒 (multithreading),回呼 (call-back)...etc


一旦函數可以被傳遞、被紀錄,這開啓了許多可能性,産生許多有趣的應用,特別是下列三者:

* 多型 (polymorphism):稍後再說明。
* 多緒 (multithreading):將函數指標傳進負責建立多緒的 API 中:例如 Win32 的 CreateThread(...pF...)。
* 回呼 (call-back):所謂的回呼機制就是:「當發生某事件時,自動呼叫某段程式碼」,Charles Petzold 稱此爲「Don’t Call Me, I'll Call You」。事件驅動 (event-driven) 的系統經常透過函數指標來實現回呼機制,例如 Win32 的 WinProc 其實就是一種回呼,用來處理視窗的訊息。

函數指標的致命缺點是:無法對參數 (parameter) 和返回值 (return value) 的型態進行檢查,因為函數已經退化成指標,指標是不帶有這些型態資訊的。少了型態檢查,當參數或返回值不一致時,會造成嚴重的錯誤。編譯器和虛擬機器 (VM) 並不會幫我們找出函數指標這樣的致命錯誤。所以,許多新的程式語言不支援函數指標,而改用其他方式。

多型 (polymorphism)

多型的實現方式很複雜,大致上是編譯器或 VM 在資料結構內加入一個資料指標,此指標通常稱爲 vptr,是 Virtual Table Pointer 的意思。vptr 指向一個 Virtual Table,此 Virtual Table 是一個陣列 (array),由許多函數指標所組成,每個函數指標各自指向一個函數的地址。如圖 1 所示。

對 1

不管是 C++ 編譯器、或是 Java VM、或是 .NET CLR,內部都是以此方式來實現多型。儘管如此,這只能算是 black magic,對於 C++、Java 與 .NET 語言來說,函數指標「並未因此」和語言本身有直接相關。換句話說,C++ 和 Java 與 .NET 語言,就算語法本身不支援函數指標,照樣也能實現多型。事實上,C++ 固然支援函數指標,但不是爲了多型的關係,而是爲了和 C 相容 (畢竟 C++ 是 C 的 superset);IL Asm (.NET 上的組合語言) 固然支援函數指標,但由於安全的理由,使用上受到相當大的限制,且不是爲了多型的關係。至於 Java 與 C# 則都不支援函數指標。

沒 錯,Java 與 C# 都不支援函數指標。雖然剛剛解釋過,這不會影響對於多型的支援,但是這會不會影響對於多緒 (multithreading) 與回呼 (call-back) 機制的支援呢?答案是:不會!因為 Java 可以利用多型或反映 (reflection) 來實現多緒與回呼,而 C#可 以利用多型或反映或委託 (delegate) 來實現多緒與回呼。


反映(reflection)

顧名思義,反映 (reflection) 機制就像是在吳承恩所著的西遊記中所提及的「照妖鏡」,可以讓類別或物件 (object) 在執行時期「現出原形」。我們可以利用反映機制來深入瞭解某類別 (class) 的建構子 (constructor)、方法 (method)、欄位 (field),甚至可以改變欄位的值、呼叫方法、建立新的物件。有了反映機制,程式員即使對所欲使用的類別所知不多,也能照樣寫程式。反映機制能夠用來 呼叫方法,這正是反映機制能夠取代函數指標的原因。

以 Java 來說,java.lang.reflect.Method (以下簡稱 Method) 類別是用來表示某類別的某方法。我們可以透過 java.lang.Class (以下簡稱 Class) 類別的許多方法來取得 Method 物件。Method 類別提供 invoke() 方法,透過 invoke(),此 Method 物件所表示的方法可以被呼叫,所有的參數則是被組織成一個陣列,以方便傳入 invoke()。

舉個例子,下面是一個名為 Invoke 的程式,它會將命令列的 Java 類別名稱和要呼叫的方法名稱作為參數。為了簡單起見,我假定此方法是靜態的,且沒有參數:

=======================================
import java.lang.reflect.*;

class Invoke {
public static void main(String[] args ) {
try {
Class c = Class.forName( args[0] );
Method m = c.getMethod( args[1], new Class [] { } );
Object ret = m.invoke( null, null );
System.out.println(args[0] + "." + args[1] +"() = " + ret );
} catch ( ClassNotFoundException ex ) {
System.out.println("找不到此類別");
} catch (NoSuchMethodException ex ) {
System.out.println("此方法不存在");
} catch (IllegalAccessException ex ) {
System.out.println("沒有權限調用此方法");
} catch (InvocationTargetException ex ) {
System.out.println("調用此方法時發生下列例外:\n" +
ex.getTargetException() );
}
}

}
=======================================

我們可以執行 Invoke 來取得系統的時間:
java Invoke java.lang.System CurrentTimeMillis

執行的結果如下所示:
java.lang.System.currentTimeMillis() = 1049551169474

我們的第一步就是用名稱去尋找指定的 Class。我們用類別名稱 (命令列的第一個參數) 去呼叫 forName() 方法,然後用方法名稱 (命令列的第二個參數) 去取得方法。getMethod() 方法有兩個參數:第一個是方法名稱 (命令列的第二個參數),第二個是 Class 物件的陣列,這個陣例指明了方法的 signature (任何方法都可能會被多載,所以必須指定 signature 來分辨。) 因為我們的簡單程式只呼叫沒有參數的方法,我們建立一個 Class 物件的匿名空陣列。如果我們想要呼叫有參數的方法,我們可以傳遞一個類別陣列,陣列的內容是各個類別的型態,依順序排列。

一旦我們有了 Method 物件,就呼叫它的 invoke() 方法,這會造成我們的目標方法被調用,並且將結果以 Object 物件傳回。如果要對此物件做其他額外的事,你必須將它轉型為更精確的型態。

invoke() 方法的第一個參數就是我們想要呼叫目標方法的物件,如果該方法是靜態的,就沒有物件,所以我們把第一個參數設為 null,這就是我們範例中的情形。第二個參數是要傳給目標方法作為參數的物件陣列,它們的型態要符合呼叫 getMethod() 方法中所指定的型態。因為我們呼叫的方法沒有參數,所以我們傳遞 null 作為 invoke() 的第二個參數。

以上是 Java 的例子,事實上,.NET 的反映機制也相去不遠,不再贅述。反映機制是最動態的機制,比多型的功能更強大。然而,反映的速度比多型慢許多 (而且多型又比函數指標稍慢),所以若非必要,應該少用反映機制。事實上,不管是 Java API 或 .NET Framework,都不使用反映機制來實現回呼與多緒。

多緒 (multithreading)

Java沒有函數指標(為了系統安全),也不用反映機制來處理多緒(一方面為了效率,二方面反映機制是在JDK1.1才開始支援),而是使用多型的機制來處理多緒,作法如下:(另一種作法是實作java.lang.Runnable介面,與下面的作法雷同,不另說明。)

將執行緒的程式寫在下面的run()方法中:

class MyThread extends java.lang.Thread {
public void run() {
// ...
}

}

再利用下面的方式來啟動此執行緒:

MyThread thread = new MyThread();
thread.start();

start() 方法定義在 java.lang.Thread 類別內,start() 方法會請作業系統建立一個執行緒,再呼叫
run(),此時調用的並非在 java.lang.Thread 內定義的 run() (它是空的),而是利用多型機制,調用到 MyThread
內定義的 run()。

Java的回呼(Call Back)

通常回呼機制都是使用 publisher/subscriber (出版者/訂閱者) 的方式,必須先向系統註冊:

  • 何事件:我對何種事件感興趣
  • 何函數:當事件發生時,請呼叫我的函數,以爲通知。此函數即爲回呼函數 (call-back function)。

Java 也是使用類似的作法,差別在於 Java 無法利用函式指標,且採用物件導向的作法。Java 將出版者 (publisher) 稱爲事件來源 (event source),將訂閱者 (subscriber) 稱爲事件傾聽者 (event listener)。大致的作法如下:

  • 向事件來源註冊 (registry) 事件傾聽者
  • 該事件發生時,事件來源通知 (notify) 事件傾聽者

事件來源提供名爲 addXxxListener() 的方法來讓事件傾聽者註冊之用,此方法需要傳入事件傾聽者當參數。至於是何種事件,則由 Xxx 以爲識別。例如:addMouseListener() 表示註冊「滑鼠事件」的事件傾聽者。

利用 addXxxListener(),事件來源就可以將事件傾聽者記錄在欄位 (field) 中。當事件發生時,事件來源就可以從欄位中知道該通知和物件。可是應該呼叫該物件的那個方法呢?如果該物件沒有提供該方法呢?

想要解決此問題,事件來源就必須過濾註冊的對象,addXxxListener() 所需要的參數不可以是籠統的 java.lang.Object,而必須是一個實作 XxxListener 介面 (interface) 的物件。只要在 XxxListener 介面內宣告一個比方說 XxxEventHappened(),那麽任何實作 XxxListener 的物件,都必定有實現 XxxEventHappened(),所以事件來源就可以在事件發生時,呼叫事件傾聽者的 XxxEventHappened()。這正是依靠多型機制才能達成。

事件來源通知事件傾聽者時,往往需要夾帶一些額外的訊息,例如:事件來源是誰、事件發生於何時、事件發生的原因為何…。這些訊息被封裝成事件物件,當作參數傳給 XxxEventHappened()。

例如,我要向一個名為 jButton1 的 javax.swing.JButton 物件註冊,成為它的事件傾聽者,那麼就必須實作 java.awt.event.ActionListener 介面,提供 actionPerformed() 方法,如下所示:

class MyActionListener implements ActionListener {
public void actionPerformed(ActionEvent evt) {
// ...
}
}

註冊的方式如下:
MyActionListener mal = new MyActionListener();
jButton1.addActionListener(mal);

使用多型的機制來實現多緒和回呼,不但麻煩 (必須繼承),也不能使用靜態方法,因為靜態方法本來就沒有多型的機制 (static 一定是 non-virtual)。順便一提,前面所提到的反映機制,可以支援靜態方法 (static method)。


********************** 我 是 分 隔 線 *************************
更多關於Polymorphsim
http://www.inote.tw/2008/02/java-polymorphism.html
Polymorphism又稱為多型,他的討論要在類別間有繼承關係才有意義,而他的定義很少發現有人能一次說得很清楚,所以海芋就以幾句話來帶過他。

簡單來說,多型是指子類別繼承父類別時,同時改寫父類別的函式或變數,而我們可以宣告一個屬於父類別的變數,去存取子類別中改寫自父類別的函式或變數,這我們稱之為多型。

舉個生活一點的例子,若有一個父類別是動物,那麼有一個子類別鳥繼承自動物這個類別,當我們看到鳥的時侯,可以說牠是一隻鳥,也可以說牠是一隻動物。同樣的東西,卻有不同的表示型態。
而舉個程式碼而言:

而在此例子中,animal是宣告給父類別,所以他只能認識父類別擁有的函式或變數,所以他能存取「getLegs()」這個函式。
而另一個要記的重點是,父類別若轉成子類別需要靠強迫轉型,子類別轉成父類別則是屬於自動轉型,而兄弟類別之類亦可以使用強迫的方式來轉型喔!


沒有留言:

張貼留言