【C#基础概念】程序集与托管模块的概念
阅读原文时间:2023年07月08日阅读:1

本文是为了学习程序集而整理的网上资料,主要包括两个部分,概念和使用,前部分讲怎样理解程序集,后部分讲述怎样使用的细节。

"程序集与托管代码块"(摘自Himage的blog),希望大家看了此篇文章后对程序集的概念清楚一点

    如果你正在开发面向DotNet平台的应用程序,那么你肯定对“程序集”和“托管模块”这两个概念不陌生,这是DotNet带来的术语。这两个概念很容易混淆,有人认为它们指的是同一样事物,其实不然。这里,我写下自己的一些理解。

       为了便于说明问题,我们先看看一个公司的某项目开发团队,这个“团队”由团队“成员”组成(可能还有一些资源),而在这些团队成员中,必然有一个成员是这个团队的头头,为了表示不是普通的成员,头头都有一个称号,比如说“经理”或者“负责人”。经常的,说到某个团队,我们只要知道它的头头就可以了。也就是说,头头代表了这个团队(注意,这里是"代表")。如果其他人有什么事情要与这个团队交流(比如团队向外说明本团队完成了什么任务等),没有必要将每个成员找来交流,我们只要找到头头就可以了。 因为头头知道本团队的一切,他掌握了关于团队的所有信息:本团队有哪些成员,而且有些什么资源可以利用,与其他哪些团队有交流等等。常见的团队有多个成员,一个成员的团队很少见(头头也是他),但为了说明问题,我们假定一个成员的团队和多个成员的团队都存在。

    从上面的讨论中我们可以这样总结:团队是个逻辑概念,并不是指某个人或资源,它是一个集合,而且这个集合不为空,只有当拥有大于或者等于多个成员的时候它才称之为团队。

       嘿,大家看出来了,我之所以要说项目开发团队和成员,是因为在DotNet中的程序集和托管模块的概念与此类似。程序集其实并不是说某个文件,它是一个逻辑概念,就像一个团队。当然,我们习惯说a.exe 或者b.dll是个程序集,其实这样说多少会让人混淆,请看下面的说明。而托管模块就像是团队中的团队成员。哈,你肯定想到了,其中必然有一个托管模块是整个“团队”的“头头”,它负责管理所有的托管模块,关于这个程序集的一些信息也保存在这个托管模块中。我们把这样的模块叫做“主托管模块”(我记得某本书上是这样叫的)。为了区分主托管模块和普通托管模块,怎么办呢?这个好说,人最擅长给自己挂头衔了,借鉴一下。我们给普通托管模块和主托管模块不同的后缀名,普通托管模块的后缀名是.netmodule,而主托管模块的后缀名是.exe 或者.dll。和开发团队中头头代表整个团队一样,主托管模块代表着整个程序集。既然是主托管模块,那么它肯定与其他托管模块不一样。一般的托管模块包含有IL代码和元数据,而这个主托管模块有没有IL代码和元数据并不重要,但它一定要有“清单”,也就是关于整个程序集的数据。

为什么主托管模块会有两种后缀名呢?这是因为分工的原因,有的程序集是为其他程序集提供便利的,它实现了一些数据类型(类库)或者拥有某些资源――这就是后缀名为.dll的程序集。这样的程序集不喜欢抛头露面,它们是“幕后程序集”。另一种程序集,后缀名为.exe,用户直接和它们打交道。有的功能它自己实现,但有些呢就交给幕后程序集去实现。为了区别,我们就把这样的程序集叫做“前台程序集”吧。DotNet规范中没有这两个概念,这里是为了理解才造的词。当然,读者朋友都会有自己比较习惯的理解名字。一个可执行应用程序中,只有一个前台程序集的概念,但是可以有零个或一个或多个幕后程序集。

我们又回到实际生活中的开发团队概念上来。团队的组成有这样几种情况:

a.一个成员,这样的成员是比较厉害的那种,比如个人软件开发者,自己对自己负责,自己是自己的头头;

b.多个成员,其中某个成员是头头;

程序集与此类似,所以就有了单模块和多模块程序集(有时也称单文件程序集和多文件

程序集)。如果要实现的功能不是很烦杂,那么就用单模块程序集吧;但是,如果要实现的功能比较多,而且也好分开,那么建议你用多模块程序集吧,如果以后某个功能的实现方案改变的话,只要修改这个模块就行了,这样一来,极大的降低了开发的烦杂度。还有几点要注意,有的托管模块并不实现某种计算功能,它们仅仅是提供一些资源,比如说字符串;有时程序集除了包含托管模块外,还包含一些另外的资源文件。上面说的好像不是很直观,我们再结合下面的几幅图来理解。  

   
我相信现在大家对程序集和托管模块分别是什么以及两者间的关系有了较好的理解。但是如果有源代码辅助一下那就更好了。是的。对程序员来说,源代码比什么都亲切。好的,下面就举两个简单的例子(用C#语言表述)。一个是单模块程序集,一个是多模块程序集。两者都是前台程序集(后缀名是exe)。我用Visual
C# 2003 集成开发环境试了一下,竟然发现它不支持多程序集的开发(希望是我没有找到)。没关系,我们还有DotNet FrameWork
SDK呢,不用怕。它自带的C#编译器csc.exe很好用。至于csc的用法我就不多说了。

例子1。单模块程序集:

首先找个文本编辑器,将下面的代码敲进去,然后将它保存起来,取名为hello.cs。 例如我把它存为e:"test"hello.cs。

/*

 *hello.cs 在控制台上显示一行字符串

 */

using System;

namespace nsApp

{

    public class CEnterPoint

    {

       static void Main()

       {

           Console.WriteLine("Hello, verybody!");

       }

    }

}

然后,打开SDK命令行提示,定位在hello.cs所在的文件夹,比如我的是e:"test。在命令行提示中输入命令
csc hello.cs,这个命令默认的是生成 .exe文件,也就是一个主托管模块。
现在到hello.cs所在的文件夹中看看,你会发现一个新文件hello.exe。好,一个主托管模块诞生了。既然有主托管模块,那么就标志着一个程序集的诞生。再看看,没有其他的托管模块,那么这个程序集就是一个单模块程序集。

例子2。多模块程序集:

假设我们要编一个程序来管理动物园。这里为了简单,不考虑继承等问题,每个动物用一个类来表示,并且用一个托管模块来实现。将所有的动物都放在命名空间nsZoo中。现在动物园有两种动物:Dog和Cat。看下面的代码:

/* Dog.cs */

using System;

namespace nsZoo

{

    public class Dog

    {

       public void SayHello()

       {

           Console.WriteLine("I am a dog, wang wang wang");

       } 

    }

}

/* Cat.cs */

using System;

namespace nsZoo

{

    public class Cat

    {   

       public void SayHello()

       {

           Console.WriteLine("I am a cat, miao miao miao");

       }

    }

}

/* Main.cs */

using System;

using nsZoo;

namespace nsApp

{  

    class CEnterPoint

    {   

       static void Main(string[] args)

       {   

           Dog aDog = new Dog();

           Cat aCat = new Cat();

           aDog.SayHello();

           aCat.SayHello();

           Console.ReadLine();

       }

    }

}

将上面的三个文件保存好,比如放在文件夹e:"zoo 中。再一次打开SDK命令行提示,定位到e:"zoo  

输入命令 csc /t:module Dog.cs 看看文件夹,你会发现一个新文件 Dog.netmodule

哈,这是一个托管模块,普通的托管模块。

接着输入命令 csc /t:module Cat.cs 同样得到一个普通托管模块 Cat.netmodule 。好现在有了两个普通托管模块。

为了实现一个程序集的梦想,必须还要有一个主托管模块。好,接着输入

csc /addmodule:Dog.netmodule;Cat.netmodule Main.cs  你会在文件夹中发现一个名为Main.exe的文件,这就是你想要的主托管模块。

好了。现在你有三个托管模块,并且任命其中一个为主托管模块,这样你就拥有了一个程序集了。这个程序集的组成是:Dog.netmodule,
Cat.netmodule, Main.exe 。我们习惯说这个程序集是Main.exe 。但是一定要知道,Main.exe
其实是一个程序集的“头头”,由它代表着整个程序集。

相信有了上面的介绍之后大家对程序集的概念及建立都有点清楚下面的文章载自博客圆:http://www.cnblogs.com/stzyw/archive/2005/08/25/222361.aspx

任何.NET二进制代码使用的.NET运行时是一个或一组程序集组成的。当你编译一个应用程序时,实际上是在创建一个程序集。

任何时候构建一个EXE或DLL文件时必须使用/t:library编译参数创建与该应用程序相对应的包含清单(manifest)的程序集,清单(manifest)记录了.NET运行时程序集的相关信息。另外,使用/t:module编译参数,  当编译应用程序或使用程序集生成工具(the Assembly Generation tool)时必须使用/addmodule开关将这个DLL文件加入到其他程序集中。

清单数据(Manifest Data)

程序集的清单有不同的方法存储。若编译一个独立的应用程序或DLL文件,清单与程序或DLL的PE结合在一起,这称为单一文件的程序集(single-file assembly)。多文件程序集(multifile assembly)是指清单以另外一个独立实体的形式作为程序集的一部分,或者作为程序集中的一个模块。

同样程序集的定义很大程度取决你如何使用它。从客户的角度来看,程序集是模块、导出类型、(必需、可选)资源的命名和版本化(versioned)的集合。从程序集创建者的角度来看,程序集是客户能使用的一组相关联的模块、类型、资源、导出的方法的封装包(mean of packaging)。也就是说,清单是程序集和用户之间沟通的桥梁。下面列出程序集中清单包含的信息:

·          程序集名称(Assembly name)

·          版本信息(Versioning information)

        版本信息是由四个不同的部分组成的版本号,分别是主版号、子版号、构建序号、修订号。

·          (可选的)共享名和带符号的程序集散列(An(Optional)shared name and signed assembly hash)

        主要是关于程序集部署的相关信息。

·          文件(Files)

         包含在程序集中的文件列表。

·          引用的程序集(Referenced assemblies)

         直接引用的外部程序集列表。

·          类型(Types)

        程序集内部的所有类型及模块映像包含的类型的列表。(This is list of all types in the assembly with a mappig to containing the type.)

·          安全(Security)

        安全权限的列表,权限已明确地被程序集抛弃。(permissions This is a list of security permissions that are explicity refused by the assembly.)

·          定制属性(Custom attributes)

·          产品信息        包含公司、商标、产品及版权信息。

程序集的优势

·          程序集的封装:将多个模块封装到一个物理文件,起到性能优化的作用。当你创建一个应用程序并使用多文件程序集时,.NET运行时只需加载相关的模块。这种策略起到减少应用程序的工作集。

·          程序集的部署

             在.NET框架中程序集是最小的部署单元。虽然它诱人的说,程序集是部署应用程序的方法,但是技术上并不是如此。许多关于程序集部署的正确的观点是:在.NET中程序集应该看作一个窗体的类部署(class deployment)-就像Win32中的一个DLL文件。一个单独的应用程序是由多个程序组成的。

             因为程序集是自描述的(self-desciribing),所以部署程序集最简单的方法就是直接把程序集复制到目的文件夹。当尝试运行包含这个程序集的应用程序时,清单将向.NET运行时提供包含在这个程序集中的方法信息。另外,清单(manifest)也向应用程序提供该程序集所引用的外部程序集的相关信息。

             许多通过公共方法部署的程序集依然是私有的程序集,即程序集被复制到文件夹,但它们不是共享的。缺省情况下程序集是私有的,除非明确地指定程序集为共享程序集(shared assembly)。

·          程序集的版本

使用程序集另外一个主要优势是程序集内置了版本号。程序集中止了DLL地狱。DLL地狱是指当一个应用程序重写了一个被其他应用程序引用的DLL文件,而该应用程序引用的是低版本的同名DLL文件,这就造成了引用低版本DLL的应用程序运行出错,这种情况。虽然Win32资源文件格式内置了版本资源类型,但是操作系统不会强制执行任何版本控制,以便依赖这个DLL文件的应用程序能正常运行。

基于这个问题,清单不仅包含程序集本身的版本信息,也包含了该程序集所有被引用的程序集和这些程序集的版本信息。因为有了这个体系结构,.NET运行时能确保版本规则被支持,当出现新版本的程序集时,版本不兼容的共享DLLs由操作系统自动安装,使得应用程序可以继续正常运行。

用多个模块创建程序集(Creating Assembies with Multiple Modules)

模块是基于DLL,但是它不是真正意义上的DLL,它必须被包含程序集中。示例:

// NetModuleTestServer.cs

// Build with the following command-line switches:

//     csc /t:library NetModuleTestServer.cs

public class NetModuleTestServer

{

    public static void Bar()

    {

        System.Console.WriteLine("NetModuleTestServer.Bar(NetModuleTestServer.netmodule)");

    }

}

// NetModuleTestClientApp.cs

// Build with the following command-line switches:

//

// csc addmodule:NetModuleServer.netmodule NetModuleTestClientApp.cs

using System;

using System.Diagnostics;

using System.Reflection;

class NetModuleTestClientApp

{

    public static void Main()

    {

        Assembly DLLAssembly = Assembly.GetAssembly(

            typeof(NetModuleTestServer));

        Console.WriteLine(""nNetModuleTestServer.dll Assembly " +

            "Information");

        Console.WriteLine(""t" + DLLAssembly);

        Process p = Process.GetCurrentProcess();

        string AssemblyName = p.ProcessName + ".exe";

        Assembly ThisAssembly = Assembly.LoadFrom(AssemblyName);

        Console.WriteLine("NetModuleTestClient.exe Assembly " +

            "Information");

        Console.WriteLine(""t" + ThisAssembly + ""n");

        Console.WriteLine("Calling NetModuleTestServer.Bar");

        NetModuleTestServer.Bar();

    }

}

创建共享程序集

若要创建一个在多个应用程序使用的程序集,并且版本信息相当重要,则必须共享程序集。若要共享程序集,必须给程序集指定强名称。可以通过.NET SDK提供强名称工具(the Strong Name tool)创建强名称。使用强名称有四个主要理由:

·          它是.NET生成唯一的全局名称的机制

·         
因为强名称工具生成的密钥对包含了一个签名,你可以判断程序集创建之后是否被篡改。(Because the generated key pair
includes a singature,you can tell whether an assembly has been tampered
with after its original creation.)

·          强名称能防止第三方在你构建的程序集的基础上发表新的版本。这是因为密钥对包含签名起了作用,因为第三方不知道你的私钥。

·         
当.NET加载程序集时,运行时会自动验证调用者是否合法。(When .NET load an assembly,the runtime can
verify that the assembly came from the publisher that the caller is
expecting.)

创建强名称的步骤:

·          使用强名称工具为程序集创建一个强名称。示例:

sn -k InsideCSharp.key

·          在客户端源代码文件中添加属性AssemblyKeyFile,把强名称指定给程序集。

使用全局程序集缓存工作

.NET每次加载程序集时都会创建一个代码缓存区,通常成为全局程序集缓存。使用全局程序集缓存有三个作用:

·          存储从Internet或文件服务器下载的代码。注:从一个特定的应用程序加载的代码存储在缓冲中的私有区域,以防止其他程序集访问。

·          存储被多个.NET应用程序共享的组件数据。利用全局缓存工具将程序集加载到缓存全局区域,使得该程序集可以被本地机器的所有应用程序访问。

·          程序集的本地代码是预运行时编译的,同时存储在全局程序集缓存区。(native code versions of assemblies that have been preJITted are stored in the cache.)

缓存视图

1、在windows"assembly文件夹中可以查看程序集的相关信息,如版本号、语言(culture)、公钥标记等等信息。

2、在命令行使用gacutil。其中-i参数:加载程序集;-u参数:卸载程序集(可以指定版本号)、-l参数:列出程序集的相关信息。

// DllTestServer.cs

// Build with the following command-line switches:

//     csc /t:library DllTestServer.cs

public class DllTestServer

{

    public static void Foo()

    {

        System.Console.WriteLine("DllTestServer.Foo " +

            "(DllTestServer.DLL)");

    }

}

// DllTestClient.cs

// Build with the following command-line switches:

//     csc DllTestClient.cs /r:DllTestServer.dll 

using System;

using System.Diagnostics;

using System.Reflection;

class DllTestClientApp

{

    public static void Main()

    {

        Assembly DLLAssembly =

            Assembly.GetAssembly(typeof(DllTestServer));

        Console.WriteLine(""nDllTestServer.dll Assembly " +

            "Information"); 

        Console.WriteLine(""t" + DLLAssembly);

        Process p = Process.GetCurrentProcess();

        string AssemblyName = p.ProcessName + ".exe";

        Assembly ThisAssembly = Assembly.LoadFrom(AssemblyName);

        Console.WriteLine("DllTestClient.exe Assembly " +

            "Information"); 

        Console.WriteLine(""t" + ThisAssembly + ""n");

    Console.WriteLine("Calling DllTestServer.Foo");

    DllTestServer.Foo();

    }

}