名家经典系列
点击查看第二章
点击查看第三章
C# 7.0本质论
Essential C# 7.0
[美] 马克米凯利斯(Mark Michaelis)著
周 靖 译
第1章
C#概述
C#是一种成熟的语言,基于作为前身的C风格语言(C、C++和Java)的功能而设计,有经验的程序员能很快熟悉。此外,可用C#构建在多种操作系统(平台)上运行的软件组件和应用程序。
本章用传统HelloWorld程序介绍C#,重点是C#语法基础,包括定义C#程序入口。通过本章的学习,你将熟悉C#的语法风格和结构,能开始写最简单的C#程序。讨论C#语法基础之前,将简单介绍托管执行环境,并解释C#程序在运行时如何执行。最后讨论变量声明、控制台输入/输出以及基本的C#代码注释机制。
1.1 Hello, World
学习新语言最好的办法就是写代码。第一个例子是经典HelloWorld程序,它在屏幕上显示一些文本。代码清单1.1展示了完整HelloWorld程序,我们将在之后的小节编译代码。
注意 C#是区分大小写的语言,大小写不正确会使代码无法成功编译。
有Java、C或者C++编程经验的读者很快就能看出相似的地方。类似于Java,C#也从C和C++继承了基本的语法。语法标点(比如分号和大括号)、特性(比如区分大小写)和关键字(比如class、public和void)对于这些程序员来说并不陌生。初学者和其他语言背景的程序员通过这个程序能很快体会到这些构造的直观性。
1.1.1 创建、编辑、编译和运行C#源代码
写好C#代码后需要编译和运行。这时要选择使用哪个.NET实现(或者说.NET框架)。这些实现通常打包成一个软件开发包(Software Development Kit,SDK),其中包括编译器、运行时执行引擎、“运行时”能访问的语言可访问功能框架(参见本章后面的1.7.1节),以及可能和SDK捆绑的其他工具(比如供自动化生成的生成引擎)。由于C#自2000年便已公布,目前有多个不同的.NET框架供选择(参见本章后面的1.7节)。
取决于开发的目标操作系统以及你选择的.NET框架,每种.NET框架的安装过程都有所区别。有鉴于此,建议访问https://www.microsoft.com/net/download 了解具体的下载和安装指示。先选好.NET框架,再根据目标操作系统选择要下载的包。虽然我可以在这里提供更多细节,但.NET下载站点为支持的各种组合提供了最新、最全的指令。
如不确定要使用的.NET框架,就默认选择.NET Core。它可运行于Linux、macOS和Microsoft Windows,是.NET开发团队投入最大的实现。另外,由于它具有跨平台能力,所以本书优先使用.NET Core。
有许多源代码编辑工具可供选择,包括最基本的Windows记事本、Mac/macOS TextEdit和Linux vi。但建议选择一个稍微高级点的工具,至少应支持彩色标注。支持C#的任何代码编辑器都可以。如果还没有特别喜欢的,推荐开源编辑器Visual Studio Code(https://code.visualstudio.com )。如果在Windows或Mac上工作,也可考虑Microsoft Visual Studio 2017(或更高版本),详情参考https://www.visualstudio.com 。两者都是免费的。
后两节我会提供这两种编辑器的操作指示。Visual Studio Code依赖命令行(Dotnet CLI)创建初始的C#程序基架并编译和运行。Windows和Mac则一般使用Visual Studio 2017。
使用Dotnet CLI
Dotnet命令dotnet.exe是Dotnet命令行接口(或称Dotnet CLI),可用于生成C#程序的初始代码库并编译和运行程序。注意这里的CLI代表“命令行接口”(Command-Line Interface)。为避免和代表“公共语言基础结构”(Common Language Infrastructure)的CLI混淆,本书在提到Dotnet CLI时都会附加Dotnet前缀。无Dotnet前缀的CLI才是“公共语言基础结构”。安装好之后,验证可以在命令行上执行dotnet。
以下是在Windows、macOS或Linux上创建、编译和执行HelloWorld程序的指示:
- 在Microsoft Windows上打开命令提示符,在Mac/macOS上打开Terminal应用。(也可考虑使用跨平台命令行接口PowerShell。)
- 在想要放代码的地方新建一个目录。考虑./HelloWorld或./EssentialCSharp/HelloWorld这样的名称。在命令行上执行:
- 导航到新目录,使之成为命令行的当前目录:
- 在HelloWorld目录中执行dotnet new console命令来生成程序基架。这会生成几个文件,最主要的是Program.cs和项目文件:
- 运行生成的程序。这会编译并运行由dotnet new console命令创建的默认Program.cs程序。程序内容和代码清单1.1相似,只是输出变成“Hello World!”。
虽然没有显式请求应用程序编译(或生成),但dotnet run command命令在执行时隐式执行了这一步。
- 编辑Program.cs文件并修改代码使之和代码清单1.1一致。用Visual Studio Code打开并编辑Program.cs会体验到支持C#的编辑器的好处,代码会用彩色标注不同类型的构造。(输出1.1展示了在Bash和PowerShell中适合命令行的一种方式。)
- 重新运行程序:
输出1.1展示了上述步骤的输出。
使用Visual Studio 2017
在Visual Studio 2017中的操作相似,只是不用命令行,而是用集成开发环境(IDE)。有菜单可选,不必一切都在命令行上进行。
- 启动Visual Studio 2017。
- 选择“文件”|“新建”|“项目”(Ctrl + Shift + N)菜单打开“新建项目”对话框。
- 在搜索框(Ctrl + E)中输入“控制台应用”并选择“控制台应用(.NET Core)—Visual C#”。在“名称”框中输入HelloWorld。在“位置”处选择你的工作目录。如图1.1所示。
- 项目创建好后会打开Program.cs文件供编辑,如图1.2所示。
- 选择“调试”|“开始执行(不调试)”(Ctrl + F5)来生成并运行程序。会显示如输出1.2所示的命令窗口,只是第一行目前为“Hello World! ”。
- 将Program.cs修改成代码清单1.1的样子。
- 返回程序并重新运行,获得如输出1.2所示的结果。
输出1.2
IDE最重要的一个功能是调试。按以下额外的步骤试验:
- 光标定位到System.Console.WriteLine这一行,选择“调试”|“切换断点”(F9)在该行激活断点。
- 选择“调试”|“开始调试”(F5)重新启动应用程序,但这次激活了调试功能。注意会在断点所在行停止执行。此时可将鼠标放到某个变量(例如args)上以观察它的值。还可以拖动左侧黄箭头将程序执行从当前行移动到方法内的另一行。
- 要继续执行,选择“调试”|“继续”(Ctrl + F5)或者点击工具栏上的“继续”按钮。
调试时输出窗口不再出现“请按任意键继续...”提示,而是自动关闭。注意Visual Studio Code也可作为IDE使用,详情参见https://code.visualstudio.com/docs/languages/csharp 。其中还提供了一个链接来解释用Visual Studio Code进行调试的问题。
1.1.2 创建项目
无论Dotnet CLI还是Visual Studio都会自动创建几个文件。第一个是名为Program.cs的C#文件。虽然可选择任何名称,但一般都用Program这一名称作为控制台程序起点。.cs是所有C#文件的标准扩展名,也是编译器默认要编译成最终程序的扩展名。为了使用代码清单1.1中的代码,可打开Program.cs文件并将其内容替换成代码清单1.1的。保存更新文件之前,注意代码清单1.1和默认生成的代码相比,唯一功能上的差异就是引号间的文本。还有就是后者多了using System;指令,这是一处语义上的差异。
虽然并非一定需要,但通常都会为C#项目生成一个项目文件。项目文件的内容随不同应用程序类型和.NET框架而变。但至少会指出哪些文件要包含到编译中,要生成什么应用程序类型(控制台、Web、移动、测试项目等),支持什么.NET框架,调试或启动应用程序需要什么设置,以及代码的其他依赖项(称为库)。例如,代码清单1.2列出了一个简单的.NET Core控制台应用项目文件。
注意应用程序标识为.NET Core版本2.0(netcoreapp2.0)的控制台应用(Exe)。其他所有设置(比如要编译哪些C#文件)则沿用默认值。例如,和项目文件同一目录(或子目录)中的所有*.cs文件都会包含到编译中。第10章会更多地讨论项目文件。
1.1.3 编译和执行
dotnet build命令生成名为HelloWorld.dll的程序集(assembly)。扩展名.dll代表“动态链接库”(Dynamic Link Library,DLL)。对于.NET Core,所有程序集都使用.dll扩展名。控制台程序也不例外,就像本例这样。.NET Core应用程序的编译输出默认放到子目录./bin/Debug/netcoreapp2.0/。之所以使用Debug这个名称,是因为默认配置就是debug。该配置造成输出为调试而不是性能而优化。编译好的输出本身不能执行。相反,需用CLI来寄宿(host)代码。对于.NET Core应用程序,这要求Dotnet.exe进程作为应用程序的寄宿进程。
开发人员可以不用dotnet run创建能直接运行的控制台程序,而是创建可由其他(较大的)程序来引用的库。库也是程序集。换言之,一次成功的C#编译,结果必然是程序集,无论该程序集是程序还是库。
1.1.4 使用本书源代码
本书源代码包含解决方案文件EssentialCSharp.sln,它组合了全书所有代码。Visual Studio和Dotnet.exe都能生成、运行和测试这些源代码。或许最简单的方式是将源代码拷贝到早先创建的HelloWorld程序中并执行。但是,解决方案包含了各章的项目文件,还提供了一个菜单来选择要执行的代码清单。详情参见以下两节。
使用Dotnet CLI
要用Dotnet CLI生成并执行代码,请打开命令提示符,将当前目录设为EssentialCSharp.sln文件所在的目录。执行dotnet build命令编译所有项目。
要运行特定项目的源代码,导航到项目文件所在目录并执行dotnet run命令。另外,在任何目录都可以执行dotnet run -p 命令。其中是要执行的项目文件的路径(例如dotnet run -p .srcChapter01Chapter01.csproj)。随后会运行程序,并提示运行的是哪个代码清单。
许多代码清单都在Chapter[??].Tests目录中提供了相应的单元测试。其中[??]是章的编号。要执行测试,在相应目录中执行dotnet test命令(在EssentialCSharp.sln所在目录执行该命令,则所有单元测试都会执行)。
使用Visual Studio
在Visual Studio中打开解决方案文件后,选择“生成”|“生成解决方案”(F6)来编译代码。要执行某一章的项目,需要先将该章的项目设为启动项目。例如,要执行第1章的示例,请右击Chapter01项目并选择“设为启动项目”。若不这样做,执行时输入非启动项目所在章的代码清单编号会抛出异常。
设置好正确项目后,选择“调试”|“开始执行(不调试)”(Ctrl + F5)来运行项目。如需调试则按F5。运行时程序会提示输入代码清单的编号(例如1.1)。如前所述,只能输入已启动项目中的代码清单。
许多代码清单都有对应的单元测试。要执行测试,打开测试项目(Chapter[??].Tests),导航到与代码清单对应的测试(比如HelloWorldTests)。双击它在代码编辑器中显示。右击要测试的方法(比如public void Main_InigoHello()),右击并选择“运行测试”(Ctrl + R, T)或“调试测试”(Ctrl + R, Ctrl + T)。
1.2 C#语法基础
成功编译并运行HelloWorld程序之后,我们来分析代码,了解它的各个组成部分。首先熟悉一下C#关键字以及可供开发者选择的标识符。
1.2.1 C#关键字
表1.1总结了C#关键字。
C# 1.0之后没有引入任何新的保留关键字,但在后续版本中,一些构造使用了上下文关键字,它们在特定位置才有意义,在其他位置则无意义。这样大多数C# 1.0代码都能兼容后续版本。
1.2.2 标识符
和其他语言一样,C#用标识符标识程序员编码的构造。在代码清单1.1中,HelloWorld和Main均为标识符。分配标识符之后,以后将用它引用所标识的构造。因此,开发者应分配有意义的名称,不要随性而为。
好的程序员总能选择简洁而有意义的名称,这使代码更容易理解和重用。清晰和一致是如此重要,以至于“框架设计准则”(http://t.cn/RD6v4RB )建议不要在标识符中使用单词缩写,甚至不要使用不被广泛接受的首字母缩写词。即使被广泛接受(如HTML),使用时也要一致。不要一会儿这样用,一会儿那样用。为避免滥用,可限制所有首字母缩写词都必须包含到术语表中。总之,要选择清晰(甚至是详细)的名称,尤其是在团队中工作,或者开发要由别人使用的库的时候。
标识符有两种基本的大小写风格。第一种风格是.NET框架创建者所谓的Pascal大小写(PascalCase),它在Pascal编程语言中很流行,要求标识符的每个单词首字母大写,例如ComponentModel、Configuration和HttpFileCollection。注意在HttpFileCollection中,由于首字母缩写词HTTP的长度超过两个字母,所以仅首字母大写。第二种风格是camel大小写(camelCase),除第一个字母小写,其他约定一样,例如quotient、firstName、httpFileCollection、ioStream和theDreadPirateRoberts。
下划线虽然合法,但标识符一般不要包含下划线、连字号或其他非字母/数字字符。此外,C#不像其前辈那样使用匈牙利命名法(为名称附加类型缩写前缀)。这避免了数据类型改变时还要重命名变量,也避免了数据类型前缀经常不一致的情况。
极少数情况下,有的标识符(比如Main)可能在C#语言中具有特殊含义。
高级主题:关键字
虽然罕见,但关键字附加“@”前缀可作为标识符使用,例如可命名局部变量@return。类似地(虽不符合C#大小写规范),可命名方法@throw()。
在Microsoft的实现中,还有4个未文档化的保留关键字:__arglist,__makeref,__reftype,__refvalue。它们仅在罕见的互操作情形下才需要使用,平时完全可以忽略。注意这4个特殊关键字以双下划线开头。C#设计者保留将来把这种标识符转化为关键字的权利。为安全起见,自己不要创建这样的标识符。
1.2.3 类型定义
C#所有代码都出现在一个类型定义的内部,最常见的类型定义以关键字class开头。如代码清单1.3所示,类定义是class <标识符> { ... }形式的代码块。
类型名称(本例是HelloWorld)可以随便取,但根据约定,它应当使用PascalCase风格。就本例来说,可选择的名称包括Greetings、HelloInigoMontoya、Hello或者简单地称为Program。(对于包含Main()方法的类,Program是个很好的名称。Main()方法的详情稍后讲述。)
1.2.4 Main方法
C#程序从Main方法开始执行。该方法以static void Main()开头。在命令控制台中输入HelloWorld.exe执行程序,程序将启动并解析Main的位置,然后执行其中第一条语句。如代码清单1.4所示。
虽然Main方法声明可进行某种程度的变化,但关键字static和方法名Main是始终都需要的。
将Main方法指定为static意味着这是“静态”方法,可用类名.方法名的形式调用。若不指定static,用于启动程序的命令控制台还要先对类进行实例化,然后才能调用方法。第6章将用整节篇幅讲述静态成员。
Main()之前的void表明方法不返回任何数据(将在第2章进一步解释)。
C#和C/C++一样使用大括号封闭构造(比如类或者方法)的主体。例如,Main方法主体就是用大括号封闭起来的。本例方法主体仅一条语句。
1.2.5 语句和语句分隔符
Main方法只含一条语句,即System.Console.WriteLine();,它在控制台上输出一行文本。C#通常用分号标识语句结束。每条语句都由代码要执行的一个或多个行动构成。声明变量、控制程序流程或调用方法都是语句的例子。
由于换行与否不影响语句的分隔,所以可将多条语句放到同一行,C#编译器认为这一行包含多条指令。例如,代码清单1.6在同一行包含了两条语句。执行时在控制台窗口分两行显示Up和Down。
C#还允许一条语句跨越多行。同样地,C#编译器根据分号判断语句结束位置。代码清单1.7展示了一个例子。
代码清单1.7的WriteLine()语句的原始版本来自HelloWorld程序,它在这里跨越了多行。
1.2.6 空白
分号使C#编译器能忽略代码中的空白。除少数特殊情况,C#允许代码随意插入空白而不改变语义。在代码清单1.6和代码清单1.7中,在语句中或语句间换行都可以,对编译器最终创建的可执行文件没有任何影响。
程序员经常利用空白对代码进行缩进来增强可读性。来看看代码清单1.8和代码清单1.9展示的两个版本的HelloWorld程序。
虽然这两个版本看起来和原始版本颇有不同,但C#编译器认为所有版本无差别。
1.3 使用变量
前面我们已接触了最基本的C#程序,下面声明局部变量。变量声明后可以赋值,可将值替换成新值,并可在计算和输出等操作中使用。但变量一经声明,数据类型就不能改变。在代码清单1.10中,string max就是变量声明。
1.3.1 数据类型
代码清单1.10声明的是string类型的变量。本章还使用了int和char。
- int是指C#的32位整型。
- char是字符类型,长度为16位,足以表示无代理项的Unicode字符。
下一章将更详细地探讨这些以及其他常见数据类型。
1.3.2 变量的声明
代码清单1.10中的string max是变量声明,它声明名为max的string变量。还可在同一条语句中声明多个变量,办法是指定数据类型一次,然后用逗号分隔不同标识符,如代码清单1.11所示。
由于声明多个变量的语句只允许提供一次数据类型,因此所有变量都具有相同类型。
C#变量名可用任何字母或下划线(_)开头,后跟任意数量的字母、数字或下划线。但根据约定,局部变量名采用camelCase命名(除了第一个单词外,其他每个单词的首字母大写),而且不包含下划线。
1.3.3 变量的赋值
局部变量声明后必须在读取前赋值。一个办法是使用=操作符,或者称为简单赋值操作符。操作符是一种特殊符号,标识了代码要执行的操作。代码清单1.12演示了如何利用赋值操作符指定miracleMax和valerie变量要指向的字符串值。
从中可以看出,既可在声明变量的同时赋值(比如变量miracleMax),也可在声明后用另一条语句赋值(比如变量valerie)。要赋的值必须放在赋值操作符右侧。
运行编译好的程序生成如输出1.3所示的结果。
输出1.3
本例列出了dotnet run命令,以后会省略,除非要附加额外参数来指定程序的运行方式。
C#要求局部变量在读取前“明确赋值”。此外,赋值作为一种操作会返回一个值。所以C#允许在同一语句中进行多个赋值操作,如代码清单1.13所示。
1.3.4 变量的使用
赋值后就能用变量名引用值。因此,在System.Console.WriteLine(miracleMax)语句中使用变量miracleMax时,程序在控制台上显示Have fun storming the castle!,也就是miracleMax的值。更改miracleMax的值并执行相同的System.Console.WriteLine(miracleMax)语句,会显示miracleMax的新值,即It would take a miracle.。
1.4 控制台输入和输出
本章已多次使用System.Console.WriteLine将文本输出到命令控制台。除了能输出数据,程序还需要能接收用户输入的数据。
1.4.1 从控制台获取输入
可用System.Console.ReadLine()方法获取控制台输入的文本。它暂停程序执行并等待用户输入。用户按回车键,程序继续。System.Console.ReadLine()方法的输出,也称为返回值,其内容即用户输入的文本字符串。代码清单1.14和输出1.4是一个例子。
输出1.4
在每条提示信息后,程序都用System.Console.ReadLine()方法获取用户输入并赋给变量。在第二个System.Console.ReadLine()赋值操作完成之后,firstName引用值Inigo,而lastName引用值Montoya。
C# 2.0新增了System.Console.ReadKey()方法。它和System.Console.Read()方法不同,返回的是用户的单次按键输入。可用它拦截用户按键操作,并执行相应行动,比如校验按键或是限制只能按数字键。
1.4.2 将输出写入控制台
代码清单1.14是用System.Console.Write()而不是System.Console.WriteLine()方法提示用户输入名和姓。System.Console.Write()方法不在输出文本后自动添加换行符,而是保持当前光标位置在同一行上。这样用户输入就会和提示内容处于同一行。代码清单1.14的输出清楚演示了System.Console.Write()的效果。
下一步是将通过System.Console.ReadLine()获取的值写回控制台。在代码清单1.16中,程序在控制台上输出用户的全名。但这段代码使用了System.Console.WriteLine()的一个变体,利用了从C# 6.0开始引入的字符串插值功能。注意在Console.WriteLine调用中为字符串字面值附加的$前缀。它表明使用了字符串插值。输出1.5是对应的输出。
输出1.5
代码清单1.16不是先用Write语句输出"Your full name is",再用Write语句输出firstName,用第三条Write语句输出空格,最后用WriteLine语句输出lastName。相反,是用C# 6.0的字符串插值功能一次性输出。字符串中的大括号被解释成表达式。编译器会求值这些表达式,转换成字符串并插入当前位置。不需要单独执行多个代码段并将结果整合成字符串,该技术允许一个步骤完成全部操作,从而增强了代码的可读性。
C# 6.0之前则采用不同的方式,称为复合格式化。它要求先提供格式字符串来定义输出格式,如代码清单1.17所示。
本例的格式字符串是Your full name is {0} {1}.。它为要在字符串中插入的数据标识了两个索引占位符。每个占位符的顺序对应格式字符串之后的实参。
注意索引值从零开始。每个要插入的实参,或者称为格式项,按照与索引值对应的顺序排列在格式字符串之后。在本例中,由于firstName是紧接在格式字符串之后的第一个实参,所以它对应索引值0。类似地,lastName对应索引值1。
注意,占位符在格式字符串中不一定按顺序出现。例如,代码清单1.18交换了两个索引占位符的位置并添加了一个逗号,从而改变了姓名的显示方式(参见输出1.6)。
输出1.6
占位符除了能在格式字符串中按任意顺序出现,同一占位符还能在一个格式字符串中多次使用。另外,也可省略占位符。但每个占位符都必须有对应的实参。
1.5 注释
本节修改代码清单1.16来添加注释。注释不会改变程序的执行,只是使代码变得更容易理解。代码清单1.19中展示了新代码,输出1.7是对应的输出。
输出1.7
虽然插入了注释,但编译并执行后产生的输出和以前是一样的。
程序员用注释来描述和解释自己写的代码,尤其是在语法本身难以理解的时候,或者是在另辟蹊径实现一个算法的时候。只有检查代码的程序员才需要看注释,编译器会忽略注释,因而生成的程序集中看不到源代码中的注释的一丝踪影。
表1.2总结了4种不同的C#注释。代码清单1.19使用了其中两种。
第10章将更全面地讨论XML注释。届时会讨论各种XML标记。
编程史上确有一段时期,如代码没有详尽的注释,都不好意思说自己是专业程序员。然而时代变了。没有注释但可读性好的代码,比需要注释才能说清楚的代码更有价值。如开发人员发现需要写注释才能说清楚代码块的功用,则应考虑重构,而不是洋洋洒洒写一堆注释。写注释来重复代码本来就讲得清的事情,只会使代码变得臃肿并降低可读性,还容易过时,因为将来可能更改代码但没有来得及更新注释。
1.6 托管执行和CLI
处理器不能直接解释程序集。程序集用的是另一种语言,即公共中间语言(Common Intermediate Language,CIL),或称中间语言(IL)。C#编译器将C#源代码文件转换成中间语言。为了将CIL代码转换成处理器能理解的机器码,还要完成一个额外的步骤(通常在运行时进行)。该步骤涉及C#程序执行的一个重要元素:VES(Virtual Execution System,虚拟执行系统)。VES也称为运行时(runtime)。它根据需要编译CIL代码,这个过程称为即时编译或JIT编译(just-in-time compilation)。如代码在像“运行时”这样的一个“代理”的上下文中执行,就称为托管代码(managed code),在“运行时”的控制下执行的过程则称为托管执行(managed execution)。之所以称为“托管”,是因为“运行时”管理着诸如内存分配、安全性和JIT编译等方面,从而控制了主要的程序行为。执行时不需要“运行时”的代码称为本机代码(native code)或非托管代码(unmanaged code)。
“运行时”规范包含在一个包容面更广的规范中,即CLI(Common Language Infrastructure,公共语言基础结构)规范。作为国际标准,CLI包含了以下几方面的规范。
- VES或“运行时”。
- CIL。
- 支持语言互操作性的类型系统,称为CTS(Common Type System,公共类型系统)。
- 编写通过CLI兼容语言访问的库的指导原则(这部分内容见公共语言规范(Common Language Specification,CLS))。
- 使各种服务能被CLI识别的元数据(包括程序集的布局或文件格式规范)。
在“运行时”执行引擎的上下文中运行,程序员不需要直接写代码就能使用几种服务和功能,包括:
- 语言互操作性:不同源语言间的互操作性。语言编译器将每种源语言转换成相同中间语言(CIL)来实现这种互操作性。
- 类型安全:检查类型间转换,确保兼容的类型才能相互转换。这有助于防范缓冲区溢出(这是产生安全隐患的主要原因)。
- 代码访问安全性:程序集开发者的代码有权在计算机上执行的证明。
- 垃圾回收:一种内存管理机制,自动释放“运行时”为数据分配的空间。
- 平台可移植性:同一程序集可在多种操作系统上运行。要实现这一点,一个显而易见的限制就是不能使用平台特有的库。所以平台依赖问题需单独解决。
B- CL(基类库):提供开发者能(在所有.NET框架中)依赖的大型代码库,使其不必亲自写这些代码。
CIL和ILDASM
前面说过,C#编译器将C#代码转换成CIL代码而不是机器码。处理器只理解机器码,所以CIL代码必须先转换成机器码才能由处理器执行。可用CIL反汇编程序将程序集解构为CIL。通常使用Microsoft特有的文件名ILDASM来称呼这种CIL反汇编程序(ILDASM是IL Disassembler的简称),它能对程序集执行反汇编,提取C#编译器生成的CIL。
反汇编.NET程序集的结果比机器码更易理解。许多开发人员害怕即使别人没有拿到源代码,程序也容易被反汇编并曝光其算法。其实无论是否基于CLI,任何程序防止反编译唯一安全的方法就是禁止访问编译好的程序(例如只在网站上存放程序,不把它分发到用户机器)。但假如目的只是减小别人获得源代码的可能性,可考虑使用一些混淆器(obfuscator)产品。这种产品会打开IL代码,转换成一种功能不变但更难理解的形式。这可以防止普通开发者访问代码,使程序集难以被反编译成容易理解的代码。除非程序需要对算法进行高级安全防护,否则混淆器足以确保安全。
1.7 多个.NET框架
本章之前说过,目前存在多个.NET框架。Microsoft的宗旨是在最大范围的操作系统和硬件平台上提供.NET实现,表1.3列出了最主要的实现。
除非特别注明,否则本书所有例子都兼容.NET Core和Microsoft .NET Framework。但由于.NET Core才是.NET的未来,所以本书配套代码(从http://github.com/IntelliTect/EssentialCSharp 或http://bookzhou.com 下载)都配置成默认使用.NET Core。
1.7.1 应用程序编程接口
数据类型(比如System.Console)的所有方法(常规地说是成员)定义了该类型的应用程序编程接口(Application Programming Interface,API)。API定义软件如何与其他组件交互,所以单独一个数据类型还不够。通常,是一组数据类型的所有API结合起来为某个组件集合创建一个API。以.NET为例,一个程序集中的所有类型(及其成员)构成了该程序集的API。类似地,.NET Core或Microsoft .NET Framework中的所有程序集构成了更大的API。通常将这一组更大的API称为框架,所以我们用“.NET框架”一词指代Microsoft .NET Framework的所有程序集公开的API。API通常包含一组接口和协议(或指令),帮助你使用一系列组件进行编程。事实上,对于.NET来说,协议本身就是.NET程序集的执行规则。
1.7.2 C#和.NET版本控制
.NET框架的开发周期有别于C#语言,这造成底层.NET框架和对应的C#语言使用了不同的版本号。例如,使用C# 5.0编译器将默认基于Microsoft .NET Framework 4.6来编译。表1.4简单总结了Microsoft .NET Framework和.NET Core的C#和.NET版本。
随C# 6.0增加的最重要的一个框架功能或许是对跨平台编译的支持。换言之,不仅能用Windows上运行的Microsoft .NET Framework编译,还能使用Linux和macOS上运行的.NET Core实现来编译。虽然.NET Core的功能比完整的Microsoft .NET Framework少,但足以使整个ASP.NET网站在非Windows和IIS的系统上运行。这意味着同一个代码库可编译并执行在多个平台上运行的应用程序。.NET Core是一套完整的SDK,包含从.NET Compiler Platform(即“Roslyn”,本身在Linux和macOS上运行)到.NET Core“运行时”的一切,另外还提供了像Dotnet命令行实用程序(dotnet.exe,自C# 7.0引入)这样的工具。
1.7.3 .NET Standard
有这么多不同的.NET实现,每个.NET框架还有这么多版本,而且每个实现都支持一套不同的、但多少有点重叠的API,这造成框架分叉得越来越厉害。这增大了写跨.NET框架可重用代码的难度,因为要检查特定API是否支持。为降低复杂度,Microsoft推出了.NET Standard来定义不同版本的标准应支持哪些API。换言之,要相容于某个.NET Standard版本,.NET框架必须支持该标准所规定的API。但由于许多实现已经发布,所以哪个API要进入哪个标准的决策树在一定程度上基于现有实现及其与.NET Standard版本号的关联。
写作本书时最新发布的是.NET Standard 2.0。该版本的好处在于所有基础框架都已经或准备实现这个标准。所以,.NET Standard 2.0事实上重新统合了各个老版本框架中被分叉的特色API。
1.8 小结
本章对C#进行初步介绍。通过本章你熟悉了基本C#语法。由于C#与C++风格语言的相似性,本章许多内容可能都是你所熟悉的。但C#和托管代码确实有一些独特性,比如会编译成CIL等。C#的另一个关键特征在于它完全面向对象。即使是在控制台上读取和写入数据这样的事情,也是面向对象的。面向对象是C#的基础,这一点将贯穿全书。
下一章探讨C#的基本数据类型,并讨论如何将这些数据类型应用于操作数来构建表达式。