您好,欢迎来到网暖!

当前位置:网暖 » 站长资讯 » 建站基础 » 网络技术 » 文章详细 订阅RssFeed

《C# 6.0 本质论》 - 学习笔记

来源:网络整理 浏览:134次 时间:2020-08-10

**《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 个和 3 个。

使用负操作符 (-) 等价于从零减去操作数。

一元正操作数 (+) 对值几乎没有影响。它在 C# 语言中是多余的,只是出于对称性的考虑才加进来。

二元操作符要求两个操作数。 C# 为二元操作符使用中缀表示法 : 操作符在左、右操作数之间。每个二元表达式的结果要么赋给一个变量,要么以某种方式使用 (例如用作为另一个表达式的操作数) 。

在 C# 中,只有调用、递增、递减和对象创建表达式才能作为独立的语句使用。

一元 (+) 操作符定义为获取 int 、 uint 、 long 、 ulong 、 float 、 double 和 decimal 类型 (及其可空版本) 的操作数。用于其他类型 (如 short ) 时,操作数会根据实际情况转换为上述某个类型。

算数操作符的每一边都有一个操作数,计算结果赋给一个变量。

(P059)

圆括号可以明确地将一个操作数与它所属的操作符相关联。

(P060)

C# 的大多数操作符都是左结合的,赋值操作符右结合。

有时候,圆括号操作符并不改变表达式的求值结果。不过,使用圆括号来提高代码的可读性依然是一良好的编程的习惯。

[规范]

  1. 要使用圆括号增加代码的易读性,尤其是在操作符优先级不是让人一目了然的时候;

在 C# 中,操作数总是从左向右求值。

操作符也可用于非数值类型。例如,可以使用加法操作符来拼接两个或者更多字符串。

(P061)

当必须进行本地化时,应该有节制地使用加法操作符,最好使用组合格式化。

[规范]

  1. 当必须进行本地化时,要用组合格式化而不是加法操作符来拼接字符串;

虽然 char 类型存储的是字符而不是数字,但它是整型 (意味着它基于整数) ,可以和其他整型一起参与算数运算。然而,不是基于存储的字符来解释 char 类型的值,而是基于它的基础值。

可以利用 char 类型的这个特点判断两个字符相距多远。

(P062)

二进制浮点类型实际存储的是二进制分数而不是十进制分数。所以,一次简单的赋值就可能引发精度问题。

[规范]

  1. 避免在需要准确的十进制算术运算时使用二进制浮点类型,而是使用 decimal 浮点类型;

比较两个值是否相等的时候,浮点类型的不准确性可能造成非常严重的后果。

(P063)

[规范]

  1. 避免将二进制浮点类型用于相等性条件式。要么判断两个值之差是否在容差范围之内,要么使用 decimal 类型;

(P064)

(+=) 操作符使左边的变量递增右边的值。

(P065)

赋值操作符还可以和减法、乘法、除法和取余操作符结合。

C# 提供了特殊的一元操作符来实现计数器的递增和递减。递增操作符 (++) 每次使一个变量递增 1 。

可以使用递减操作符 (--) 使变量递减 1 。

递增和递减操作符在循环中经常用到。

(P066)

递增和递减操作符用于控制特定操作的执行次数。

只要数据类型支持 “下一个值” 和 “上一个值” 的概念,就适合使用递增和递减操作符。

递增或递减操作符的位置决定了所赋的值是操作数计算之前还是之后的值。

(P067)

递增和递减操作符相对于操作数的位置影响了表达式的结果。前缀操作符的结果是变量 递增 / 递减 之后的值,而后缀操作符的结果是变量 递增 / 递减 之前的值。

[规范]

  1. 避免混淆递增和递减操作符的用法;

(P068)

常量表达式是 C# 编译器能在编译时完成求值的表达式 (而不是在程序运行时才能求值) ,因为其完全由常量操作数构成。

const 关键字的作用就是声明常量符号。由于常量和 “变量” 相反 —— “常” 意味着 “不可变” —— 以后在代码中任何修改它的企图都会造成编译时错误。

[规范]

  1. 不要使用常量表示将来可能改变的任何值;

(P072)

规范提倡除了单行语句之外都使用代码块。

使用大括号,可以将多个语句合并成代码块,允许在符合条件时执行多个语句。

(P074)

事实上,设计规范规定除非是单行语句,否则不要省略大括号。

[规范]

  1. 避免在 if 语句中省略大括号,除非只有一行语句;

总的来说,作用域决定一个名称引用什么事物,而声明空间决定同名的两个事物是否冲突。

(P075)

声明空间中的每个局部变量名称必须是唯一的。声明空间覆盖了包含在最初声明局部变量的代码块中的所有子代码块。

(P076)

相等性操作符使用两个等号,赋值操作符使用一个等号。

(P077)

关系和相等性操作符总是生成 bool 值。

逻辑操作符 (logic operator) 获取布尔操作数并生成布尔结果。可以使用逻辑操作符合并多个布尔表达式来构成更复杂的布尔表达式。

(P078)

^ 符号是异或 (exclusive OR , XOR) 操作符,若应用于两个布尔操作数,那么只有在两个操作数中仅有一个为 true 的前提下, XOR 操作符才会返回 true 。

条件操作符是三元操作符,因为它需要 3 个操作数,即 condition 、 consequence 和 alternative 。

作为 C# 中唯一的三元操作符,条件操作符也经常被称为 “三元操作符” 。

(P079)

和 if 语句不同,条件操作符的结果必须赋给某个变量 (或者作为参数传递) 。它不能单独作为一个语句使用。

[规范]

  1. 考虑使用 if / else 语句,而不是使用过于复杂的条件表达式;

空接合操作符 (null coalescing operator) ?? 能简单地表示 “如果这个值为空,就使用另一个值” 。

?? 操作符支持短路求值。

(P080)

空接合操作符能完美地 “链接” 。

空结合操作符是 C# 2.0 和可空值类型一起引入的,它的操作数既可以是可空值类型,也可以是引用类型。

C# 6.0 引入了一种更为简化的 null 条件操作符 (null-condition operator) ?. 。

(P083)

两个移位操作符是 >> 和 << ,分别称为右移位和左移位操作符。除此之外,还有复合移位和赋值操作符 <<= 和 >>= 。

AND 和 OR 操作符的按位版本不进行 “短路求值” 。

(P086)

按位取反操作符 (~) 是对操作数的每一位取反,操作数可以是 int 、 uint 、 long 和 ulong 类型。

(P087)

斐波那契数 (Fibonacci number) 是斐波那契数列 (Fibonacci series) 的成员,这个数列中的所有数都是数列中前两个数之和。数列最开头两个数是 1 和 1 。

for 主要用于重复次数已知的循环,比如从 0 ~ n 的计数。 do / while 类似于 while 循环,区别在于它至少会循环一次。

do / while 循环与 while 循环非常相似,只是它最适合需要循环 1 ~ n 次的情况,而且 n 在循环开始前无法确定。 do / while 循环的一个典型应用就是反复提醒用户输入。

(P088)

由于递增操作在循环语法中有一席之地,所以递增和递减操作符经常作为 for 循环的一部分使用。

(P089)

[规范]

  1. 如果发现正在写的 for 循环包含了复杂条件和多个循环变量,要考虑重构方法,以使控制流更容易理解;

for 循环只不过是一种比写 while 循环更方便的方法。 for 循环能改写成 while 循环。

(P090)

[规范]

  1. 假如事先知道循环次数,而且循环中需要用到控制循环次数的 “计数器” ,那么要使用 for 循环;

  2. 假如事先不知道循环次数,而且不需要计数器,那么要使用 while 循环;

foreach 循环的特点是每一项只被遍历一次 : 不会像其他循环那样出现计数错误,也不可能越过集合边界。

(P092)

将一个值和许多不同的常量值比较时, switch 语句比 if 语句更容易理解。

switch 的 “主导类型” (governing type) 允许的主导数据类型包括 bool 、 sbyte 、 byte 、 short 、 ushort 、 int 、 uint 、 long 、 ulong 、 char 、 任何枚举 (enum) 类型、上述所有值类型的可空类型以及 string 。

[规范]

  1. 不要使用 continue 作为跳转语句退出 switch 小节。如果 switch 语句是在一个循环中使用的,这样写是合法的。但是,这样做很容易对之后的 switch 小节中出现的 break 语句的意义感到迷惑;

(P093)

switch 语句至少要有一个 switch 小节。

虽然在之前的规范中提到,在一般情况下应该避免省略大括号,但有一个例外,就是要省略 case 和 break 语句的大括号,因为它们的作用是指示一个块的开始与结束。

(P094)

switch 小节可以以任意顺序出现, default 小节不一定非要出现在 switch 语句的最后。事实上, default 的 switch 小节完全可以省略;它是可选的。

C# 要求每个 switch 小节 (包括最后一个小节) 的结束点 “不可到达” 。这意味着 switch 小节通常以 break 、 return 、 throw 或 goto 结尾。

如果希望 switch 小节执行另一个 switch 小节中的语句,可以显式使用 goto 语句来实现。

C# 使用 break 语句退出循环或者 switch 语句。任何时候遇到 break 语句,控制都会立即离开循环或 switch 。

(P097)

一般都可以使用 if 语句代替 continue 语句,这样做还能增强可读性。

continue 语句的问题在于,它在一次循环中提供了多个出口,从而影响了可读性。

C# 确实支持 goto ,而且只能利用 goto 在 switch 语句中实现贯穿。

(P098)

C# 禁止通过 goto 跳转到代码块内部。只能用 goto 在代码块内部跳转,或者跳到一个封闭的代码块。

[规范]

  1. 避免使用 goto ;

控制流语句中的条件表达式在运行时求值。相反,C# 预处理器在编译时调用。

每个预处理指令都以 # 开头,而且必须在一行中写完。换行符 (而不是分号) 标志着预处理指令的结束。

(P102)

C# 允许使用 #region 指令声明代码区域。 #region 和 #endregion 必须成对使用,两个指令都可以选择在指令后面跟随一个描述性的字符串。除此之外,还可以将一个区域嵌套到另一个区域中。

【第04章】

(P106)

[规范]

  1. 要为方法名使用动词或动词短语;

方法总是和类型 —— 通常是类 —— 关联。类型将相关的方法分为一组。

方法通过返回值将数据返回给调用者。

(P107)

方法调用由方法名称和实参列表和返回值构成。

命名空间是一种分类机制,用于组合功能相关的所有类型。

命名空间是分级的,级数可以任意,但是很少见到超过 6 级的命名空间。

命名空间主要用于按照功能领域组织类型,以便更容易地查找和理解它们。

[规范]

  1. 要为命名空间使用 Pascal 大小写风格;

  2. 考虑将源代码的文件目录结构组织成与命名空间的层级结构相匹配的形式;

类型本质上是对方法及其相关数据进行组合的一种方式。

(P109)

在方法名称之后是圆括号中的实参列表,每个实参以逗号分隔,对应于声明方法时指定的形参。

方法可接收任意数量的形参,每个形参都具有特定的数据类型。调用者为形参提供的值称为实参;每个实参都要和一个形参对应。

可以将方法的返回值作为另一个方法的实参使用。

[注意]

  1. 通常,开发者应侧重于可读性,而不是在写出更短的代码方面耗费心机。为了使代码一目了然,进而在长时间里更容易维护,可读性是关键;

(P111)

C# 的每个方法都必须在某个类型中。

将一组相关语句转移到一个方法中,而不是把它们留在一个较大的方法中,这是重构 (refactoring) 的一种形式。

与简单地为一个代码块加上注释相比,重构的效果更好,因为只需看方法名就可清楚地知道这个方法要做的事情。

(P112)

  1. 要为参数名使用驼峰大小写风格;

虽然方法可以指定多个参数,但返回类型只能有一个。

如果方法有返回类型,它的主体必须有 “不可到达的结束点” 。

换言之,一个具有返回类型的方法不允许在不返回任何值的情况下将控制贯穿到方法的末尾。

为了保证这一点,最简单的办法就是将 return 语句作为方法的最后一个语句。

(P113)

注意, return 语句将控制转移出 switch ,所以,在以 return 语句作为方法最后一个语句的方法中,不需要用 break 语句防止非法 “贯穿” switch 小节。

虽然 C# 允许一个方法有多个返回语句,但为了增强代码的可读性,以及使代码更容易维护,应该尽可能地确定单一的退出位置,而不是在方法的多个代码中散布多个 return 语句。

为了支持不带方法主体的最简单的方法声明, C# 6.0 引入了表达式主体方法 (expression bodied method) ,使用表达式而不是一个完整的方法主体来声明一个方法。

与使用大括号包含方法主体不同,表达式主体方法使用 Lambda 操作符 (=>) ,结果数据类型必须与方法的返回类型匹配。也就是说,尽管在表达式主体方法实现中并没有显式的返回语句,表达式的返回类型仍然必须与方法声明的返回类型匹配。

表达式主体方法是完整方法主体声明的语法简化表示。因此,表达式主体方法的使用应限于最简化的方法实现,通常用于单行可表示的方法。

和 C++ 不同, C# 类从来不将实现与声明分开。 C# 不区分头文件 (.h) 和实现文件 (.cpp) 。相反,声明和实现总是出现在同一个文件中。

(P114)

重名的两个或更多类型只要在不同命名空间中,就没有歧义。

using 指令不会导入任何嵌套命名空间 (nested namespace) 中的类型。嵌套命名空间 (由命名空间中的句点符号来标识) 必须显式导入。

与 Java 相比, C# 不允许在 using 指令中使用通配符,每个命名空间都必须显式地导入。

(P115)

不仅可以在文件顶部使用 using 指令,还可以在命名空间声明的顶部包含它们。

在文件顶部放置 using 指令和在命名空间声明的顶部位置 using 指令的区别在于,后者的 using 指令只在声明的命名空间内有效。

(P116)

using static 指令允许省略规定类型的任何成员之前的命名空间和类型名称。

别名的两个最常见的用途是消除两个同名类型的歧义和缩写长名称。

(P120)

调用者中的变量名与被调用方法中的参数名相匹配。这种匹配纯粹是为了增强可读性,名称是否匹配与方法调用的行为无关。被调用方法的参数和发出调用的方法的局部变量在不同声明空间中,相互之间没有任何关系。

(P123)

out 参数在功能上和 ref 参数完全一致,唯一的区别是, C# 语言对别名变量的读写有不同的规定。

开发人员可以通过声明一个或多个 out 参数来克服方法只有一个返回类型的限制。

[注意]

  1. 每个正常返回的代码路径都必须对所有 out 参数进行赋值;

(P124)

参数数组不一定是方法的唯一参数,但必须是方法声明中的最后一个参数。由于只有最后一个参数才可能是参数数组,所以方法最多只能有一个参数数组。

(P125)

[规范]

  1. 当一个方法需要处理任意数量 (包括零个) 额外实参时,要使用参数数组;

(P127)

[注意]

C# 依据方法名、参数数据类型或者参数数量的不同来定义方法的唯一性。

(P129)

实现重载方法时经常采用的一种模式,它的基本思路是 : 开发者只需在一个方法中实现核心逻辑,其他所有重载版本都调用那个方法。如果核心实现发生了改变,那么只需要在一个位置修改,而不必在每个实现中都进行修改。

[注意]

  1. 在一个方法中实现核心功能,所有其他重载的方法都调用这个方法。这意味着你可以只修改核心方法的实现,其他重载的方法就会自动地享受到修改;

从 C# 4.0 开始,语言的设计者增添了对可选参数 (optional parameters) 的支持。声明方法时将常量值赋给参数,以后调用方法时就不必每个参数都指定。

(P130)

可选参数一定放在所有必须的参数 (无默认值的参数) 后面。另外,默认值必须是常量,或者说必须是能在编译时确定的值,这一点极大限制了 “可选参数” 的应用。

(P131)

[规范]

  1. 要尽量为所有参数提供好的默认值;

  2. 要提供简单的方法重载,其必需的参数的数量要少;

  3. 考虑从最简单到最复杂来组织重载;

C# 4.0 新增的另一个方法调用功能是命名参数 (named arguments) 。利用命名参数,调用者可显式地为一个参数赋值,而不是像以前那样只能依据参数顺序来决定哪个值赋给哪个参数。

添加了命名参数后,参数名就成为方法接口的一部分。更改名称会导致使用命名参数的代码无法编译。

[规范]

  1. 要将参数名视为 API 的一部分。如果 API 之间的版本兼容性很重要,就要避免更改参数名;

(P135)

try 关键字告诉编译器 : 开发者认为块中的代码有可能引发异常;如果真的引发了异常,那么某个 catch 块要尝试处理这个异常。

try 块之后必须紧跟着一个或多个 catch 块 (或 / 和一个 finally 块) 。 catch 块可选择指定异常的数据类型。只要数据类型与异常类型匹配,对应的 catch 块就会执行。但是,假如一直找不到合适的 catch 块,引发的异常就会变成一个未处理的异常,就好像没有进行异常处理一样。

(P136)

处理异常的顺序非常重要。 catch 块必须按照从最具体到最不具体排列。

无论控制是正常地离开 try 块还是由于 try 块中的代码引发异常而离开的,只要控制离开 try 块, finally 块就会执行。

finally 块的作用是提供一个最终位置,在其中放入无论是否发生异常都要执行的代码。

finally 块最适合用来执行资源清理。

事实上,完全可以只写一个 try 块和一个 finally 块,而不写任何 catch 块。

无论 try 块是否引发异常,甚至无论是否写了一个 catch 块来处理异常, finally 块都会执行。

(P137)

[规范]

  1. 避免从 finally 块显式地引发异常 (因方法调用而隐式地引发的异常可以被接受) ;

  2. 要优先使用 try / finally 而不是 try / catch 块来实现资源清理代码;

  3. 要在抛出的异常中描述异常为什么发生。如有可能,还要说明如何防范;

(P138)

可以指定一个不获取任何参数的 catch 块。

(P139)

没有指定数据类型的 catch 块称为常规 catch 块 (generic catch block) ,它等价于获取 object 数据类型的 catch 块。由于所有类最终都从 object 派生,所以没有数据类型的 catch 块必须放到最后。

常规 catch 块很少使用,因为没有办法捕获有关异常的任何信息。

[规范]

  1. 避免使用常规 catch 块,而应该使用捕获 System.Exception 的 catch 块来代替;

  2. 避免捕获无法获知其正确行动的异常。对这种异常不进行处理比处理地不正确要好;

  3. 避免在重新引发前捕获和记录异常。要允许异常逃脱,直至它被正确处理;

(P140)

有时 catch 块能捕获到异常,但不能正确或者完整地处理它。在这种情况下,可以让这个 catch 块重新引发异常,具体的办法是使用一个单独的 throw 语句,不要在它后面指定任何异常。

(P141)

[规范]

  1. 要在捕获并重新引发异常时使用空的 throw 语句,以便保持调用栈;

  2. 要通过引发异常而不是返回错误码来报告执行失败;

  3. 不要让公共成员将异常作为返回值或者 out 参数。要通过异常来指明错误;不要通过它们作为返回值来指明错误;

异常是专门为了跟踪例外的、事先没有预料到的、而且可能造成严重后果的情况而设计的。为预料之中的情况使用异常,会造成代码难以阅读、理解和维护。

[规范]

  1. 不要用异常来处理正常的、预期的情况;用异常处理异常的、非预期的情况;

(P142)

从 .NET Framework 4 开始,枚举类型也添加了 TryParse() 方法;

【第05章】

(P144)

面向对象编程的关键优势之一是不需要完全从头创建新的程序。而是可以将现有的一系列对象组装到一起,并用新的功能扩展类,或者添加更多的类。

为了支持封装, C# 必须支持类、属性、访问修饰符以及方法。

开发人员一旦熟悉了面向对象编程,除非写一些极为简单程序,否则很难回到结构化编程。

(P146)

虽然并非必须,但一般应该将每个类都放到它自己的文件中,用类名对文件进行命名。这样可以更容易地寻找定义了一个特定类的代码。

[规范]

  1. 不要在一个源代码文件中放置多个类;

  2. 要用所含公共类型的名称来命名源代码文件;

定义好新类后,就可以像使用 .NET Framework 内置的类那样使用它了。

换言之,可以声明那个类型的变量,或者定义方法来接收新类型的参数。

类是模板,定义了对象在实例化的时候看起来像什么样子。所以,对象是类的实例。

从类创建对象的过程称为实例化 (instantiation) ,因为对象是类的实例 (instance) 。

C# 使用 new 关键字实例化对象。

(P147)

面向对象编程将方法和数据装入对象。这提供了所有类成员 (类的数据和方法) 的一个分组,使它们不再需要单独处理。

程序员应将 new 的作用理解成实例化对象而不是分配内存。在堆和栈上分配对象都支持 new 操作符,这进一步强调了 new 不是关于内存分配的,也不是关于是否有必要进行回收的。

和 C++ 不同, C# 不支持隐式确定性资源清理 (在编译时确定的位置进行隐式对象析构) 。幸好, C# 通过 using 语句支持显式确定性资源清理,通过终结器支持隐式非确定性资源清理。

(P148)

面向对象设计的一个核心部分是对数据进行分组,以提供一个特定的结构。

在面向对象术语中,在类中存储数据的变量称为成员变量。

实例字段是在类的级别上声明的变量,用于存储与对象关联的数据。因此,关联 (association) 是字段类型和包容类型之间的联系。

注意,字段不包含 static 修饰符,这意味着它是实例字段。只能从其包容类的实例 (对象) 中访问实例字段,无法直接从类中访问 (换言之,不创建实例就不能访问) 。

(P150)

静态方法不能直接访问类的实例字段,必须获取类的实例才能调用实例成员 —— 无论该实例成员是方法还是字段。

在类的实例成员内部,可以获取对这个类的引用。在 C# 中,为了显式指出当前访问的字段或方法是包容类的实例成员,可以使用关键字 this 。调用任何实例成员时 this 都是隐式的,它返回对象本身的实例。

(P151)

虽然可为所有本地类成员引用添加 this 前缀,但规范的原则是,如果不会带来更多的价值就不要在代码中“添乱”。所以,只在必要时才使用 this 关键字。

(P152)

C# 关键字 this 完全等价于 Visual Basic 关键字 Me 。

假如存在与字段同名的局部变量或参数,省略 this 将访问局部变量或参数,而不是字段。所以,在这种情况下, this 是必须的。

还可使用 this 关键字显式访问类的方法。

有时需要使用 this 传递对当前正在执行的对象的引用。

(P156)

在类的外部不可见的成员称为私有成员。

(P157)

如果不为类成员添加访问修饰符,那么默认使用的是 private 。也就是说,成员默认为私有成员。公共成员必须显式指定。

(P161)

在 C# 6.0 之前的版本中,属性初始化只能通过方法进行。但到了 C# 6.0 ,就可以使用类似字段初始化的语法,在声明时自动初始化实现的属性。

[规范]

  1. 要使用属性简化对简单数据 (进行少量计算) 的访问;

  2. 避免从属性的取值方法中引发异常;

  3. 要在属性引发异常时保留原始属性值;

  4. 如果没有额外的实现逻辑,要优先使用自动实现的属性,而不是带有简单支持字段的属性;

[规范]

  1. 考虑为支持字段和属性使用相同的大小写风格,为支持字段附加 “_” 前缀。但不要使用双下划线,因为以双下划线开头的标识符是为 C# 编译器保留的;

  2. 要使用名词、名词短语或形容词来命名属性;

  3. 考虑让属性和它的类型同名;

  4. 避免用驼峰大小写风格命名字段;

  5. 如果有用的话,要为布尔属性附加 “Is” “Can” 或 “Has” 前缀;

  6. 不要声明 public 或 protected 的实例字段 (而是通过属性来公开字段) ;

  7. 要用 Pascal 大小写风格命名属性;

  8. 要优先使用自动实现的属性而不是字段;

  9. 如果没有额外的实现逻辑,要优先使用自动实现的属性,而不是自己编写完整版本;

(P163)

[规范]

  1. 避免从属性外部 (即使是在包容属性的类中) 访问属性的支持字段;

  2. 调用 ArgumentException() 或 ArgumentNullException() 构造器时,要为 paramName 参数传递 “value” (“value” 是属性赋值方法隐含的参数名) ;

(P165)

[规范]

  1. 如果不想调用者更改属性的值,要创建只读属性;

  2. 在 C# 6.0 (或以后的版本) 中,如果不想调用者更改属性的值,要创建只读的自动实现的属性,而不是带有后备字段的只读属性;

(P167)

[规范]

  1. 要为所有属性的取值方法和赋值方法的实现应用适当的可访问性修饰符;

  2. 不要提供只写属性,也不要让属性的赋值方法的可访问性比取值方法更宽松;

(P170)

构造器是 “运行时” 用来初始化对象实例的方法。

(P171)

假如类没有显式定义的构造器, C# 编译器会在编译时自动添加一个。该构造器不获取参数,称为默认构造器。

一旦为类显式添加了构造器, C# 编译器就不再自动提供默认构造器。

C# 3.0 新增了对象初始化器,用于初始化对象中所有可以访问的字段和属性。

总之,构造器退出时,所有属性都应该初始化成合理的默认值。

(P172)

[规范]

  1. 要为所有属性提供有意义的默认值,确保默认值不会造成安全漏洞或造成代码效率大幅下降。对于自动实现的属性,要通过构造器设置默认值;

  2. 要允许以任意顺序设置属性,即使这会造成对象临时处于无效状态;

(P173)

[规范]

  1. 如果使用构造器参数来设置属性,构造器参数 (驼峰大小写风格) 要使用和属性 (Pascal 大小写风格) 相同的名称,区别仅仅是大小写风格;

  2. 要为构造器提供可选参数,或者提供便利的重载构造器,用有意义的默认值初始化属性;

  3. 要允许以任意顺序设置属性,即使这会造成对象临时处于无效状态;

(P177)

在 C# 中,与全局字段或函数等价的是静态字段或方法。

(P178)

实例字段,也就是非静态字段,可以在声明的同时进行初始化。静态字段也可以。

和实例字段不同,未初始化的静态字段将获得默认值 (0 、 null 、 false 等) ,即 default(T) 的结果,其中 T 是类型名。所以,即使没有显式赋值的静态字段也能被访问。

静态字段不从属于实例,而是从属于类。

(P180)

由于静态方法不通过实例引用,所以 this 关键字在静态方法中无效。

静态构造器不显式调用,而是 “运行时” 在首次访问类时自动调用静态构造器。

由于静态构造器不能显式调用,所以不允许任何参数。

(P181)

使用静态构造器将类中的静态数据初始化成特定的值,尤其是无法通过声明时的一次简单赋值来获得初始值的时候。

在静态构造器中进行的赋值,将优先于声明时的赋值,这和实例字段的情况一样。注意,没有 “静态终结器” 的说法。

[规范]

  1. 考虑以内联方式初始化静态字段,不要使用静态构造器或者在声明时赋值;

还可以将属性声明为 static 。

(P182)

使用静态属性几乎肯定要比使用公共静态字段好,因为公共静态字段在任何地方都能调用,而静态属性则至少提供了一定程度的封装。

(P183)

在声明类时使用 static 关键字,具有两个方面的意义。首先,它防止程序员写代码来实例化静态类;其次,它防止在类的内部声明任何实例字段或方法。

静态类的另一个特点是 C# 编译器自动在 CIL 代码中把它标记为 abstract 和 sealed 。这会将类指定为不可扩展;换言之,不能从它派生出其他类。

(P184)

如果扩展方法的签名已经和被扩展类型中的签名匹配,扩展方法永远不会得到调用,除非是作为一个普通的静态方法。

(P185)

扩展方法要慎用。

[规范]

  1. 避免轻率地定义扩展方法,尤其是要避免为自己没有所有权的类型定义扩展方法;

和 const 值一样, const 字段 (称为常量字段) 包含在编译时确定的值,它不可以在运行时改变。

常量字段自动成为静态字段,因为不需要为每个对象实例都生成新的字段实例。但是,将常量字段显式声明为 static 会造成编译错误。

[规范]

  1. 要为永远不变的值使用常量字段;

  2. 不要为将来会发生变化的值使用常量字段;

(P186)

和 const 不同, readonly 修饰符只能用于字段 (不能用于局部变量) 。

它指出字段值只能从构造器中更改,或者在声明时通过初始化器修改。

和 const 字段不一样,每个实例的 readonly 字段都可以不同。

由于 readonly 字段必须从构造器中设置,所以编译器要求这种字段能从其属性外部访问。

(P187)

将 readonly 应用于数组不会冻结数组的内容,而是冻结数组实例 (也冻结了数组中的元素数量) ,这是因为无法将值重新赋给新的实例。但数组中的元素仍然是可写的。

[规范]

  1. 在 C# 6.0 (及之后版本) 中,要优先使用只读的自动实现的属性,而不是定义只读字段;

  2. 在 C# 6.0 之前的版本中,要为预定义对象实例使用 public static readonly 字段;

  3. 如果 API 版本的兼容性有要求,要避免将 C# 6.0 之前版本中的公共的 readonly 字段修改为 C# 6.0 (及之后版本) 中的只读的自动实现的属性;

在类中除了定义方法和字段,还可以定义另一个类。这称为嵌套类 (nested class) 。假如一个类在它的包容类外部没有多大意义,就适合把它设计成嵌套类。

(P188)

嵌套类的独特之处是可以为类自身指定 private 访问修饰符。

(P189)

嵌套类中的 this 成员代表嵌套类而不是包容类的实例。嵌套类要想访问包容类的实例,一个办法是显式传递包容类的实例,比如通过构造器或者方法参数。

嵌套类的另一个有趣的特点是它能访问包容类的任何成员,其中包括私有成员。反之则不然,包容类不能访问嵌套类的私有成员。

嵌套类用得很少。要从包容类型外部引用,就不能定义成嵌套类。另外要警惕 public 嵌套类,它们意味着不良的编码风格,可能造成混淆和难以阅读。

[规范]

  1. 避免声明公共嵌套类型。唯一的例外是在这种类型的声明没有多大意义的时候,或者这种类型的声明是与一种高级的自定义场景有关;

分部类主要用于将一个类的定义划分到多个文件中。

分部类对代码生成或修改工具来说意义重大。

C# 2.0 (和更高版本) 使用 class 前的上下文关键字 partial 来声明分部类。

除了用于代码生成器,分部类另一个常见的应用是将每个嵌套类都放到它们自己的文件中。这是为了与编程规范 “将每个类定义都放到它自己的文件中” 保持一致。

(P190)

分部类不允许对编译好的类 (或其他程序集中的类) 进行扩展。分部类只是在同一个程序集中将一个类的实现拆分到多个文件中。

(P192)

分部方法必须返回 void 。

【第06章】

(P193)

派生类型总是隐式地属于基类型。

[注意]

  1. 代码中的继承用于定义 “属于” 关系,派生类是对基类的特化;

(P195)

每个派生类都拥有由其所有基类公开的全部成员。

[注意]

  1. 通过继承,基类的每个成员都会出现在派生类的链条中;

所有类都隐式地派生于 object ,不管是否这样指定。

[注意]

  1. 除非明确指定了基类,否则所有类都默认从 object 派生;

(P196)

从基类型转换为派生类型,要求执行显式转型,而显式转型在运行时可能会失败。

[注意]

  1. 派生对象可隐式转型为它的基类。相反,基类向派生类的转换要求显式的转型操作符,因为转换可能会失败。虽然编译器允许可能有效的显式转型,但 “运行时” 会坚持进行检查,如果在执行时出现非法的转型,会引发异常;

(P197)

派生类继承了除构造器和析构器之外的所有基类成员。但是,继承并不意味着一定能访问。

(P198)

根据封装原则,派生类不能访问基类的 private 成员。

[注意]

  1. 派生类不能访问基类的私有成员;

(P199)

[注意]

  1. 基类中的受保护成员只能从基类以及其派生链中的其他类访问;

基本规则是,要从派生类中访问受保护成员,必须在编译时确定是从派生类 (或者它的某个子类) 的实例中访问受保护成员。

由于每个派生类都可作为它的任何基类的实例使用,所以对一个类型进行扩展的方法也可扩展它的任何派生类型。

如果扩展基类,所有扩展方法在派生类中也可以使用。

很少为基类写扩展方法。扩展方法的一个基本原则是,假如手上有基类的代码,直接修改基类会更好。

(P201)

密封类要求使用 sealed 修饰符,这样做的结果是不能从它们派生出其他类。 string 类型就用 sealed 修饰符禁止了派生。

基类除构造器和析构器之外的所有成员都会在派生类中继承。

(P202)

C# 支持重写实例方法和属性,但不支持重写字段或者任何静态成员。

在基类中,必须将允许重写的每个成员标记为 virtual 。

默认情况下, Java 中的方法都是虚方法

推荐站点

  • 腾讯腾讯

    腾讯网(www.QQ.com)是中国浏览量最大的中文门户网站,是腾讯公司推出的集新闻信息、互动社区、娱乐产品和基础服务为一体的大型综合门户网站。腾讯网服务于全球华人用户,致力成为最具传播力和互动性,权威、主流、时尚的互联网媒体平台。通过强大的实时新闻和全面深入的信息资讯服务,为中国数以亿计的互联网用户提供富有创意的网上新生活。

    www.qq.com
  • 搜狐搜狐

    搜狐网是全球最大的中文门户网站,为用户提供24小时不间断的最新资讯,及搜索、邮件等网络服务。内容包括全球热点事件、突发新闻、时事评论、热播影视剧、体育赛事、行业动态、生活服务信息,以及论坛、博客、微博、我的搜狐等互动空间。

    www.sohu.com
  • 网易网易

    网易是中国领先的互联网技术公司,为用户提供免费邮箱、游戏、搜索引擎服务,开设新闻、娱乐、体育等30多个内容频道,及博客、视频、论坛等互动交流,网聚人的力量。

    www.163.com
  • 新浪新浪

    新浪网为全球用户24小时提供全面及时的中文资讯,内容覆盖国内外突发新闻事件、体坛赛事、娱乐时尚、产业资讯、实用信息等,设有新闻、体育、娱乐、财经、科技、房产、汽车等30多个内容频道,同时开设博客、视频、论坛等自由互动交流空间。

    www.sina.com.cn
  • 百度一下百度一下

    百度一下,你就知道

    www.baidu.com