3 程序集
面试出现频率:虽然很重要但不怎么出现,可能会考你定义,以及程序集包括什么,然后自然的话题就跑到反射上去了。
重要程度:8/10,很重要
需要理解的程度:知道程序集包括IL和元数据。知道元数据的作用以及反射的概念。知道GAC是什么。关于反射在后面另有独立章节。对于程序集的强命名,个人认为过于偏僻。
3.1 概念
程序集构成了基于.NET的应用程序的部署、版本控制、重用和安全权限的基本单元。程序集以可执行 (.exe) 文件或动态链接库 (.dll) 文件的形式出现。它们向公共语言运行时提供了解类型实现所需要的信息。可以将程序集看成是构成逻辑功能单元并为一起工作而生成的类型和资源的集合。
如果程序集中含有多个命名空间,则每个命名空间有自己的IL和元数据(即托管模块)。多个托管模块合成一个程序集。CLR是和程序集一起工作的,而不是和托管模块一起。
如果你的程序只是Hello World级的小控制台应用程序,那么编译之后,可能你只会用到.NET最主要的基础类库mscorlib.dll(最重要的程序集之一)。但对于团队级的系统来说,可能会有大量dll文件作为类库。如果你在VS中选择新建一个Class Library,则编译后生成的结果文件是dll文件,没有可执行程序,你也不能在VS中试图运行一个Class Library。
不同程序集中相同的命名空间中相同的成员(例如类型)被认为是不同的。例如My.dll和Your.dll同时在一个命名空间A中定义了一个类B,则它们是不同的。
程序集是自描述的:它的清单部分含有它需要访问的其他程序集(依赖对象)名单,它的元数据包含了程序集中所有类型以及它们的成员。它的IL代码则包括了成员的实现。
程序集是可配置的:可以将其配置到私有或共享(全局程序集缓存,GAC)中。当你在一个类库中引用其他程序集(通过Add References)时,系统将该程序集的dll文件拷贝到你的类库的子目录bin\Debug下(这就是私有配置)。注意Add References不会显示GAC中的程序集。全局的程序集不需要Add References,IDE自动添加。配置到GAC的步骤是一个很偏僻的话题,可参考https://msdn.microsoft.com/zh-cn/library/yf1d93sz.aspx。
3.2 程序集的结构
程序集最重要的两部分是IL和元数据。它们合称托管模块。程序集包括以下部分:
- PE/COFF头:包含了供操作系统查看和利用的信息。Windows操作系统能够加载并运行.dll和.exe是因为它能够理解PE/COFF文件的格式。
- CLR头:告诉操作系统这个PE/COFF文件是一个.NET程序集,区别于其他类型的可执行程序。程序集中包含的IL语言代码并不是计算机可以直接执行的,还需要进行即时编译,那么在对IL语言代码进行编译前,需要先将编译的环境运行起来。
- 清单(manifest):相当于一个目录,描述了程序集本身的信息,例如程序集标识(名称、版本、文化)、程序集包含的资源(Resources)、组成程序集的文件、该程序集需要用到的所有外部程序集名单等。
- 元数据:如果说清单描述了程序集自身的信息,那么元数据则描述了程序集所包含的内容。这些内容包括:程序集包含的模块、类型、类型的成员、类型和类型成员的可见性等。注意,元数据并不包含类型的实现,有点类似于C++中的.h头文件。在.NET中,查看元数据的过程叫做反射(Reflection)。
- IL:也就是元数据中类型的实现,包括方法、属性等。
- 资源文件: 例如图标文件,文本文件,.resx资源文件等。
3.3 元数据的作用
部分元数据的作用:
- IDE通过元数据进行智能感知,例如在你打出一个.之后,自动弹出下拉菜单,获得类型的方法和属性等。
- CLR的代码验证过程使用元数据确保代码只执行类型安全的操作。
- 序列化和反序列化的基础。
- 通过访问元数据来获得类型的成员(即反射)。虽然这会降低性能,但很多时候必须要这么做,例如类型是动态类型,ORM框架即为一个常见的场景。
3.4 程序集和命名空间有何区别?
命名空间是一个程序集内相关类型的一个分组。例如System.IO命名空间包含了有关文件IO的类型。有时,多个程序文件可能共享一个命名空间。例如如果你开发一组几何类圆圈,三角和正方形,你可以将他们的命名空间都设为“Shapes”。
命名空间可以嵌套。例如namespace System.IO等同于
namespace System{
namespace IO{
…
}
}
一个程序集可以包括多个命名空间。在不同程序集中相同名字的命名空间是不同的两个对象。程序集和命名空间的主要区别:
- 程序集是部署,重用应用程序的最小单位,但命名空间不是,它更多的是将具有相似内容的一组类型和方法组织到一起。例如mscorlib.dll中的System命名空间,包含了.NET所有的基元类型。
- 一个程序集可以包括多个命名空间,反之则不行。
- Using引用的对象是命名空间,而不能是程序集。你不能using mscorlib.dll。但当你using 例如System.Data(这是一个嵌套的命名空间)时,你可以使用System.Data命名空间的所有可访问类,属性及方法,就像其代码是你的一部分一样。
3.5 什么是GAC?
当你安装了CLR,你就有了一个Global Assembly Cache(全局程序集缓存,GAC)。安装CLR时,系统将把它认为重要的若干程序集放入GAC,例如mscorlib.dll。从 .NET Framework 4 开始,全局程序集缓存的默认位置为 %windir%\Microsoft.NET\assembly。 在 .NET Framework 的早期版本中,默认位置为 %windir%\assembly。
有时候当安装某些应用程序时,也会触发安装程序将程序集放入GAC。
GAC是一个机器级别的程序集,其中包括mscorlib.dll等至关重要的程序集。在Add Reference中,它不会被自动包括进来,必须手动浏览才可以找到部署到GAC中的程序集。如果你打算将类库部署到GAC,一般来说,这个库应当被大量其他工程引用。
不能把可执行的程序集部署到GAC。部署到GAC的细节,参阅精通C#第14章以及https://msdn.microsoft.com/zh-cn/library/yf1d93sz.aspx。在全局程序集缓存中部署的程序集必须具有强名称。将一个程序集添加到全局程序集缓存时,必须对构成该程序集的所有文件执行完整性检查。
4 综合问题
题目:hello world程序。
using System;
class Program
{
static void Main(string[] args)
{
string text = "hello, world!";
Console.WriteLine(text);
}
}
问:该程序需要引入什么参考?
答:什么都不需要。
问:也就是说你可以把VS帮你引用的所有参考都删了?
答:是。这个程序只需要基础类库。
问:那你都删了之后,Console类型从哪里来?
答:从mscorlib.dll里来。另外,string这个类型也从那儿来,因为string是基元类型,所有的基元类型都在mscorlib.dll的System命名空间。所以你不能把第一行那个using拿掉。
问:为什么我从来没见过mscorlib.dll?
答:因为它在GAC里,每次自动引用。
问:如果我用VS编程,运行程序(非调试模式),会发生什么?
答:VS会先用C#编译器将源代码编译为一个程序集。程序集包括IL代码。因为源代码没问题,所以编译成功,之后,CLR引用程序集中所有需要的其他程序集(这个例子就是没有其他程序集),进行运行时检查,检查也没问题,就开始调用JIT进行即时编译。将IL转换为机器码。机器运行机器码,打印出hello, world!,然后退出程序。
问:你刚刚提到了程序集,那是作什么用的?
答:程序集是部署和重用应用程序的最小单元。它是自解释的,主要包括IL和元数据,以及资源文件等。
问:你接触过或者对程序集进行过访问吗?
答:在反射时会访问程序集中的元数据。
问:反射有什么用处?它对性能是否有影响?
答:且听以后分解。
5 总结与提高
本部分内容虽然比较抽象,平时也基本不会用到,但作为背景知识,了解一下没有坏处。通过熟悉.NET各个版本的更新,我们可以对.NET框架十余年的发展和它所要达到的目标有一个更加明确的认识。.NET的整个发展就是
- 不断统一:例如WCF统一了Web服务曾经有的各种类型的呼叫方式。LINQ统一了各种资源(XML,各类型数据库)的访问和筛选方式,如果你熟悉表达式树,你甚至可以写一个自己的LINQ TO something。统一的过程就是解放开发者的过程。
- 不断解耦:例如WPF相比Winform,更好的做到了将设计和代码分开,真正让两拨人同时工作。最新的ASP.NET Core彻底和System.Web和IIS解耦。只出现需要的东西,不需要的连影子也不能有。
- 提高代码友好程度:C#中有数不胜数的例子,随便举几个:C# 6的$符号,async和await关键字(异步的巅峰),以及那越来越像函数式编程,无处不在的lambda表达式。代码的可读性越来越强,甚至完全不懂编程的人,只要他认识英语,就能看懂大概。
几条主要脉络:
- Web服务:RPC以及其他 -> WCF (SOAP) -> Web API (REST) -> Web API 2 (REST)
- Web应用: ASP -> ASP.NET -> ASP.NET MVC -> ASP.NET Core
- 数据库:ADO.NET -> ADO.NET Entity Framework (ORM)
- 异步编程:委托 -> 事件 -> 任务 -> 任务语法糖
而未来则是函数式编程的世界,Web App的世界,开源的世界,依赖注入的世界,以及nuget的世界。通过学习.NET的演化史,我认为这个平台的未来是光明的。熟悉.NET的历史,你可以令人信服的证明你对.NET充满兴趣,在和面试官闲聊时,也是不错的谈资,特别是面试官本人也是技术大牛时,他可能会觉得你是个可造之材。如果你资历深厚,甚至了解.NET出现之前业界的状况,那么你对.NET对整个开发产业的改变一定有着比我深入更多的认识,甚至你可以猜测.NET将来的发展方向。
对于程序集这部分,实际上还是有比较多机会接触到的,了解程序集对后面反射,动态类型和晚期绑定等很多内容的学习大有帮助。