.NET面试题系列[1] - .NET框架基础知识(1)
.NET框架基础知识(1)
参考资料:
- http://www.tracefact.net/CLR-and-Framework/DotNet-Framework.aspx (非常经典的一篇文章)
- 精通C# (第六版)
- CLR via C# (第三版)
1 术语
面试出现频率:从来没人问过。事实上我都不知道怎么问,考背书吗?倒是可以问问知不知道现在.NET最新版本是什么,考察面试者是否对新技术足够敏感。
重要程度:3/10
需要理解的程度:知道这些缩写(CLR,BCL,FCL,CTS,CLS)各代表什么即可。仔细读一遍http://www.tracefact.net/CLR-and-Framework/DotNet-Framework.aspx
1.1什么是.NET框架?在各个平台版本中,有什么值得强调的更新?
.NET框架是以一种采用系统虚拟机(即CLR)运行的,面向CLR的编程平台,以CLR为基础。.NET的基础类库运行于CLR之上(类比Java的虚拟机),作为其他各种功能的基石。.NET框架支持多种语言(C#、F#、VB.NET、C++、Python等)的开发。它的前身是Windows DNA。现在.NET框架的扩展性甚至超过Java,其的Mono为Mac OS提供了支持,Xamarin可媲美安卓开发,可以在任何手机上开发。
.NET框架是开源的。它的代码在https://github.com/dotnet/。如果你的commit有幸被接受,即使改动有多么微小,也是无上的荣耀,你绝对应该把它写到你简历的第一行,这个成就可以和“为Linux内核优化做过贡献”相比,那可比曾经在BAT做过几年的经历牛逼多了。
所有.NET支持的语言编写出来的程序,在支持.NET的编译器编译之后,会先产出程序集,其主要内容是IL和元数据。之后,JIT再将其翻译为机器码。
甲骨文公司的Java EE是.NET平台的竞争对手之一。
.NET框架现在已经出到了版本4.6.1。在3.0之前,.NET框架的Web解决方案是ASP.NET(Webform & MVC),数据库连接为ADO.NET(支持过去的ODBC,OLE等,并支持SQL Server和Oracle),Windows Form则作为Windows下的应用解决方案。
.NET最重大的一个版本更新是3.0,其中,提出了WCF(统一了过去Web服务混乱的形式,形成了一个统一的格式,并采用SOAP),WPF(作为Windows form的增强版)以及WF。
.NET3.5集成了LINQ。另外Entity Framework取代ADO.NET,它对应VS2008。
.NET4.0提出了任务并行库和PLINQ。
.NET 5 (即.NET Core 1.0)在2016年6月27日推出。是次推出伴随着ASP.NET Core (即ASP.NET 6)和Entity Framework 7。这些产品将支持Windows,OS X和Linux三种操作系统。
新版本的.NET项目使用.json文件代替了过去的.xxproj,.sln和.suo文件,这符合目前的主流,即用json代替XML。新版本的.NET框架要传输给我们的理念是:这是一个跨平台的,开源的框架。一切都是依赖注入,一切都是nuget,开发彻底组件化,能解耦的全都解耦。ASP.NET Core彻底摆脱了System.Web这个顽疾,在其中,我们甚至连MVC都是注入进去的。如果想得到什么组件,要么通过依赖注入,要么就使用nuget。永远不要手动add reference,目前我知道的唯一的例外是System.Configuration。当你和团队其他人并行开发系统的不同模块时,你们可以用nuget互相得到对方模块中的工程。Nuget相比add reference,更不容易出错,界面更友好,且不会轻易陷入dll陷阱。
经过.NET牌编译器编译之后的程序集有两种形态:类库(.dll)形态和可执行文件(.exe)形态。.NET自带了很多类库,统称为FCL。BCL是FCL的一个子集。
1.2 基础类库(BCL)
Base Class Library (BCL) 是微软所提出的一组标准库,可提供给.NET Framework所有语言使用。随着 Windows 以及.NET Framework 的成长,BCL 已近乎成为在.NET上的 Windows API。mscorlib.dll程序集几乎就是基础类库的代名词。
当安装.NET Framework时,所有的基础类库被部署到全局程序集缓存(GAC)。它的位置一般在C:\Windows\assembly。所以你不需要在你的工程中手动引用任何的基础类库,它们会被自动引用。如果你从GAC中删除了mscorlib.dll,你的IDE将变成一个什么都不懂的白痴。因为没有mscorlib.dll,意味着没有基础类库,没有整型,字符串,控制台…你什么都做不了。
部分mscorlib.dll包括的命名空间:
- System:.NET Framework 类库中最基底的服务,提供应用程序域 (Application Domain),数据类型,I/O 以及其他类库的基础。
- System.Collections:提供非泛型数据结构以及集合对象的支持,其中 System.Collections.Generic中包括所有的泛型数据结构。
- System.Configuration:提供 .NET 应用程序在配置设置上的支持。
- System.Data:ADO.NET 的组成类库,为数据访问功能的核心功能。
- System.Drawing:提供 .NET 的绘图能力,包含基本位图处理以及视频与色彩处理,打印支持也由本名字空间提供,此名字空间包装了大多数的 GDI 以及 GDI+ 的 API。
- System.IO:提供数据流与文件读写的支持
- System.Net:.NET 中的网络功能
- System.Reflection:反射
- System.Diagnostics:.NET 中提供系统诊断,除错,追踪与运行外部进程的能力
- System.ServiceModel:WCF 的组成类库,于 .NET Framework 3.0 时出现。
- System.Text:对文字,编码以及正规表达式的支持。
- System.Threading:线程控制
- System.Windows.Forms: Windows Forms 的组成类库,包装了 Win32 用户界面,视窗,共用控件,以及 Shell 的基础 API,以提供设计 Windows 应用程序用户界面所需的支持。
- System.Windows:WPF 的组成类库,于 .NET Framework 3.0 时出现。
- System.Web:ASP.NET 的组成类库,令工程可以和 IIS 服务器交互,XML Web Service 开发的基本支持也由本类别提供。ASP.NET Core中消失(如果你不打算用IIS做服务器的容器,则你不需要这个类库)。
- System.Xml:XML 解析器
- System.Linq,System.Xml.Linq:LINQ 的核心类库,System.Linq 是 LINQ to Object,而 System.Xml.Linq 则是 LINQ to XML。
然而在C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\我们还有一个System.dll,这个参考是每次新建工程时VS自动引用的若干参考之一。这个程序集中也有一个System命名空间,它的内容和mscorlib.dll中的不同。可以看到,System这个命名空间存在于不止一个程序集中。这意味着不同的程序集可以共享一个命名空间。
在System.dll中,System类型拥有Uri这个成员,mscorlib.dll中System类型拥有int这个成员(基元类型)。所以我们可以做个试验,如果我们将工程中对System的引用去掉,那么我们就不能定义一个Uri类型的对象。但我们仍然可以使用int类型,因为它虽然也在System这个类型里面,但位于mscorlib.dll中。当你去掉对System的引用时,你仅仅去掉了System.dll和里面的功能,但你没有去掉mscorlib.dll中System类型的功能。
BCL是属于整个.NET框架的,并非某种语言的一个基础类库。例如,C#的string类型的所有功能和定义来源于mscrolib.dll中的System.String,而VB的string类型的功能和定义也来源于相同的地方。基础类库中定义的类型称为基元类型,它也是为.NET框架所有的语言共享。
在.NET Core中,BCL改名换姓变成了Corefx。源码在https://github.com/dotnet/corefx。
1.3 框架类库(FCL)
作为一名.NET程序员,每天都要打交道的就是FCL了(框架类库)。BCL是FCL的一个子集。简单来说FCL除了BCL的那部分,就是我们要引用的外部参考。
1.4 CTS(公共类型系统)和CLS(公共语言规范)
简单的说,CTS就是说话的语法和规范。你可以理解为,英语是一种语言,英语的CTS(至少绝大一部分)就是“实用英语语法(张道真)”这本书。如果C#没了语法,那就没有class,没有接口,变成了伪码。
参考资料中的第一个链接讲的很好,我就在这里总结一下吧:
- CTS是一套语法。类似“英语语法”。它规定了一套约束,例如英语规定所有的字词都是由26个字母组成的(以及其他很多规则)。服从这套语法的语言都可以被看成是英语的某种方言,例如中古英语,现代英语都是英语,而汉语不符合字词由字母组成,所以它不是英语。同理所有服从CTS的语言,都可以被看成.NET框架的语言。
- CTS中定义了类型,允许它有属性,字段,方法等。
- .NET框架的众多语言各自实现了CTS的一部分功能。做一个不太恰当的类比,C#可以被认为是“美国英语”,F#是“英国英语”而VB是“印度英语”等。他们是英语的各种方言。他们共享一套相同的词汇表,但也各有各的特点。例如颜色在英国英语中的拼写是colour,美国英语则是color。
- 由于.NET框架的众多语言在编译时都要转换为IL,因此IL实现的CTS功能是它们的并集,也就是CTS全部的功能。你可以理解为,虽然.NET框架语言那么多,但一编译了之后,就成了一种语言。
- .NET框架的众多语言分享CTS的一小部分功能,这部分功能称为CLS(Common Language Specification,公共语言规范)。这是这些语言(的程序集)可以相互使用的前提。如果你创建一个新语言,其实现了CTS的一部分功能,但不包括CLS,那你的语言就不能被其他.NET框架的语言(的程序集)使用。如果你创建的语言甚至不符合CTS,例如你在词汇表中加入了汉字,那不好意思,你创建的语言不能叫英语。
很明显,CLS是CTS的一个子集,而且是最小的子集。(最小功能集)
图片来自CLR via C#。
1.5 为什么说.NET是平台无关的?
.NET程序集可以在非微软操作系统如Mac OS,各种版本的Linux,以及iOS和Android移动设备上开发和执行。.NET的平台无关性主要体现为:.NET程序集可以在任何的平台上运行,不管是Windows,还是Mac,只要这个平台拥有将IL转换为机器码,以及加载其他相关程序集的能力(即CLR),而任何机器都可以运行机器码。这类似于Java的虚拟机,只要平台装了Java虚拟机,则这个平台就可以运行Java程序。
1.6 CLR(公共语言运行时)
CLR是让程序执行所需的外部服务的集合,类似Java需要JVM虚拟机才可以运行。
它的核心功能(比如即时编译,内存管理,程序集加载,安全性,异常处理和线程同步)可由面向CLR的所有语言使用。例如,CLR允许创建线程,所以面向CLR的所有语言都能创建线程。
CLR是.NET的运行基础,管理.NET程序集的执行。它运行于Windows之上,很多功能仅仅是Windows上的一个wrapper,例如线程,内存管理等,这些实际上是Windows在管理。但JIT则是它独有的,如果没有它,就不能把IL变成机器码,计算机也就不认识C#,你也就不能运行C#程序。
在开始运行.NET程序之前,编译器将代码转换为IL。IL代码并不能直接运行,CLR将真正需要用到的程序集导入内存,读取元数据,接着为类型开辟内存空间,执行所有需要的安全检查,并最终运行代码:
- CLR找到代码中拥有Main方法的类型并且加载这个类型。CLR中一个名为Class loader(类加载程序)的组件负责这项工作。它会从GAC、配置文件、程序集元数据中寻找这个类型,然后将它的类型信息加载到内存中的数据结构中。在Class loader找到并加载完这个类型之后,它的类型信息会被缓存起来,这样就无需再次进行相同的过程。当然,如果这个类型引用了其他的类型,则会导致一连串的程序集加载,这将定义程序代码执行的环境(类似Java的JVM)。注意即使工程很大,有几百个程序集,CLR不会全部加载,只会在真正用到该程序集的时候才加载。
- 验证。在CLR中,还存在一个验证程序(verifier),该验证程序的工作是在运行时确保代码是类型安全的。它主要校验两个方面,一个是元数据是正确的,一个是IL代码必须是类型安全的,类型的签名必须正确。这是早期绑定验证,验证在运行时之前发生。对于动态类型,此时不做任何检查。
- 即时编译。(此时就从编译时过渡到了运行时)这一步就是将托管的IL代码编译为可以执行的机器代码的过程,由CLR的即时编译器(JIT Complier)完成。即时编译只有在方法的第一次调用时发生。类型加载程序(Class loader)会为每个方法插入一个存根。在调用方法时,CLR会检查方法的存根,如果存根为空,则执行JIT编译过程,并将该方法被编译后的本地机器代码地址写入到方法存根中。当第二次对同一方法进行调用时,会再次检查这个存根,如果发现其保存了本地机器代码的地址,则直接跳转到本地机器代码进行执行,无需再次进行JIT编译。JIT编译还会优化本地的代码。
在程序运行时,CLR还负责:
- 异常处理
- 内存管理与垃圾回收
- 线程管理(线程池)
托管代码是必须在CLR下执行的代码,而非托管代码则不需要CLR的支持就可以运行。CLR本身用于管理托管代码,因此它是由非托管代码编写的,并不是一个包含了托管代码的程序集,也不能使用IL DASM进行查看。它位于C:\%SystemRoot%\Microsoft.NET\Framework\版本号下,视安装的机器不同有两个版本,一个是工作站版本的mscorwks.dll,一个是服务器版本的mscorsvr.dll。wks和svr分别代表workstation和server。
CLR via C#这本书选择通过C#作为视角,讨论CLR的各种功能。通过对这本书的阅读,你会对一些实际由CLR进行管理的行为例如垃圾回收,线程管理有更加深刻的认识。
2. 编译:IL与JIT
面试出现频率:低。不排除部分IL专家会试探性问你一些IL命令,但我相信你答不出来他们也不会在意。学了IL和没学,一般人看不出来区别,学了IL,也不意味着你就很厉害。个人认为,学IL唯一的用处就在于证明你看到的书上写的各种结论,或者验证一些性能方面的想法。你可以参看这篇文章:http://blog.zhaojie.me/2009/06/my-view-of-il-2-il-shows-little-about-clr.html
重要程度:3/10,常识性了解即可
需要理解的程度:知道IL是中间代码,知道JIT的优点(带缓存的编译),以及它可能会对你的代码进行优化。
2.1 什么是IL(CIL)?如何获得IL代码?
在.NET的开发过程中, IL的官方术语是MSIL或CIL(Common Intermediate Language,即公共中间语言)。因此,IL,MSIL和CIL指的是同一种东西。
当使用支持.NET的编译器编译之后,生成.dll或.exe文件。这文件称作.NET程序集,包含IL和元数据。不同语言(例如C#和VB)经过不同编译器(例如C#编译器和VB编译器),编译一段功能相似的代码(区别仅仅在于语法),其IL也基本相似。虽然IL相对C#较为底层,但它仍然是一个十分高级的语言。它并不是汇编语言。
可以通过ildasm(在cmd中运行)工具加载任意的.NET程序集并分析它的内容,包括它所包含的IL代码和元数据。注意,高级语言只公开了CLR的所有功能的一个子集,而IL允许开发人员访问CLR所有的功能。
关于IL的扩展阅读,可参看老赵谈IL系列:
http://blog.zhaojie.me/2009/06/my-view-of-il-1-il-and-asm.html
2.2 什么是JIT?还有什么其他编译方式?何时使用到JIT?
即时编译(英语:Just-in-time compilation)是动态编译的一种形式,是一种提高程序运行效率的方法。通常,程序有两种运行方式:静态编译与动态编译。静态编译的程序在执行前全部被翻译为机器码,而动态编译执行的则是一句句,边运行边翻译。
即时编译则混合了这二者,一句句编译源代码,但是会将翻译过的代码缓存起来以降低性能损耗。相对于静态编译代码,即时编译的代码可以处理延迟绑定并增强安全性。
CLR的JIT负责将IL编译成机器码。 当程序编译成程序集之后,CLR加载任何需要用到的其他程序集,并开始使用JIT将CIL编译为机器码。JIT编译器会在方法的首次调用时,从类型的元数据中查找方法,并进行检查,例如检查类型是否安全。如果出现了问题,则触发运行时错误。以后对方法的所有调用都以本地代码的形式全速运行,无须重新检查。
2.3 本地代码的优化
CLR的JIT编译器会对本地代码进行优化。例如字符串驻留中对常量字符串相加的优化。和没有优化相比,优化之后的代码将获得更出色的性能。但过度的优化可能会出现问题,在CLR via C#的易失构造中,作者举了一个例子。
1 class Program 2 { 3 private static bool s_stopWorker = false; 4 5 static void Main() 6 { 7 Console.WriteLine("Main: letting worker run for 2 seconds"); 8 Thread t = new Thread(Worker); 9 t.Start(); 10 11 Thread.Sleep(2000); 12 s_stopWorker = true; 13 Console.WriteLine("Main: waiting for worker to stop"); 14 t.Join(); 15 } 16 17 private static void Worker(object o) 18 { 19 int x = 0; 20 while (!s_stopWorker) 21 { 22 x++; 23 } 24 Console.WriteLine("Worker: stopped when x = {0}", x); 25 } 26 }
如果使用f5呼叫出Visual Studio的调试模式,则程序会像预期的那样正常运行直到结束。使用调试器会造成JIT编译器在Debug模式进行编译,它生成未优化的代码,目的是方便你进行单步调试。如果是选择了x86的Release模式进行编译:
它将会生成被CLR优化的代码。值得一提的是,x86编译器是一个更成熟的编译器,执行优化比x64更大胆。x64不会执行上面所说的特定的优化。在再次用f6进行编译之后,用ctrl+f5运行程序,程序将会陷入无限循环。
注意:必须用x86+Release编译,然后以非调试模式运行(即Ctrl+F5),才能看到这个效果。问题发生的原因是,x86的编译优化过度。它发现变量s_stopWorker要么为true要么为false。它还发现这个值在worker方法本身中从来没有变化。因此,编译器会生成代码检查s_stopWorker,如果s_stopWorker为true,就显示“Worker: stopped when x = 0”。如果s_stopWorker为false编译器就生成代码进入一个无限循环,并一直递增x。解决的办法是为s_stopWorker加入修饰词volatile。
PDB文件包含了可以令调试器在本地工作的信息。可以这么说:有了PDB文件,本地的debug才成为可能。如果你打算发布Release版本,则不需要该文件。使用Release模式编译的结果中也不包含PDB文件。例如,你写了一个小的控制台程序给别人用,那么你不需要把\bin\debug里面所有的文件都拷贝给别人,你只需要程序本身,必要的dll和config文件即可。