1. 程式人生 > >C#Assembly詳解

C#Assembly詳解

       Assembly, 這裡把它翻譯為配件或程式集, 以示和元件(Component)加以區別。一個配件有時候是指一個EXE或者DLL檔案, 實際上是一個應用程式(就是指帶有主程式入口點的模組)或者一個庫檔案。但是配件實際上可以是由一個或者多個檔案組成(dlls, exes, html等等), 代表一組資源, 以及型別的定義和實現的集合.。一個配件也可以包含對其它配件的引用。 所有這些資源、型別和引用都在一個列表(manifest)中描述。manifest也是配件的一部分,所以配件是一個自我描述的,不需要其它附加的部件。

       對其描述配件的另一個重要特性是,它是.Net環境下型別標識的一部分,也可以說是基本單位。因為,區分一個型別的標識就是包含這個型別的配件名字加上

型別名本身。

       舉個例子,配件A定義了型別T, 配件B也定義了同名型別T,但是.Net把這兩個型別認為是不同的型別。 注意,不要把配件(assembly)和名稱空間(namespace)混淆起來。其實名稱空間僅僅是用來把型別名用樹的形式組織起來的手段。對於執行是環境來講,型別名就是型別名,和名字空間一點關係都沒有。 總之,記住配件名加上型別名唯一標識一個執行時型別。 另外,配件也是.Net框架用於安全策略的基本單位,許多安全策略都是基於配件的。

       怎樣生成一個配件呢?

       生成一個配件的最簡單辦法就是用.Net

編譯器。例如:下面是一個C#程式ctest.cs

public class CTest
{
    public CTest()
    {
        System.Console.WriteLine( "Hello from CTest" );
    }
}

命令列這樣寫:

csc /t:library ctest.cs

然後,你可以用ILDM檢視一下這個配件中究竟定義了什麼。

產生配件的另外一種辦法是,把多個模組(modules, 它也是由編譯器產生的,對於C#,就是用/target:module選項,用配件聯結器(al.exe)裝配成一個配件。

私有配件和共享配件之間有什麼區別?

私有配件通常只被一個應用程式使用,一般它被儲存在應用程式目錄,或者其子目錄下面.。而共享配件通常儲存在全域性的配件catch緩衝區中, 它是一個由.Net執行時環境維護的配件倉庫。

共享配件通常是許多程式都要使用的程式碼庫,比如.Net框架的類庫就是如此。

事實上, 我們應該如下區分三種配件:

* 私有(private):

只對一個應用程式可見; 這是預設配置, 其它的應用程式不能對其引用,這個配件必須在應用程式目錄或者其子目錄下面有個拷貝.

* 公有(public):

對其它的應用程式可見, 不管它在什麼目錄下面(可以是URL),其它的應用程式都可以對其直接引用.

* 公有共享(public shared):

共享的帶有版本控制的配件的當前實現, 應該使用這種型別. 這種型別特別適合於第三方控制元件.

Net環境怎樣查詢配件?

當然是按照路徑查詢, 規則如下:

* 在應用程式所在目錄及其子目錄下面私有配件

* 對於共享元件, 除了上面的規則, 再加上.Net提供的共享配件緩衝區路徑.

配件怎樣版本化?

我們已經知道所有的型別物件是使用全域性的ID標識的, 那麼配件是怎樣版本化呢?

配件通過版本號控制所謂的版本相容性, 引用配件的時候,就需要給出配件名字和版本號.

版本號分為4個部分(舉例, 5.5.2.33). 分類如下:

不相容: 前兩個不同

可能相容: 前兩個相同, 第3個不同

相容: 前三個相同, 第4個不同

注意: 版本控制只適用於共享配件!

介紹

       在傳統的Windows應用程式開發中,動態連線庫(DLL)為軟體提供了一種重要的可重用機制。同樣元件物件模型(COM)也通過DLLs和EXEs的形式提供了元件重用機制。在.NET的世界裡, 則由assembly(譯者注:可以翻譯為程式集,不過感覺不十分貼切,因此以下均保留了英文原文)提供了類似的可重用的程式碼繫結機制。Assembly中包含了可以在CLR(Common Language Runtime)中執行的程式碼。所有的.NET應用程式都是由一個或多個assembly組成的,不論你在建立一個Console, WinForms,WebForms應用程式或者一個類庫時,實際上你都是在建立assembly。甚至.NET本身也是通過assembly來實現其功能。

       一個assembly可以由一個或者多個檔案組成,簡單來說,你可以把assembly理解成一個邏輯上的DLL。每個assembly必須有一個單獨的執行入口DllMain, WinMain, Main等。Assembly也有一套配置(Deploying)和版本控制(Versioning)的機制。和傳統的DLL等COM元件相比,.NET有著明顯的優點(我們將在後面看到),另外它還可以避免一些諸如DLL相容性等問題的困擾(地獄般的困擾,譯者深有體會),並可以大大簡化配置上存在的問題。

靜態和動態的Assembly

       通常我們可以用Visual Studio.NET或命令列編譯器(.NET SDK中帶的)來生成assembly。

       如果你正確的編譯你的程式碼,assembly就會以DLL或者EXE的形式儲存在磁碟上。像這樣儲存在物理磁碟上的assembly被稱為靜態assembly。

       .NET也允許你通過Reflection APIs來動態生成assembly。(Reflection指獲得assembly資訊以及assembly型別資訊的功能,型別資訊指assembly中的class, interface, member, method等內容。Reflection APIs在System.Reflection名稱空間內)。像這樣的駐留在記憶體裡的assembly被稱作動態assembly。如果需要,動態assembly也可以儲存在磁碟中。

系統需求

       下面我們將主要討論靜態assembly,本文所帶例程需要執行在裝有.NET SDK的機器上(Beta1或者Beta2)。你可以使用Visual Studio.NET來建立單檔案的assembly。

私有的和共享的Assembly

       當啟動一個.NET應用程式的時候,程式首先要檢查自己的安裝目錄中是否有需要的assembly,如果幾個程式執行,那麼每個都要在自己的安裝目錄中查詢自己需要的assembly。也就是說每個程式都使用自己的assembly備份,這樣的assembly稱為私有assembly。它們只在應用程式的安裝目錄範圍內有效。

       一些情況下,你可以發現多個應用程式需要使用共享的assembly而不只是使用他們自己的,對這種情況,你可以在全域性assembly快取(譯者:Global Assembly Cache,這個翻譯有點不倫不類,大家明白就好)中管理該assembly(後面會提到)。這樣的assembly在全域性過程中有效,可以被機器內的所有程式共享,被稱為共享Assembly。如果應用程式不能在自己的安裝目錄中得到需要的assembly,它將在全域性assembly快取中查詢。如果願意你可以考慮把你的assembly成為共享assembly。

Assembly的優點

       在深入assembly細節之前,我們先來大概瞭解一下和傳統的COM元件相比,assembly有那些優點:

       Assembly可以把你從DLL地獄中解救出來。

       DLL地獄很折磨人,典型的COM元件應用通常是把一個單獨版本的元件放在指定的機器上,這樣帶來的問題就是開發人員在更新或者維護元件時常常會因為元件版本的向後相容性的限制而碰釘子。而.NET中解決這個問題的方式很簡單:建一個私有的assembly好了,它將有能力管理同一組件的不同版本,assembly保留其不同版本的copy,如果不同的應用程式需要使用同一組件的不同版本,那麼通過呼叫元件不同的copy就可以。這樣就可以避免元件相容性常常出現的問題。.NET也允許我們跨機器來共享assembly,當然這種共享要受到嚴格的限制。

       Assembly支援並行(side-by-side execution)執行

       這麼說有點不好理解,不過很簡單,也就是說同一assembly的不同版本可以在同一個機器上同時執行。不同的應用程式

可以同時使用同一assembly的不同版本。共享式assembly支援這種並行執行

       Assembly是自描述的

       COM元件需要把一些細節資訊儲存在系統登錄檔或型別庫裡。當使用COM元件的程式執行時,它首先要去登錄檔裡收集元件的細節資訊然後才能呼叫。不象COM元件,.NET Assembly是自描述的,它們不需要把任何資訊儲存在登錄檔裡,所有的資訊都存在於assembly自己的元資料(Metadata)裡了(後面會講到Metadata)。

       配置簡化

       assembly是自描述的,它不依賴於登錄檔儲存資訊,因此完全可以使用XCOPY之類的方法來配置它。

       解除安裝容易

       不需要登錄檔,當然簡單的刪掉就算是解除安裝了。

Assembly的結構

       載建立一個assembly之前,我們先來了解一下assembly的組成結構。Assembly由以下幾部分組成:

       Assembly Manifest(譯者:Assembly清單?不貼切,其實類似於一個目錄或者入口)

       包含assembly的資料結構的細節。

       型別元資料(Type Metadata)

       包含assembly中允許的型別資料。(前面提到過,class, interface,member, property等)

       Microsoft Intermediate Language code (MSIL)

單檔案和多檔案Assembly

       上面提到的assembly結構中包含的東西可以被繫結到一個單獨的檔案裡。這樣的assembly叫單檔案assembly。另外,所有的MSIL程式碼和相關的元資料也可以被分到多個檔案中,這些檔案中每一個單獨的檔案稱為一個.NET Module(模組),.NET module中也可以包括其他一些檔案如影象檔案或資原始檔。

       下面我們瞭解一下assembly manifest的更詳細的資訊。Assembly manifest儲存了assembly細節的資料結構。對多檔案assembly來說,assembly manifest好像一個“繫結器”把多個檔案繫結到一個assembly中。請注意Manifest和Metadata並不相同,Metadata儲存的是在assembly和module裡用到的資料型別(如class, interface, method等)的相應資訊,而Manifest是用來描述assembly本身結構的細節資訊的。

       對單檔案Assembly來說,Manifest嵌在DLL或EXE檔案內,對多檔案assembly, Manifest可以內嵌在每個檔案中也可以存在於一個委託(constituent)檔案裡。後面將會有詳細說明。

       下面列出了Manifest中的主要資訊:

  • *Assembly名字
  • 版本號
  • Assembly執行的機器的作業系統和處理器
  • Assembly中包含的檔案列表
  • 所有assembly依賴的資訊
  • Strong Name資訊

Metadata

       Metadata資料是對assembly中資料的定義。每個EXE或DLL包含自己的詳細的型別資訊,這種資料叫Metadata。主要包括以下資訊:

  • Assembly的名字和版本
  • Assembly暴露出的型別資訊
  • 基本的類和介面資訊細節
  • 安全訪問細節
  • 屬性細節(complier and custom)

Modules

       前面提過Assembly可以有一個或多個Modules組成。Module可以看作是一系列可管理的功能模組。它們轉化為MSIL,一旦程式碼在runtime中執行,它們就可以被加入assembly。請注意module本身並不能執行,要利用它們首先要把它們加到assembly裡。當然一個module可以被加到多個assembly中;配置一個assembly的同時也必須配置所用的modules。

建立單檔案Assemblies

       現在我們瞭解了.NET Assembly的一些基本知識,下面我們可以用C#建立一個簡單的assembly。你可以用VS.NET或者命令列編譯器,下面這個例子可以使用命令列編譯:

using System;

public class Employee
{
    string m_name;
    public string Name
    {
        get
        {
            return m_name;
        }
        set
        {
            m_name=value;
        }
    }

    public int GetSalary()
    {
        //put your logic instead of hard coded value
        return 10000;
    }
}

上面的程式碼說明建立了一個叫Employee的類,該類包含了一個的方法和一個屬性,你可以在文字編輯器中輸入以上程式碼並儲存為employee.cs。用下面的形式做命令列編譯:

csc /t:library employee.cs     (csc是C Sharp Compiler)

執行過上面的命令,你將得到一個叫Employee.dll的檔案,這就是一個單檔案的assembly。

建立多檔案的Assembly

       這裡我們將建立一個叫CompanyStaff的assembly,包括兩個類Clerk和Manager。下面我們看看建立多檔案assembly的兩種辦法:

       第一種方法是分別編譯Clerk和Manager兩個類到不同的modules中,然後把兩個modules加到CompanyStaff DLL中去得到最終的assembly。這時CompanyStaff DLL將管理assembly manifest。這種方法可以用正常的命令列編譯實現。(這裡是CSC)

       第二種方法是分別編譯Clerk和Manager兩個類到不同的modules中,然後生成一個單獨的包含有assembly manifest的檔案,並用這個檔案來表示最終的assembly。這種方法將使用一個叫做AL.EXE的工具來建立assembly。

       使用命令列編譯器建立多檔案assembly

       我們將進行以下步驟:

  • 建立一個叫Clerk的類到一個module中,包含一個方法叫GetClerkName,返回一個數組包含公司內職員的名字。
  • 建立一個叫Manager的類到一個module中,包含一個方法叫GetManagerName,返回一個數組包含公司內經理的名字。
  • 建立CompanyStaff類,包含一個叫做DisplayStaff的方法來例項化Clerk和Manager兩個類並把其中職員及經理的名字簡單的打印出來。把這個類編譯到一個assembly(DLL)中,這時也就將Clerk和Manager的module資訊編譯到最終的DLL中去了。
  • 建立一個客戶端程式來使用該assembly。

Step1: 建立Clerk Module

把下面的程式碼輸入到Clerk.cs中

using System;

public class Clerk
{
    public string[] GetClerkNames()
    {
        string[] names={"Clerk1","Clerk2","Clerk3"};
        return names;
    }
}

用命令列編譯這個類:

csc /t:module clerk.cs

這裡/t:module開關告訴編譯器把程式碼編譯成一個module。

需要說明的是,在beta1中編譯時,如果使用C# compiler,將得到副檔名為.dll的module,如果用VB.NET的complier,得到的副檔名為.MCM。而在beta2種得到的都是副檔名為.NETMODULE的module.

Step2: 輸入下面程式碼到Manager.cs檔案

using System;

public class Manager
{
    public string[] GetManagerNames()
    {
        string[] names={"Manager1","Manager2","Manager3"};
        return names;
    }
}

用下面的命令列形式編譯:

csc /t:module manager.cs

Step3: 建立CompanyStaff assembly

在companystaff.cs檔案中輸入以下程式碼:

using System;

public class CompanyStaff
{
    public void DisplayStaff()
    {
        Clerk c=new Clerk();
        Manager m=new Manager();
        string[] ClerkNames;
        string[] ManagerNames;
        ClerkNames=c.GetClerkNames();
        ManagerNames=m.GetManagerNames();
        Console.WriteLine("Clerks :");
        Console.WriteLine("=======");
        for(int i=0;i<ClerkNames.Length;i++)
        {
            Console.WriteLine(ClerkNames);
        }
        Console.WriteLine();
        Console.WriteLine("Managers");
        Console.WriteLine("=======");
        for(int i=0;i<ManagerNames.Length;i++)
        {
            Console.WriteLine(ManagerNames);
        }
    }
}

用下面的命令列形式編譯:

csc /t:library /addmodule:clerk.dll /addmodule:manager.dll companystaff.cs

這裡/addmodule開關用來把前面建好的兩個module加到CompanyStaff.dll中,也就是一個多檔案assembly中。

Step4: 建立一個客戶程式來使用assembly

在SimpleClient.cs檔案中輸入以下程式碼。

using System;

public class SimpleClient
{
    public static void Main()
    {
        CompanyStaff cs =new CompanyStaff();
        cs.DisplayStaff();
        Console.Write("Press Enter To Exit...");
        Console.ReadLine();
    }
}

用下面的命令列形式編譯:

csc simpleclient.cs /r:companystaff.dll

這樣就準備好了,你可以執行simpleclient.exe,將會列出clerk和manager的名字。

用AL工具建立一個多檔案assembly

       現在我們可以使用AL來建立CompanyStaff assembly了。AL是一個用來整合一個或多個MSIL程式碼檔案或者資原始檔並生成一個帶有管理manifest assembly的工具。和前面例子中的Step1與Step2一樣生成modules。因為我們要在一個獨立的檔案中建立assembly manifest,所以我們不必再親自建立CompanyStaff.dll檔案,我們要用AL來生成它。

輸入下面的命令列:

al clerk.dll manager.dll /out:CompanyStaffAL.dll /t:library

AL命令需要接受MSIL檔案或資源,以空格分開,另外我們還要指定輸出檔名(這裡是CompanyStaffAL.dll,是為了與前面已經生成的檔名區分開)

現在你的assembly準備好了,我們可以建立一個客戶程式來使用它。在前面的例子裡,我們把DisplayStaff方法寫在了CompanyStaff類內,現在,我們可以通過AL得到CompanyStaff.dll,所以我們可以在客戶程式中寫一個同樣的程式碼來實現同樣的功能了。

在SimplaClientAL.cs檔案中輸入下面程式碼:

using System;

public class SimpleClientAL
{
public void DisplayStaff()
{
Clerk c=new Clerk();
Manager m=new Manager();
string[] ClerkNames;
string[] ManagerNames;
ClerkNames=c.GetClerkNames();
ManagerNames=m.GetManagerNames();
Console.WriteLine("Clerks :");
Console.WriteLine("=======");
for(int i=0;i<ClerkNames.Length;i++)
{
Console.WriteLine(ClerkNames);
}
Console.WriteLine();
Console.WriteLine("Managers :");
Console.WriteLine("=======");
for(int i=0;i<ManagerNames.Length;i++)
{
Console.WriteLine(ManagerNames);
}
}

public static void Main()
{
SimpleClientAL cs =new SimpleClientAL();
cs.DisplayStaff();
Console.Write("Press Enter To Exit...");
Console.ReadLine();
}

}

編譯上面的程式碼並執行,你可以得到和前面的例子一樣的結果。

共享式assembly和全域性assembly快取

       到目前為止,我們看到的都是私有式的assembly。當然在.NET應用中,我們多數都在單獨的使用一些私有式的assembly,然而有時候你可能會需要在很多個應用程式中共享一個單獨的assembly的備份。我們前面提到過,共享assembly需要把assembly放到全域性assembly快取中去(Global Assembly Cache)。全域性assembly快取是磁碟上一個特殊的目錄,一般它位於<driver>\WINNT\ASSEMBLY目錄下。注意當安裝過.NET後,這個目錄在explorer下顯示和其他目錄有點不同,如果想看一下它的實際內容,你可以用命令列的形式來檢視。

注意:不能簡單的把你的assembly copy到這個目錄下。首先你需要給你的assembly一個strong name,然後可以用AL把這個assembly安裝到全域性assembly快取中去。

Strong Name

       如果想把assembly設為共享,為了和其他共享的assembly區分開來,每一個assembly都需要一個唯一標誌,這個標誌指的就是Strong Name。 Strong Name是通過公鑰加密技術生成的。一個有私鑰的assembly可以生成和令一個帶有不同私鑰的assembly完全不同的strong name。.NET SDK使用一個叫SN.EXE(Shared Name)的工具來產生這樣的公鑰/私鑰對。

Versioning

       向前面看到的那樣,多數時候,.NET assembly被用作私有模式。對這樣的assembly,因為它們都位於應用程式自己的目錄下,所以versioning看起來並不是十分重要。然而對共享式assembly來說,versioning是很重要的。共享式assembly可以以並行的形式使用(前面提到過並行使用的概念),因此完全有可能在同一臺機器上存在同一個assembly的不同版本。當應用程式要使用一個assembly時候,它就應該提供最近的或以前的版本的資訊。如果開發者需要使用不同版本,就需要在程式碼中明確的設定版本號,其格式如下:

<major version>.<minor version>.<build number>.<revision>

Runtime將通過前兩個引數來決定當前使用的版本是否和以前的版本相容(major version和minor version)。如果這兩個引數有變化,那麼assembly將被認為是不相容的,全域性assembly快取會為該assembly生成一個單獨的入口,如果在程式碼中指定了版本號的話,major version就是可選的了。

下面顯示了.NET如何將同一個assembly(EmployeeShared)的不同版本視為不同的assembly的例子。

建立一個共享式的assembly

       現在你應該已經知道什麼是共享式assembly了,下面我們將建立一個叫EmployeeShared的assembly。建立一個共享式assembly包括以下幾個步驟:

  • 建立assembly程式碼並在程式碼中指定其版本號。
  • 用SN工具建立一個公鑰/私鑰對。
  • 編譯assembly並用上一步中建立的公鑰/私鑰對簽名。
  • 在全域性assembly快取中安裝該assembly。

Step1: 建立assembly程式碼並在程式碼中指定其版本號

在EmploeeShared.cs檔案中輸入以下程式碼:

using System;

using System.Runtime.CompilerServices;

[assembly:AssemblyVersionAttribute("1.1")]
public class EmployeeShared
{
string m_name;
public string Name
{
get
{
return m_name;
}
set
{
m_name=value;
}
}

public int GetSalary()
{
//put your logic instead of hard coded value
return 10000;
}
}

我們建立了這個類,包含一個屬性Name和一個方法GetSalary。注意AssemblyVersionAttribute的用法,它為該assembly設定了版本資訊。

Step2: 用SN工具建立一個公鑰/私鑰對

為了給你的assembly賦一個Strong Name,你需要一個公鑰/私鑰對。可以使用.NET SDK提供的工具SN (Shared Name),輸入以下命令列:

Sn -k employshared.snk

該命令在指定檔案裡建立了一個鑰匙對,引數-k表示我們要把鑰匙對寫到輸出檔案裡。

副檔名.SNK只是個習慣,你可以讓它叫任何名字。

Step 3: 編譯assembly並用上一步中建立的公鑰/私鑰對簽名

現在我們可以用鑰匙對為我們的assembly簽名了,這可以通過在編譯時加上/a.keyfile開關來實現:

csc /t:library employeeshared.cs /a.keyfile:employeeshared.snk

注意如果你在使用VS.NET,你可以更簡單的在AssemblyInfo檔案中指明key檔案。如下所示:

[assembly:AssemblyKeyFile("employeeshared.snk")]

你也可以在這個檔案里加上我們前面提過的般本號屬性而不用在原始碼裡指定。

Step4: 在全域性assembly快取中安裝該assembly

我們的assembly已經用過私鑰簽名了,下面可以把它放在全域性assembly快取中了。像前面一樣使用AL,命令列如下:

al /I:employeeshared.dll

開關/I表示我們要將assembly安裝到全域性assembly快取中。

好了,現在assembly被安裝在全域性assembly快取並且可以使用了。想驗證一下的話到explorer中看一下Assembly目錄。

注意:在Beta2中,安裝assembly到全域性assembly快取中可以使用一個叫GACUTIL的工具。可以使用ILDASM.exe檢視assembly資訊。

有時候你可能需要分析assembly,尤其是別人開發的assembly。這種情況下你可以使用一個叫ILDASM (Intermediate Language Disassembler)的工具。這個工具就象我們從前用過的OLE View或者VB的object viewer一樣,你可以把你的assembly或者module匯入這個工具來檢視assembly各方面的特性,比如它包含的member, method和manifest。看起來就像下面這樣。

你可以通過雙擊樹結構中的節點得到更多的資訊。

總結

assembly是.NET的組成模組。.NET應用程式由一個或者多個assembly組成。一個.NET assembly由一個或多個檔案組成並且在其自身的manifest中儲存自己的註冊資訊。通常一個assembly只為一個應用程式服務,這樣的assembly叫做私有式assembly。你也可以通過配置讓一個assembly的copy為多個應用程式服務。這樣的assembly叫做共享式assembly。共享式assembly在全域性assembly快取中被管理。共享式assembly必須有一個在整個機器範圍內唯一標識的strong name。