範例 1
http://www.codeproject.com/Articles/6334/Plug-ins-in-C
PluginsInCSharp_source.zip
範例 2
http://huan-lin.blogspot.tw/2009/02/dynamically-loaded-dll-with-cshar-4.html
小引
八年前(2001 年),我曾寫過一篇標題為「DLL 應用 - 設計可抽換的模組」的文章,當時的範例是以 Delphi 實作,之後經過一些修改,也成為自己開發 Windows 應用程式的主要框架。後來轉到 .NET 平台,又將此範例分別改寫成 Delphi.NET 和 C# 版本,並於 .NET Magazine 上發表類似的文章,標題是「設計動態載入的 Plug-in 應用程式」,這時候已經是 2005 年了。如今又過了四年,因為 C# 4.0 的 dynamic 型別,便想把這個範例拿出來改一下,看看有甚麼不一樣的地方。
接著會先簡單介紹一下這個框架的基本概念,並示範「動態載入組件,靜態繫結方法呼叫」的寫法。最後再將範例程式改成使用 C# 4.0 動態型別(動態繫結),並比較兩種方式的執行時間。雖然已經知道動態繫結一定比較慢,但結果竟差了十倍之多,還是有點驚訝。
簡介
這次的範例程式雖然和之前的文章採用類似的作法,但去掉了 Windows Forms 的部分,也就是說,僅保留動態載入 DLL 與呼叫 DLL 內含物件的部分,成為更一般化的框架。
前面已經有舊文連結,這裡就不重複太多細節,先看一下這個框架的套件圖好了:
圖 1:套件圖
裡面的 MainApp 就是主程式,Plugin1 和 Plugin2 分別代表可動態載入的模組(DLL 組件)。而 PluginInterface(也是 DLL 組件)就是讓主程式和各 DLL 模組「有點黏又不會太黏」的膠水介面。簡單地說,它的功能主要在避免主程式直接參考各 DLL 模組,藉以降低彼此的耦合度。PluginInterface 可說是主程式和 DLL 模組之間的合約。
以此框架來實作可抽換 DLL 模組時,有三項主要的工作:定義膠水介面、建立 DLL 模組、在主程式中載入並呼叫 DLL 模組中的物件。
Part I:定義膠水介面
這個介面定義了主程式與其他擴充模組之間的合約。我通常將此膠水介面命名為 IPlugin,並且編譯成一個獨立的 DLL 組件。這裡將它命名為 PluginInterface.dll。參考以下程式碼:
程式碼列表 1:IPlugin 介面
1: namespace PluginInterface
2: {
3: public interface IPlugin
4: {
5: void Execute();
6: }
7: }
這個示範性的膠水介面非常簡單,只定義了一個 Execute 方法。也就是說,所有 plugin DLL 都至少要提供一個實作 IPlugin 介面的類別。
Part II:建立可抽換模組
這裡簡單描述一下建立一個可抽換 DLL 專案的步驟:
程式碼列表 2:Plugin1 的 PluginClass
1: using PluginInterface;
2:
3: namespace Plugin1
4: {
5: public class PluginClass : IPlugin
6: {
7: public void Execute()
8: {
9: Console.WriteLine("Inside Execute(): " + DateTime.Now.ToString());
10: }
11: }
12: }
Execute 方法只有一行程式碼,用來顯示當時的時間,這可以幫助我們觀察組件載入後,動態呼叫物件方法時總共花了多少時間。
Part III:在主程式中動態呼叫 DLL 方法
首先,主程式專案也要加入 PluginInterface.dll 組件參考,然後在程式中利用 Reflection 機制動態載入組件(註1),並建立組件中的物件,然後轉型為 IPlugin 介面參考,再透過此介面參考來呼叫物件的方法。參考程式碼列表 3。
程式碼列表 3:主程式動態載入與呼叫 DLL 方法
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Reflection;
6:
7: namespace Main
8: {
9: class Program
10: {
11: static void Main(string[] args)
12: {
13: Assembly asmb = Assembly.LoadFrom("Plugin1.dll");
14:
15: DateTime beginTime = DateTime.Now;
16: Console.WriteLine("Begin time: " + beginTime.ToString());
17:
18: IPlugin obj = (IPlugin) asmb.CreateInstance("Plugin1.PluginClass");
19: obj.Execute();
20:
21: DateTime endTime = DateTime.Now;
22: Console.WriteLine("End time: " + beginTime.ToString());
23: Console.WriteLine("Total: " + (endTime - beginTime).ToString());
24: }
25: }
26: }
注意第 18 行在動態建立物件之後,必須將它轉型成 IPlugin 介面。此範例程式的執行結果如下:
Begin time: 2/6/2009 2:53:50 AM Inside Execute(): 2/6/2009 2:53:50 AM End time: 2/6/2009 2:53:50 AM Total: 00:00:00.1213872
程式計算的執行時間不到 0.2 秒,這並未包含載入組件的時間,而是從建立 plugin 物件開始,直到呼叫的 Execute 方法結束為止。
改用 C# 4.0 動態型別
若使用 C# 4.0 dynamic 型別,在建立物件時就毋需轉型成 IPlugin 介面,故載入 DLL 和呼叫物件方法的部分可改成這樣:
Assembly asmb = Assembly.LoadFrom("Plugin1.dll");
dynamic obj = asmb.CreateInstance("Plugin1.PluginClass");
obj.Execute();
此修改有兩個影響。首先,主程式並不需要參考 PluginInterface.dll 組件,其建立組件、建立物件、以及呼叫物件方法的繫結動作,全都是在執行時期完成。其次,在 Visual Studio 中輸入 "obj." 時,IntelliSense 功能不會提示它有甚麼方法(因為根本不知道它是什麼型別);這是動態型別的一項缺點。
另一個缺點是執行速度較慢。以下是程式改寫後的執行結果:
Begin time: 2/6/2009 2:56:39 AM Inside Execute(): 2/6/2009 2:56:49 AM End time: 2/6/2009 2:56:39 AM Total: 00:00:10.3399824
執行時間和原先靜態繫結的版本竟然差了 10 倍!觀察多次執行的結果,最快也要九秒。我的測試環境是用 Virtual PC 2007 跑 Windows Server 2008(兩個範例程式都是在相同環境上執行)。
靜態繫結 vs. 動態繫結
若採用動態型別,在這個例子當中給我的感覺是主程式和抽換模組之間的耦合更寬鬆,不像膠水介面那樣黏得那麼緊。因為各抽換模組中的類別並不一定要實作 IPlugin 介面,反正只要該類別有提供與 IPlugin 介面相容的方法--更精確地說,只要 .NET runtime 在執行時能夠繫結該方法--主程式就可以順利呼叫它。換言之,PluginInterface 變得有點「僅供參考」的味道了。
若使用靜態繫結的方式,即以膠水介面來銜接主程式和各個擴充模組,三種角色之間的關係當然就緊密一些。即使膠水介面本身非常單純(不包含實作),可是一旦 IPlugin 有變動,例如:增加或移除某個方法,那麼主程式和所有擴充模組就必須重新編譯。此作法除了呼叫方法時比動態繫結還快,另一個明顯的好處是寫程式時有 IntelliSense 的協助,腦袋就不用去記 IPlugin 有哪些方法了。
小結
綜合以上的討論,就動態載入 DLL 模組這個場合,個人還是偏向使用靜態繫結的膠水介面。因為使用 dynamic 型別對此框架所帶來的程式撰寫上的方便並不多,執行速度卻比靜態繫結慢很多。
註1:使用此框架時需注意,.NET DLL 組件一旦載入,就會一直留在記憶體中,直到載入它的主程式結束為止。若要更靈活運用記憶體,就必須使用把 DLL 組件載入到不同的 app domain。