2.1 .NET CLI概述

.NET CLI是一个跨平台工具,用于创建、构建、运行和发布.NET应用程序。.NET CLI不需要开发人员主动安装,在安装.NET SDK时会默认安装.NET CLI。因此,不需要在计算机中特意安装.NET CLI。

为了验证是否安装了.NET CLI,可在Windows中打开命令提示符窗口输入dotnet命令,如果是Linux,则可在终端Bash窗口中直接输入dotnet命令,按Enter键执行该命令。执行dotnet命令的过程如图2-1所示,图中展示了Windows命令提示符窗口中的输出结果。

.NET CLI提供了一些命令,将.NET应用程序开发的常规操作进行集成管理。熟悉这些命令,有助于开发人员在一些非IDE的编辑器工具中进行.NET应用程序的开发、构建、测试和发布。

.NET CLI最方便的应用场景可能是在DevOps流程中进行持续集成(Continuous Integration,CI),开发人员可以将命令灵活地运用到持续集成的工具链中,从而对.NET应用程序进行生成和构建操作。

图2-1 执行dotnet命令的过程

dotnet命令说明如表2-1所示,表中列举的是一些基本命令。

表2-1 dotnet命令说明

下面使用CLI创建一个控制台应用程序,以加深读者对.NET CLI的理解。

如代码2-1所示,创建一个控制台应用程序。

代码2-1

生成的控制台应用程序的目录如图2-2所示。

图2-2 生成的控制台应用程序的目录

使用dotnet build命令可以构建.NET应用程序,如代码2-2所示。

代码2-2

运行.NET应用程序,如代码2-3所示。

代码2-3

也可以使用dotnet命令指定.NET应用程序的.dll文件,运行控制台应用程序,输出结果如图2-3所示。

图2-3 输出结果

通过上述内容,读者可以基本上了解.NET CLI的使用。有的读者可能想深入了解.NET CLI内部的工作机制,在命令背后.NET CLI执行了什么操作,以及.NET CLI是如何演化的。

代码2-4展示了RestoreCommand类的实现,开发人员可以通过对CLI的源代码进行探索来查看其运行机制(关于CLI的源代码可以从GitHub官网的dotnet/sdk仓库中查看)。

代码2-4

上述代码来源于.NET CLI工具,用于实现dotnet restore命令。在dotnet restore命令的背后,该命令及参数会生成对应的MSBuild命令,如代码2-5所示。

代码2-5

代码2-6展示了其他命令,它们与dotnet build命令等效。

代码2-6

关于MSBuild,本章不展开介绍,感兴趣的读者可以查阅MSBuild的相关资料进行了解。

通过学习前面的内容,读者基本了解了.NET CLI。读者也可以自行创建一个.NET CLI工具。

为了帮助读者更深入地理解.NET CLI,笔者通过一个简单的实例来演示创建一个控制台。该实例是一个日期提供程序,如代码2-7所示,使用Windows命令创建一个名为dotnet-date-tool的文件夹。

代码2-7

在文件夹dotnet-date-tool创建完成后,使用cd命令切换到指定的目录下。

在目录切换完成后,输入dotnet new console命令,创建控制台实例,如代码2-8所示。

代码2-8

在控制台实例创建完成后,可以通过Visual Studio或Visual Studio Code等编辑器打开项目文件。

图2-4所示为System.CommandLine包的安装过程。System.CommandLine是一个命令行解析器,提供了规范化的API,使开发人员可以快速创建命令行工具。

图2-4 System.CommandLine包的安装过程

在如图2-4所示的安装过程中,--prerelease参数表示System.CommandLine包目前正处于预发布状态,但是作为dotnet背后的命令行引擎,它是安全的。

在Program类中编写dotnet-date-tool命令行工具,主要用于输出具有格式化的日期字符串。如代码2-9所示,将System.CommandLine包作为命令行引擎,创建RootCommand对象,RootCommand对象表示程序本身的命令(如dotnet、docker),创建HandleCmd方法用于处理命令行参数。

代码2-9

在RootCommand对象中创建两个Option对象,dotnet-date-tool命令可以接收一个或多个Option对象,Option对象表示命令的参数,分别创建参数--name和--format。在Option对象的构造函数中可以添加命令行参数的描述信息,在--format参数中将它对应的Option对象的IsRequired属性设置为true,表示该参数为必须项。也可以通过RootCommand对象设置该命令行程序的基础信息,如调用RootCommand对象的Name属性,添加命令行工具的名称,调用Description属性,添加命令行程序的描述信息等。

基础信息的配置编写完后,先调用SetHandler扩展方法添加命令处理方法,再调用RootCommand对象的InvokeAsync扩展方法对参数进行解析。

在HandleCmd方法中,接收参数--name和--format的信息,并判断--format参数的值是否为空,在--format参数的值不为空的情况下判断--name参数的值,如果不为空则输出包含name名称的字符串,否则只输出格式化日期。图2-5所示为执行dotnet-date-tool命令输出的帮助信息。

执行dotnet-date-tool命令后,提示Option'--format'is required.,这证明Option对象的IsRequired属性的设置已生效,--format参数为必填项。在控制台窗口输出的信息中可以看到对该命令及其参数的描述。接下来通过命令的参数执行,如图2-6所示为dotnet-date-tool参数的验证信息,通过多次输入,对HandleCmd方法内部的判断逻辑进行验证。

图2-5 执行dotnet-date-tool命令输出的帮助信息

图2-6 dotnet-date-tool参数的验证信息

2.1.1 将C#编译成机器代码

代码编译是程序语言设计的基石,IDE是开发人员日常开发过程中最重要的生产力工具,这些开发工具是如何将代码编译成目标语言和可执行文件的?

Visual Studio集成了C#编译器,用于将C#代码转换为机器语言(CPU可以理解的语言),并以.dll和.exe的形式返回输出文件。将C#代码转换为机器语言的过程可以划分为两个阶段,分别是编译时过程和运行时过程。

在计算机编程的初期阶段,开发人员用机器代码编写应用程序,它看起来与代码2-10中的代码类似,这些实际上是一条条计算机指令。开发人员通过一条条指令直接与硬件打交道,同样可以实现许多复杂的功能。

代码2-10

机器语言的执行效率通常比较高,但可读性很差。众所周知,在计算机科学中有一句名言,“计算机科学中没有什么是不能通过增加一层抽象来解决的”,这就促成了高级语言的诞生。

高级语言是对机器语言进行抽象,在编写过程中允许开发人员无须像机器语言那样输入复杂的指令,也不需要花费大量的时间编写诸如内存管理、硬件的兼容性等与实际业务无关的底层代码。

注意:开发人员不需要清楚计算机的每个细节就可以进行高效的编码,这意味着有一套机制用来实现高级语言到机器语言的转换,同样,C#也必须转换为处理器可以理解的语言,因为处理器不知道C#,只知道机器语言。

C#编译器在编译时会将代码作为输入,并以中间语言(Intermediate Language,IL)的形式输出,该代码保存在*.exe文件或*.dll文件中。将C#代码编译为IL代码的过程如图2-7所示。

图2-7 将C#代码编译为IL代码的过程

如图2-7所示,这并不是一个完整的编译流程,没有生成处理器能够处理的指令,也就是缺少机器语言的生成过程,因此,需要一个过程将IL代码转换为机器代码。而处理这个过程的正是公共语言运行时,即CLR。

CLR在计算机上运行,可以管理IL代码的执行。简单来说,它知道如何执行通过IL代码编写的应用程序,并且使用JIT编译器将IL代码转换为机器代码,有时候也被称为本机代码(Machine Code)。代码的执行过程如图2-8所示。

图2-8 代码的执行过程

由上述内容可知,编写C#代码既不需要关注硬件设备的组成,也不需要考虑兼容性,因为CLR和JIT将负责这个过程,它们可以将IL代码编译为计算机使用的代码。

上面介绍的是C#编译器,下面介绍Roslyn编译器。Roslyn是C#和Visual Basic.NET的开源编译器,是一个完全使用C#托管代码开发的编译器和分析器。如代码2-11所示,创建C#代码,通过C#Roslyn编译器进行编译。

代码2-11

对源文件进行编译后,可以打开.NET SDK中的C#Roslyn编译器。C#Roslyn编译器可以直接通过dotnet命令进行调用,如代码2-12所示。

代码2-12

如代码2-13所示,调用csc.dll文件,将C#源文件编译为Program.dll文件。

代码2-13

需要注意的是,-reference参数用于指定外部程序集。*.cs表示编译当前目录下的所有.cs文件,当然也可以编译指定的单个文件。-out参数用于指定输出的文件名,这不是一个必选参数,若不指定该参数,则默认生成Program.exe文件。

图2-9所示为执行csc命令编译后的Program.dll文件,返回了错误信息。

图2-9 执行csc编译后的Program.dll文件

如图2-9所示,需要创建Program.runtimeconfig.json文件,用于指定运行时信息。如代码2-14所示,定义运行时信息。

代码2-14

使用dotnet命令调用Program.dll文件,输出结果如图2-10所示。

图2-10 输出结果

代码从编写到执行的流程如图2-11所示。

学习一门语言第一个经典的例子就是编写Hello World应用程序。下面介绍Hello World应用程序从C#代码到机器代码的编译过程。

图2-11 代码从编写到执行的流程

C#代码

IL代码

机器代码

2.1.2 运行时

.NET提供了CLR,运行时会将中间语言代码转换为当前CPU平台支持的机器代码并执行这些机器代码。在CLR下托管的代码称为托管代码(Managed Code),运行时是托管代码的执行环境。

CLR逐渐成为运行时的代名词,而在技术上更准确的虚拟执行系统(VES)则很少在CLI规范之外的地方提到。事实上,CLR提供了类型安全、内存安全、异常处理、垃圾回收、多线程等机制,这些机制为.NET应用程序提供了一个更安全、更高效的运行环境。

2.1.3 程序集和清单

在众多计算机语言中,若按编译特点划分,可以分为编译型语言、解释型语言和混合型语言。

编译型语言是需要通过编译器将源代码编译为机器代码才能执行的高级语言,如C、C++等。解释型语言则不需要预先编译,在执行时逐行编译。混合型语言也需要编译,但不直接编译成机器代码,而是编译成中间语言,通过运行时执行中间语言,将中间语言解释为机器代码来执行。

C#可视为混合型语言,这意味着当开发人员创建源文件时,这些文件需要先编译才能运行。C#不像JavaScript和PHP这种动态类型语言(或脚本语言)一样能直接运行。程序集的产生正是为了解决该问题,程序集内会保存相关的中间语言和其他资源文件(如TXT、JPG、Excel、XML等,若是作为嵌入程序集的资源,则作为嵌入文件保存到程序集中,本章不对其展开介绍)。

程序集是由一个或多个源代码文件生成的输出文件。程序集是.NET应用程序在资源管理器中基本的文件单元,具有.exe扩展名和.dll扩展名两种类型,扩展名为.exe的程序集是可执行文件(Executable File),扩展名为.dll的程序集是动态链接库(Dynamic-Link Library)。

程序集清单从本质上来说是程序集的一个标头(Header),提供程序集运行所需的描述信息和程序集唯一性标识信息,包含程序集的版本信息、范围信息,以及与程序集相关的其他信息,清单内容如图2-12所示。

.NET程序集中包含描述程序集自身的元数据(清单,Manifest),而清单内容页则包含所需要的外部程序集、程序集版本号、模块名称等其他信息。

图2-12 清单内容

程序集中可以包含多个不同类型的资源文件。单个文件和多个文件的存储方式如图2-13所示,程序集中包括.jpg文件和.bmp文件,或者其他格式的文件。

图2-13 单个文件和多个文件的存储方式

多文件程序集不是将多个文件物理链接起来,而是通过程序集清单进行链接,CLR将它们作为一个单元进行管理。

2.1.4 公共中间语言

公共中间语言(Common Intermediate Language,CIL),简称中间语言,包括Microsoft中间语言(MSIL)和中间语言(IL),由这个名字可以得知CIL的一个重要特点,即它支持多种语言在同一个应用程序中进行交互。CIL不只是C#的中间语言,还是其他许多.NET大家族编程语言的中间语言,如Visual Basic、F#等。

2.1.5 .NET Native

.NET Native是一项预编译技术,用于创建平台特定的可执行文件。通常,.NET应用程序会编译为中间语言,在运行期间利用JIT编译器将IL代码翻译为机器代码。相比之下,.NET Native则将应用程序直接编译为机器代码,这意味着用这种方式编译的应用程序具有机器代码的性能。

通常,.NET应用程序首先编译为IL代码,然后由JIT编译器编译为Native代码。利用.NET Native可以不需要.NET运行时,也不需要JIT编译器,而是直接运行机器代码。