C# 4.0 動態型別應用例:動態載入 DLL 模組

小引

八年前(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 專案的步驟:
  1. 建立一個新的 Class Library 專案,命名為 Plugin1。
  2. 加入組件參考:PluginInterface.dll。
  3. 建立一個類別:PluginClass。此類別必須實作 IPlugin 介面,參考程式碼列表 2。
程式碼列表 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。

6 則留言:

  1. 你好:
    多年前就看過您的文章:DLL 應用 - 設計可抽換的模組,使用delphi去做模組切割。
    請教你在.NET 之下,有類似delphi用bpl的方式去作team work、系統等等的切割嗎?
    還是一樣只能用dll做呢?
    謝謝

    回覆刪除
  2. 在 .NET 之下用 Delphi 的 bpl 啊,印象中好像沒有。後來我用 Delphi.NET 時,也是做成 DLL。由於時間有點遠,且機器上已經沒有裝 Delphi.NET,我也不是很確定它能否邊譯出 .NET bpl。若你有裝 Delphi .NET,不妨看看 .NET 專案模板裡面有沒有這樣的選項。若有的話,應該是 OK 的。

    回覆刪除
  3. 謝謝回應
    那請教一下
    微軟的Dot NET是用什麼方式去做模組的切割?

    回覆刪除
  4. 在 .NET 平台也同樣可以用 DLL 切割模組,只是它的 DLL 是 .NET 組件,跟傳統的 Win32 DLL 不同。

    回覆刪除
  5. 再次感謝你的回應
    再次請教
    在.NET之下,3-tier or Multi tier的架構是要用什麼技術?
    以前有聽過remoting這個名詞
    不知道這個東西跟上述的架構有沒有關係

    回覆刪除
  6. 有關係的。.NET 分散式應用程式架構可以使用的技術包括 Web services、COM+、Remoting 等。這些技術到了 .NET 3.0 之後已經進一步整合成一致的程式設計模型,也就是 Windows Communication Foundation。

    回覆刪除

技術提供:Blogger.
回頂端⬆️