《C# 6.0 本质论》
========== ========== ==========
[作者] (美) Mark Michaelis (美) Eric Lippert
[译者] (中) 周靖 庞燕
[出版] 人民邮电出版社
[版次] 2017年02月 第5版
[印次] 2017年02月 第1次 印刷
[定价] 108.00元
========== ========== ==========
【前言】
成功学习 C# 的关键在于,要尽可能快地开始编程。不要等自己成为一名理论方面的 “专家” 之后,才开始写代码。
学习一门计算机语言最好的方法就是在动手中学习,而不是等熟知了它的所有 “理论” 之后再动手。
为了从简单程序过渡到企业级开发, C# 开发者必须熟练地从对象及其关系的角度来思考问题。
一名知道语法的程序员和一名能因时宜地写出最高效代码的专家的区别,关键就是这些编码规范。专家不仅让代码通过编译,还遵循最佳实践,降低产生 bug 的概率,并使代码的维护变得更容易。编码规范强调了一些关键原则,开发时务必注意。
总地说来,软件工程的宗旨就是对复杂性进行管理。
【第01章】
(P001)
学习新语言最好的办法就是动手写代码。
(P003)
一次成功的 C# 编译生成的肯定是程序集,无论它是程序还是库。
在 Java 中,文件名必须和类名一致。
从 C# 2.0 开始,一个类的代码可以拆分到多个文件中,这一特性称为 “部分类” 。
编译器利用关键字来识别代码的结构与组织方式。
(P004)
C# 1.0 之后没有引入任何新的保留关键字,但在后续版本中,一些构造使用了上下文关键字 (contextual keyword) ,它们在特定位置才有意义。除了那些位置,上下文关键字没有任何特殊意义。这样,大多数的 C# 1.0 代码都完全兼容于后续的版本。
分配标识符之后,以后就能用它引用所标识的构造。因此,开发人员应分配有意义的名称,不要随意分配。
好的程序员总能选择简洁而有意义的名称,这使代码更容易理解和重用。
(P005)
[规范]
1. 要更注重标识符的清晰而不是简短;
2. 不要在标识符名称中使用单词缩写;
3. 不要使用不被广泛接受的首字母缩写词,即使被广泛接受,非必要时也不要用;
下划线虽然合法,但标识符一般不要包含下划线、连字符或其他非 字母 / 数字 字符。
[规范]
1. 要把只包含两个字母的首字母缩写词全部大写,除非它是驼峰大小写风格标识符的第一个单词;
2. 包含 3 个或更多字母的首字母缩写词,仅第一个字母才要大写,除非该缩写词是驼峰大小写风格标识符的第一个单词;
3. 在驼峰大小写风格标识符开头的首字母缩写词中,所有字母都不要大写;
4. 不要使用匈牙利命名法 (也就是,不要为变量名称附加类型前缀) ;
关键字附加 “@” 前缀可作为标识符使用。
C# 中所有代码都出现在一个类型定义的内部,最常见的类型定义是以关键字 class 开头的。
(P006)
对于包含 Main() 方法的类, Program 是个很好的名称。
[规范]
1. 要用名词或名词短语命名类;
2. 要为所有类名使用 Pascal 大小写风格;
一个程序通常包含多个类型,每个类型都包含多个方法。
方法可以重用,可以在多个地方调用,所以避免了代码的重复。
方法声明除了负责引入方法之外,还要定义方法名以及要传入和传出方法的数据。
C# 程序从 Main 方法开始执行,该方法以 static void Main() 开头。
程序会启动并解析 Main 的位置,然后执行其中第一条语句。
虽然 Main 方法声明可以进行某种程度的改变,但关键字 static 和方法名 Main 是始终都是程序必需的。
C# 要求 Main 方法的返回类型为 void 或 int ,而且要么不带参数,要么接收一个字符串数组作为参数。
(P007)
args 参数是一个字符串数组,用于接收命令行参数。
Main 返回的 int 值是状态码,标识程序执行是否成功。返回非零值通常意味着错误。
C# 的 Main 方法名使用大写 M ,以便与 C# 的 Pascal 大小写风格命名约定保持一致。
Main() 之前的 void 表明该方法不返回任何数据。
C# 通常用分号标识语句结束,每条语句都由代码要执行的一个或多个行动构成。
由于换行与否不影响语句的分隔,所以可以将多条语句放到同一行, C# 编译器会认为这一行包含多条指令。
C# 还允许一条语句跨越多行。同样地, C# 编译器会根据分号判断语句的结束位置。
(P008)
分号使 C# 编译器能忽略代码中的空白。除了少数例外情况, C# 允许在代码中随意插入空白而不改变其语义。
空白是一个或多个连续的格式字符 (如制表符、空格和换行符) 。删除单词间的所有空白肯定会造成歧义。删除引号字符串中的任何空白也会造成歧义。
程序员经常利用空白对代码进行缩进来增强可读性。
为了增强可读性,利用空白对代码进行缩进是非常重要的。写代码时要遵循已经建立的编码标准和约定,以增强代码的可读性。
(P009)
声明变量就是定义它,需要 :
1. 指定变量要包含的数据的类型;
2. 为它分配标识符 (变量名) ;
一个变量声明所指定的数据的类型称为数据类型。数据类型,或者简称为类型,是具有相似特征和行为的个体的分类。
在编程语言中,类型是被赋予了相似特性的一些个体的定义。
(P010)
局部变量名采用的是驼峰大小写风格命名 (即除了第一个单词,其他的每个单词的首字母大写) ,而且不包含下划线。
[规范]
1. 要为局部变量使用 camel 大小写风格的命名;
局部变量声明后必须在引用之前为其赋值。
C# 允许在同一条语句中进行多个赋值操作。
(P011)
赋值后就能用变量标识符引用值。
所有 string 类型的数据,不管是不是字符串字面量,都是不可变的 (或者说是不可修改的) 。也就是说,不能修改变量最初引用的数据,只能重新为变量赋值,让它引用内存中的新位置。
System.Console.ReadLine() 方法的输出,也称为返回值,就是用户输入的文本字符串。
(P012)
System.Console.Read() 方法返回的是与读取的字符值对应的整数,如果没有更多的字符可用,就返回 -1 。为了获取实际字符,需要先将整数转型为字符。
除非用户按回车键,否则 System.Console.Read() 方法不会返回输入。按回车键之前不会对字符进行处理,即使用户已经输入了多个字符。
C# 2.0 以上的版本可以使用 System.Console.ReadKey() 方法。它和 System.Console.Read() 方法不同,用户每按下一个键就返回用户所按的键。可用它拦截用户按键操作,并执行相应行动,如校验按键,限制只能按数字键。
(P013)
在字符串插值中,编译器将字符串花括号中的部分解释为可以嵌入代码 (表达式) 的区域,编译器将对嵌入的表达式估值并将其转换为字符串。字符串插值不需要先逐个执行很多个代码片段,最后再将结果组合成字符串,它可以一步完成这些输出。这使得代码更容易理解。
C# 6.0 之前的版本利用的是复合格式化 (composite formatting) 来进行一次性输出。在复合格式化中,代码首先提供格式字符串 (format string) 来定义输出格式。
(P014)
占位符在格式字符串中不一定按顺序出现。
占位符除了能在格式字符串中按任意顺序出现之外,同一个占位符还能在一个格式字符串中多次使用。
(P015)
[规范]
1. 不要使用注释,除非代码本身 “一言难尽” ;
2. 要尽量编写清晰的代码,而不是通过注释澄清复杂的算法;
(P016)
在 .NET 中,一个程序集包含的所有类型 (以及这些类型的成员) 构成这个程序集的 API 。
同样,对于程序集的组合,例如 .NET Framework 中的程序集组合,每个程序集的 API 组合在一起构成一个更大的 API 。这个更大的 API 组通常被称为框架 (framework) , .NET Framework 就是指 .NET 包含的所有程序集对外暴露的 API 。
一般地, API 包括一系列接口和协议 (或指令) ,它们定义了程序和一组部件交互的规则。实际上,在 .NET 中,协议本身就是 .NET 程序集执行的规则。
(P017)
一个公共编程框架,称为基类库 (Base Class Library , BCL) ,提供开发者能够 (在所有 CLI 实现中) 依赖的大型代码库,使他们不必亲自编写这些代码。
(P018)
.NET Core 不同于完整的 .NET Framework 功能集,它包含了整个 (ASP.NET) 网站可以在 Windows 之外的操作系统上部署所需的功能以及 IIS (Internet Information Server , 因特网信息服务器) 。这意味着,同样的代码可以被编译和执行成跨平台运行的应用程序。
.NET Core 包含了 .NET 编译平台 (“Roslyn”) 、 .NET Core 运行时、 .NET 版本管理 (.NET Version Manager , DNVM) 以及 .NET 执行环境 (.NET Execution Environment , DNX) 等工具,可以在 Linux 和 OS X 上执行。
(P020)
事实上,一些免费工具 (如 Red Gate Reflector 、 ILSpy 、 JustDecompile 、 dotPeek 和 CodeReflect) 可以将 CIL 自动反编译成 C# 。
【第02章】
(P022)
C# 有几种类型非常简单,被视为其他所有类型的基础。这些类型称为预定义类型 (predefined type) 。
C# 语言的预定义类型包括 8 种整数类型、 2 种用于科学计算的二进制浮点类型、 1 种用于金融计算的十进制浮点类型、 1 种布尔类型以及 1 种字符类型。
decimal 是一种特殊的浮点类型,能够存储大数值而无表示错误。
(P023)
C# 的所有基本类型都有短名称和完整名称。完整名称对应于 BCL (Base Class Library , 基类库) 中的类型命名。
由于基本数据类型是其他类型的基础,所以 C# 为基本数据类型的完整名称提供了短名称或缩写的关键字。
C# 开发人员一般选择使用 C# 关键字。
[规范]
1. 要在指定数据类型时使用 C# 关键字而不是 BCL 名称 (例如,使用 string 而不是 String) ;
2. 要保持一致而不要变来变去;
(P024)
浮点数的精度是可变的。
与浮点数不同, decimal 类型保证范围内的所有十进制数都是精确的。
虽然 decimal 类型具有比浮点类型更高的精度,但它的范围较小。
decimal 的计算速度稍慢 (虽然这个差别可以忽略不计) 。
除非超过范围,否则 decimal 数字表示的十进制数都是完全准确的。
(P025)
默认情况下,输入带小数点的字面量,编译器会自动把它解释成 double 类型。
整数值 (没有小数点) 通常默认为 int ,但前提是该值不要太大,以至于无法用 int 来存储。
要显示具有完整精度的数字,必须将字面量显式声明为 decimal 类型,这是通过追加一个 M (或者 m) 后缀来实现的。
(P026)
d 表示 double ,之所以用 m 表示 decimal ,是因为这种数据类型经常用于货币 (monetary) 计算。
对于整数数据类型,相应的后缀是 U 、 L 、 LU 和 UL 。整数字面量的类型是像下面这样确定的 :
1. 没有后缀的数值字面量按照以下顺序,解析成能够存储该值的第一种数据类型 : int 、 uint 、 long 、 ulong ;
2. 具有后缀 U 的数值字面量按照以下顺序,解析成能够存储该值的第一种数据类型 : uint 、 ulong ;
3. 具有后缀 L 的数值字面量按照以下顺序,解析成能够存储该值的第一种数据类型 : long 、 ulong ;
4. 如果数值字面值的后缀是 UL 或 LU ,则解析成 ulong 类型;
注意,字面量的后缀不区分大小写。但对于 long ,一般推荐使用大写字母 L ,因为小写字母 l 和数字 1 不好区分。
[规范]
1. 要使用大写的字面量后缀;
2. 十六进制和十进制的相互转换不会改变数本身,改变的只是数的表示形式;
(P027)
要以十六进制形式输出一个数值,必须使用 x 或 X 数值格式说明符。大小写决定了十六进制字母的大小写。
(P028)
虽然从理论上说,一个二进制位就足以容纳一个布尔类型的值,但 bool 数据类型的实际大小是一个字节。
字符类型 char 表示 16 位字符,其取值范围对应于 Unicode 字符集。
从技术上说, char 的大小和 16 位无符号整数 (ushort) 相同,后者的取值范围是 0 ~ 65535 。
(P029)
为了输入 char 类型的字面量,需要将字符放到一对单引号中。
反斜杠和特殊字符代码组成转义序列 (escape sequence) 。
可以使用 Unicode 代码表示任何字符。为此,请为 Unicode 值附加 \u 前缀。
(P030)
零或多个字符组成的有限序列称为字符串。
为了将字面量字符串输入代码,要将文本放入双引号 (") 内。
字符串由字符构成,所以转义序列可以嵌入字符串内。
双引号要用转义序列输出,否则会被用于定义字符串开始与结束。
在 C# 中,可以在字符串前面使用 @ 符号,指明转义序列不被处理。
结果是一个逐字字符串字面量 (verbatim string literal) ,它不仅将反斜杠当作普通字符处理,还会逐字解释所有空白字符。
(P031)
在以 @ 开头的字符串中,唯一支持的转义序列是 "" ,它代表一个双引号,这个双引号不会终止字符串。
假如同一个字符串字面量在程序集中多次出现,编译器在程序集中只定义字符串一次,且所有变量都将指向同一个字符串。
通过使用字符串插值格式,字符串可以支持嵌入的表达式。字符串插值语法在一个字符串字面量前加上一个 $ 符号前缀,然后将表达式嵌入大括号中。
注意,字符串字面量可以通过在 “@” 符号前加上 “$” 符号的字符串插值组合而成。
(P032)
字符串插值是调用 string.Format() 方法的简写。
(P033)
string.Format() 不是在控制台窗口中显示结果,而是返回结果。
增加了字符串插值功能之后, string.Format() 的重要性减弱了不少 (除了对本地化功能的支持) 。在后台,字符串插值是利用了 string.Format() 编译成 CIL 的。
目前静态方法的调用通常是包含一个命名空间的前缀后面跟类型名。
(P034)
using static 指令必须放在文件的最开始。
using static 指令只对静态方法和属性有效,对于实例成员不起作用。
using 指令与 using static 指令类似,使用后也可以省略命名空间前缀。与 using static 指令不同的是, using 指令在文件 (或命名空间) 中应用非常普遍,不仅只应用于静态成员。无论是实例化,或是静态方法调用,抑或是使用 C# 6.0 中新增的 nameof 操作符时,使用 using 指令都可以随意地省略所有的命名空间引用。
无论是使用 string.Format() 还是用 C# 6.0 的字符串插值功能构造复杂格式的字符串,总要用一组丰富的、复杂的格式模板来显示数字、日期、时间、时间段等等。
如果想在一个插值的字符串或格式化的字符串中真正出现左大括号或者右大括号,可以通过连续输入两个大括号表明这个大括号不是引入的格式模板。
输出新行所需的字符取决于执行代码的操作系统。
(P035)
字符串的长度不能直接设置,它是根据字符串中的字符数计算得到的。此外,字符串的长度不能更改,因为字符串是不可变的。
string 类型的关键特征在于它是不可变的 (immutable) 。
(P036)
与类型有关的两个额外的关键字是 null 和 void 。 null 值由关键字 null 标识,表明变量不引用任何有效的对象。 void 表示没有类型,或者没有任何值。
(P037)
null 也可以作为字符串字面量的类型使用。 null 表示将变量设为 “无” 。 null 值只能赋给引用类型、指针类型和可空值类型。
将变量设为 null ,会显式地设置引用,使它不指向任何位置。
必须注意,和根本不赋值相比,将 null 赋给引用类型的变量是完全不同的概念。换言之,赋值为 null 的变量已被设置,而未赋值的变量未被设置,所以假如在赋值前使用变量会造成编译时错误。
将 null 值赋给一个 string 变量,并不等同于将空字符串 "" 赋给它。 null 意味着变量无任何值,而 "" 意味着变量有一个称为 “空字符串” 的值。这种区分相当有用。
在返回类型的位置使用 void 意味着方法不返回任何数据,同时告诉编译器不要期望会有一个值。 void 本质上并不是一个数据类型,它只是用于指出没有数据返回这一事实。
(P038)
C# 3.0 新增了上下文关键字 var 来声明隐式类型的局部变量。
虽然允许使用 var 取代显式的数据类型,但在数据类型已知的情况下最好不要使用 var 。
用 var 声明变量,右侧的数据类型应该是非常明显的;否则应该考虑避免使用 var 声明。
C# 3.0 添加 var 的目的是支持匿名类型。匿名类型是在方法内部动态声明的数据类型,而不是通过显式的类定义来声明的。
(P039)
所有类型都可以归为值类型或引用类型。它们的区别在于复制方式 : 值类型的数据总是进行值复制,而引用类型的数据总是进行引用复制。
值类型变量直接包含值。换言之,变量引用的位置就是值在内存中实际存储的位置。因此,将第一个变量的值赋给第二个变量会在新变量的位置创建原始变量的值的一个内存副本。相同值类型的第二个变量不能引用和第一个变量相同的内存位置。所以,更改第一个变量的值不会影响第二个变量的值。
由于值类型需要创建内存副本,因此定义时不要让它们占用太多内存 (通常应该小于 16 字节) 。
(P040)
引用类型的值存储的是对数据存储位置的引用,而不是直接存储数据。要去那个位置才能找到真正的数据。因此,为了访问数据, “运行时” 要先从变量中读取内存位置,再 “跳转” 到包含数据的内存位置。引用类型指向的内存区域称堆 (heap) 。
引用类型不像值类型那样要求创建数据的内存副本,所以复制引用类型的实例比复制大的值类型实例更高效。
将引用类型的变量赋给另一个引用类型的变量,只会复制引用而不需要复制所引用的数据。
事实上,每个引用总是处理器的 “原生大小” 。也就是, 32 位处理器只需复制 32 位引用, 64 位处理器只需复制 64 位引用,以此类推。
显然,复制对一个大数据块的引用,比复制整个数据块快得多。
由于引用类型只复制对数据的引用,所以两个不同的变量可引用相同的数据。
如果两个变量引用同一个对象,利用一个变量更改对象的字段,用另一个对象访问字段时将看到更改结果。无论赋值还是方法调用都会如此。
在决定定义引用类型还是值类型时,一个决定性的因素就是 : 如果对象在逻辑上是固定大小的不可变的值,就考虑定义成值类型;如果逻辑上是可引用的可变的对象,就考虑定义成引用类型。
(P041)
一般不能将 null 值赋给值类型。这是因为根据定义,值类型不能包含引用,即使是对 “无 (nothing)” 的引用。
为了声明可以存储 null 的变量,要使用可空修饰符 (?) 。
将 null 赋给值类型,这在数据库编程中尤其有用。
有可能造成大小变小或者引发异常 (因为转换失败) 的任何转换都需要执行显式转型 (explicit cast) 。相反,不会变小,而且不会引发异常 (无论操作数的类型是什么) 的任何转换都属于隐式转型 (implicit cast) 。
在 C# 中,可以使用转型操作符执行转型。通过在圆括号中指定希望变量转换成的类型,表明你已认可在发生显式转型时可能丢失精度和数据,或者可能造成异常。
(P043)
C# 还支持 unchecked 块,它强制不进行溢出检查,不会为块中溢出的赋值引发异常。
即使编译时打开了 checked 选项,在执行期间, unchecked 关键字也会阻止 “运行时” 引发异常。
(P044)
即使不要求显式转换操作符 (因为允许隐式转型) ,仍然可以强制添加转型操作符。
每个数值数据类型都包含一个 Parse() 方法,它允许将字符串转换成对应的数值类型。
可利用特殊类型 System.Convert 将一种类型转换成另一种类型。
System.Convert 只支持小的数据类型,而且是不可扩展的。它允许从 bool 、 char 、 sbyte 、 short 、 int 、 long 、 ushort 、 uint 、 ulong 、 float 、 double 、 decimal 、 DateTime 和 string 类型中的任何一种类型转换到另一种类型。
所有类型都支持 ToString() 方法,可以用它提供一个类型的字符串表示。
(P045)
对于大多数类型, ToString() 方法只是返回数据类型的名称,而不是数据的字符串表示。只有在类型显式实现了 ToString() 的前提下才会返回字符串表示。
从 C# 2.0 (.NET 2.0) 开始,所有基元数值类型都包含静态 TryParse() 方法。该方法与 Parse() 非常相似,只是转换失败的情况下,它不引发异常,而是返回 false 。
Parse() 和 TryParse() 的关键区别在于,假如转换失败, TryParse() 不会引发异常。
C# 中的数组是基于零的。
数组中每个数据项都使用名为索引的整数值进行唯一性标识。 C# 数组中的第一个数据项使用索引 0 访问。
程序员应确保指定的索引值小于数组的大小 (数组中的元素总数) 。
因为 C# 数组是基于零的,所以数组最后一个元素的索引值要比数组元素的总数小 1 。
(P046)
初学者可将索引想象成偏移量。第一项距离数组开头的偏移量是 0 ,第二项的偏移量是 1 ,依次类推。
数组是几乎每一种编程语言的基本组成部分,因此所有开发人员都要学会它。
在 C# 中,使用方括号声明数组变量。首先要指定数组元素的类型,后跟一对方括号,再输入变量名。
在 C# 中,作为数组声明一部分的方括号是紧跟在数据类型之后的,而不是出现在变量声明之后。
(P047)
使用更多的逗号,可以定义更多的维。数组总维数等于逗号数加 1 。
数组如果在声明后赋值,则需要使用 new 关键字。
(P048)
自 C# 3.0 起,不必在 new 后面指定数组的数据类型,只要编译器能根据初始化列表中的数据类型推断出数组元素的类型。但是,方括号仍然不可缺少。
只要将 new 关键字作为数组赋值的一部分,就可以同时在方括号内指定数组的大小。
在初始化语句中指定的数组的大小必须和大括号中包含的元素数量相匹配。
从 C# 2.0 开始可以使用 default() 表达式判断数据类型的默认值。 default() 获取数据类型作为参数。
由于数组大小不需要作为变量声明的一部分,所以可以在运行时指定数组大小。
(P050)
多维数组的每一维的大小都必须一致。
交错数组不使用逗号标识新的维。相反,交错数组定义由数组构成的数组。
注意,交错数组要求内部的每个数组都创建数组实例。
(P051)
数组的长度是固定的,不能随便更改,除非重新创建数组。
Length 成员返回数组中数据项的个数,而不是返回最高的索引值。
为了将 Length 作为索引来使用,有必要在它上面减 1 ,以避免越界错误。
(P052)
Length 返回数组中元素的总数。
对于交错数组, Length 返回的是外部数组的元素数。
(P053)
使用 System.Array.BinarySearch() 方法前要对数组进行排序。
System.Array.Clear() 方法不删除数组元素,而且不将长度设为零。
System.Array.Clear() 方法将数组中的每个元素都设为其默认值。
要获取特定维的长度不是使用 Length 属性,而是使用数组的 GetLength() 实例方法。
(P054)
可以访问数组的 Rank 成员来获取整个数组的维数。
默认情况下,将一个数组变量赋值给另一个数组变量只会复制数组引用,而不是数组中单独的元素。要创建数组的全新副本,需使用数组的 Clone() 方法。该方法返回数组的一个副本,更改这个新数组中的任何成员都不会影响原始数组的成员。
可以使用字符串的 ToCharArray() 方法,将整个字符串作为字符数组返回。
(P055)
用于声明数组的方括号放在数据类型之后,而不是在变量标识符之后。
(P056)
如果是在声明之后再对数组进行赋值,需要使用 new 关键字,并可选择指定数据类型。
不能在变量声明中指定数组大小。
除非提供数组字面量,否则必须在初始化时指定数组大小。
数组的大小必须与数组字面量中的元素个数相符。
【第03章】
(P058)
通常将操作符划分为 3 大类 : 一元操作符、二元操作符和三元操作符,它们对应的操作数分别是 1 个、 2