欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

《框架设计 CLR Via C# (第2版)》 - 学习笔记

程序员文章站 2022-05-04 14:43:13
《框架设计 CLR Via C#》 (第2版) [作者] (美) Jeffrey Richter[译者] (中) 周靖 张杰良[出版] 清华大学出版社[版次] 2006年11月 第1版[印次] 2007年02月 第2次 印刷[定价] 68.00元 【前言】 Microsoft .NET Framew ......

《框架设计 clr via c#》 (第2版)

========== ========== ==========
[作者] (美) jeffrey richter
[译者] (中) 周靖 张杰良
[出版] 清华大学出版社
[版次] 2006年11月 第1版
[印次] 2007年02月 第2次 印刷
[定价] 68.00元
========== ========== ==========

【前言】

microsoft .net framework 的目标不是为构建一种特定类型的应用程序的开发人员提供一个抽象技术。相反,它的目标是为平台或者 microsoft windows 操作系统本身提供一个抽象技术。

.net framework 为所有类型的应用程序提升了抽象等级。这意味着开发人员只需学习和掌握一个编程模型和一套 api (应用程序编程接口) ,不管开发人员是用它们来构建控制台应用程序、图形应用程序、网站,还是构建由其他应用程序使用的组件。

【第01章】

(p011)

一个方法只有在首次调用时才会造成一定的性能损失。以后对该方法的所有调用都以本地代码的形式全速运行,因为不需要再次执行验证和编译成本地代码。

【第02章】

(p039)

程序集是进行重用、版本控制和安全保护的一个基本单元。它允许我们将类型和资源文件划分到单独的文件中。

(p047)

在 visual studio 中新建一个 c# 项目时,会自动创建一个 assemblyinfo.cs 文件。

(p051)

配置文件包含的是 xml 代码,可以和一个应用程序关联到一起,或者和机器关联到一起。由于使用的不是注册表设置,而是一个单独的文件,所以文件能够方便地进行备份,管理员也能将应用程序方便地复制到另一台机器 —— 只需复制必要的文件,就能顺便复制管理策略。

(p052)

对于可执行应用程序 (exe) 来说,配置文件必须在应用程序的基目录中,而且必须采用 exe 文件的全名作为文件名,再附加一个 .config 扩展名。

对于 microsoft asp.net web 窗体应用程序,文件必须在 web 应用程序的虚构根目录中,而且总是命名为 web.config 。除此之外,子目录也可以包含它们自己的 web.config 文件,而且配置设置会得以继承。

【第03章】

(p062)

不能将一个弱命名的程序集放到 gac 中。

建议编程人员尽量避免全局部署,尽量使用私有部署。

【第04章】

(p083)

c# 不需要任何特殊语法即可将一个对象强制转换成它的任何基类型,因为向基类型的转换被认为是一种安全的隐式转换。

c# 要求开发人员将一个对象显式转换成它的任何派生类型,因为这样的转型可能在运行时失败。

(p084)

类型伪装是造成许多安全漏洞的根源,并会破坏应用程序的稳定性和可靠性。因此,类型安全性是 clr 的一个极其重要的目标。

在 c# 语言中进行强制类型转换的另一种方式是使用 is 操作符。 is 操作符检查一个对象是否兼容于指定的类型,并返回一个 boolean 值 : true 或 false 。注意 is 操作符永远不会抛出异常。

(p085)

假如对象引用为 null ,那么 is 操作符总是返回 false ,因为无可用的对象来检查其类型。

as 操作符的工作方式与强制类型转换一样,只是它永远不会抛出一个异常 —— 相反,如果对象不能转型,结果就是 null 。所以,正确的做法是检查最终生成的引用是否为 null 。如果企图直接使用最终生成的引用,会造成一个 system.nullreferenceexception 异常。

(p089)

事实上, .net framework 甚至根本没有发布一个 system.io.dll 程序集。

【第05章】

(p098)

编译器直接支持的任何数据类型都称为基元类型 (primitive type) 。基元类型直接映射到 framework 类库 (fcl) 中存在的类型。

(p101)

c# 总是对结果进行截断处理,而不进行舍入。

(p104)

值类型的实例通常是在一个线程的堆栈上分配的 (虽然它们也可以嵌入一个引用类型对象中) 。

在代表值类型实例的一个变量中,并不包含一个指向实例的指针。相反,变量中包含了实例本身的字段。

由于变量已经包含实例的字段,所以在对实例的字段进行处理时,不再需要提领一个指针。

值类型的实例不受垃圾收集器的制约。因此,它们的使用缓解了托管堆上的压力,并减少了一个应用程序在其生存期内需要进行的垃圾收集次数。

所有值类型都是密封 (sealed) 类型,目的是防止将一个值类型用作其他任何引用类型或值类型的基类型。

(p105)

在 c# 中,使用 struct 声明的类型是值类型,使用 class 声明的类型是引用类型。

(p106)

值类型对象有两种表示形式 : 未装箱 (unboxed) 形式和已装箱 (boxed) 形式,而引用类型总是处于已装箱形式。

引用类型的变量包含堆中的对象的地址,默认情况下,在创建一个引用类型的变量时,它被初始化为 null ,表明引用类型变量当前并不指向一个有效的对象。

试图使用一个 null 引用类型变量,会抛出一个 nullreferenceexception 异常。

相反,值类型的变量总是包含其基础类型的一个值,而且值类型的所有成员都初始化为 0 。由于值类型变量不是指针,所以在访问一个值类型时,不可能抛出一个 nullreferenceexception 异常。

(p107)

将一个值类型的变量赋给另一个值类型变量时,会执行一次逐字段的复制。将引用类型的变量赋给另一个引用类型的变量时,只复制内存地址。

值类型的变量是自成一体的对象,对一个值类型变量执行的操作不可能影响另一个值类型变量。

(p109)

为了将一个值类型转换成一个引用类型,可以使用一个名为 “装箱” (boxing) 的机制。下面总结了对一个值类型的实例进行装箱操作时在内部发生的事情 :

1. 从托管堆中分配好内存。分配的内存量是值类型的各个字段所需要的内存量加上托管堆上的所有对象都有的两个额外成员 (即类型对象指针和同步块索引) 所需要的内存量;

2. 值类型的字段复制到新分配的堆内存;

3. 返回对象的地址。现在,这个地址是对一个对象的引用,值类型现在是一个引用类型;

(p110)

拆箱其实就是获取一个指针的过程,该指针指向包含在一个对象中的原始值类型 (数据字段) 。事实上,指针指向的是已装箱实例中的未装箱部分。

在对一个对象进行拆箱操作时候,只能将其转型为未装箱时的值类型。

(p115)

调用一个方法时,假如它没有为传给它的一种特定的值类型准备一个重载版本,那么最终肯定会调用接受一个 object 参数的重载版本。将一个值类型实例作为一个 object 来传递,会造成装箱操作的发生,从而对性能产生不利影响。

未装箱的值类型是比引用类型更为 “轻型” 的类型。这要归究于以下两个原因 :

1. 它们不在托管堆上分配;

2. 它们没有堆上的每个对象都有的额外成员,也就是一个类型对象指针和一个同步块索引;

由于未装箱的值类型没有同步块索引,所以不能使用 system.threading.monitor 类型的各种方法 (或者使用 c# 的 lock 语句) 让多个线程同步访问这个实例。

(p121)

object 的 equals 方法实现的只是 “同一性” (identity) ,而不是 “相等性” (equality) 。

(p122)

如果想要检查同一性 (看两个引用是否指向同一个对象) ,那么务必调用 referenceequals ,而不应使用 c# 的 == 操作符 (除非先把两个操作数都转型为 object) ,因为其中某个操作数的类型可能重载了 == 操作符,为其赋予有别于 “同一性” 的其他语义。

由于 clr 的反射机制较慢,所以在定义自己的值类型时,应该重写 equals 方法,并提供自己的实现,以便在用类型的实例进行值相等性比较时提高性能。

【第06章】

(p130)

定义类型时,如果没有显式地指定类型的可见性, c# 编译器会将类型的可见性设为 internal (两者之中约束性比较强的一个) 。

(p131)

定义类型 (包括嵌套类型) 的成员时,可以指定成员的可访问性。成员的可访问性表明目标代码可以合法访问哪些成员。

(p132)

在 c# 中,如果没有显式地声明成员的可访问性,那么,编译器通常 (并不总是) 将成员的可访问性默认设为 private (可访问性修饰符中约束性最强的一个) 。

(p133)

类不能将基类方法的可访问性设置得更严格,因为派生类的用户通常可以强制转换基础类型来获得对基类方法的访问。

关键字 static 仅可以用于类,而不能用于结构 (值类型) ,这是因为 clr 要求值类型必须实例化,并且没有方法停止或阻止该实例化过程。

(p134)

通过使用关键字 static 定义的类将导致 c# 编译器将该类同时标记为 abstract 和 sealed 。

(p137)

属性和事件实际上是作为方法实现的。

(p142)

暴露状态极容易产生问题,它使对象的行为无法预测,而且还公开潜在的安全漏洞。

在类的内部,始终将自己的方法、属性和事件定义为 private 和非虚拟的。

【第07章】

(p147)

常量总是被当作静态成员,而不是实例成员。

(p149)

只读字段只能在构造器方法中写入数值 (称之为一次写,即在对象首次创建时写入数值) ,编译器和验证机制确保只读字段不能被构造器外的任何其他方法写入。需要注意的是,可以采用反射 (reflection) 来修改 readonly 字段。

(p150)

当某个字段是引用类型,并且该字段标记为 readonly 时,它就是不可改变的引用,而不是字段所引用的对象。

【第08章】

(p151)

构造器是允许将类型实例初始化为有效状态的特殊方法。

创建引用类型的实例时,首先为实例的数据字段分配内存,接着初始化对象的系统开销字段 (类型对象指针和同步块索引) ,最后调用类型的实例构造器设置对象的初始状态。

创建引用类型对象时,在调用类型的实例构造器之前,为对象分配的内存始终被清零。构造器没有显式赋值的所有字段保证都有一个 0 或者 null 值。

如果定义的类中没有显式地定义任何构造器,那么,许多编译器 (包括 c# 编译器) 将定义一个默认的 (无参数的) 构造器,该构造器的实现只是调用基类的无参构造器 (parameterless constructor) 。

(p152)

如果类的修饰符为 abstract ,那么编译器生成的默认构造器的可访问性为 protected ;否则,构造器的可访问性为 public 。

如果基类没有提供无参构造器,那么,派生类必须显式地调用基类的构造器,否则编译器会报错。

如果类的修饰符为 static (sealed 和 abstract) ,那么,编译器就根本不会在类的定义中生成一个默认的构造器。

一个类型可以定义多个实例构造器。每个构造器都必须拥有一个不同的签名,而且每个构造器可以拥有不同的可访问性。

对于可验证的代码 (verifiable code) ,类的实例构造器在访问从基类继承的任何字段之前,必须调用其基类的构造器。

最终,类的实例构造器将调用基类 system.object 的公有无参构造器。该构造器不执行任何代码,只是简单地返回,因为基类 system.object 没有定义实例数据字段,因此它的构造器没有代码可以执行。

c# 语言提供了一个简单的语法,允许在构建类型实例的过程中初始化引用类型中定义的字段。

c# 编译器提供了一个方便的语法来内联初始化实例字段,并且将这个语法转换成构造器方法中的代码以执行初始化。

(p154)

clr 确实允许在值类型上定义构造器,但是执行值类型上定义的构造器的惟一方法是编写代码显式地调用这些构造器。

(p155)

值类型的实例构造器只有在被显式调用时才会执行。

(p156)

除了实例构造器外, clr 还支持类型构造器 (type constructor) ,也称为静态构造器 (static constructor) 、类构造器 (class constructor) 或者类型初始化器 (type initializer) 。

和实例构造器用来设置类型的实例的初始状态一样,类型构造器用来设置类型的初始状态。

默认情况下,类型不在类型内部定义类型构造器。如果类型定义了类型构造器,那么类型构造器的数量不能超过一个。另外,类型构造器永远没有参数。

(p157)

定义类型构造器的方法类似于定义无参实例构造器,惟一的区别在于必须将类型构造器标记为 static 。

类型构造器通常也应是私有的, c# 会自动地将类型构造器标记为 private 。

因为 clr 保证每个应用程序域的类型构造器只执行一次,而且是线程安全的,所以最后合在类型构造器中初始化类型的单实例对象。

(p158)

如果类型构造器抛出一个未处理的异常, clr 就会认为类型不可使用。试图访问该类型的任何字段或者方法都将导致 clr 抛出一个 system.typeinitializationexception 异常。

类型构造器中的代码只能访问类型的静态字段,并且它的常规用途就是初始化这些静态字段。就像对待实例字段一样, c# 提供了一个简单的语法来初始化类型的静态字段。

虽然 c# 不允许值类型使用内联字段初始化语法初始化实例字段,但允许用它来初始化静态字段。

(p162)

clr 规范将操作符重载方法指定为 public 和 static 方法。

(p166)

在关键字 implicit 或关键字 explicit 之后,可以指定关键字 operator 向编译器表明该方法是一个转换操作符方法。在关键字 operator 后面,还需要指定对象要强制转换成什么类型,而在圆括号中,需要指定要进行强制转换的对象的类型。

(p167)

默认情况下, clr 假定所有方法参数是按值传递的。

在方法中,必须知道传递的每个参数是引用类型还是值类型,因为编写的用来处理参数的代码会因此存在明显的差异。

从 clr 的角度看,关键字 out 和关键字 ref 是等效的,这就是说,无论使用哪个关键字,都会生成相同的元数据和 il 代码。

(p168)

从 il 和 clr 的角度看, out 和 ref 功能相同 —— 它们都生成一个所传递实例的指针;这两个关键字的区别在于需要编译器进一步保证代码的正确性。

(p170)

按引用传递给方法的变量的类型必须与方法签名中声明的类型相同。

(p172)

关键字 params 是应用于方法签名的最后一个参数。

关键字 params 向编译器表明将 system.paramarrayattribute 实例的自定义属性应用到参数上。

(p173)

只有方法的最后一个参数才可以标记关键字 params (paramarrayattribute) 。该参数必须标识一个一维数组,但类型不限。对方法的最后一个参数传递 null 或者 0 个条目的数组的引用都是合法的。

【第09章】

(p178)

clr 支持静态属性、实例属性、抽象属性和虚拟属性。

(p181)

在 c# 中,使用与数组类似的语法对外提供有参属性 (索引器) 。换句话说,我们可以将索引器看作 c# 开发人员重载运算符 [] 的一种方式。

(p182)

所有的索引器必须至少拥有一个参数,也可以拥有多个参数。这些参数以及返回类型可以是任意的数据类型 (除了 void) 。

和无参属性的 set 访问器方法相似,索引器的 set 访问器方法同样也包含了一个隐藏的参数,在 c# 中称之为 value 。该参数表明 “索引元素 (indexed element)” 期望的新值。

clr 本身并不区别无参属性和有参属性,对 clr 来讲,每个属性只是定义在类型中的一对方法和一块元数据。

(p183)

在 c# 中,每个类型都可以定义多个索引器,只要索引器的参数集不同即可。

(p185)

当定义有访问器方法的属性拥有不同的可访问性时,c# 语法要求属性本身必须声明为最低约束性的可访问性,而且约束性较强的可访问性只可以应用于一个访问器方法。

【第10章】

(p186)

公共语言运行库 (common language runtime , clr) 的事件模型建立在委托 (delegate) 这一机制之上。委托是一种类型安全的调用回调方法 (callback method) 的方式。回调方法意味着哪个对象接收对象所订阅事件的通知。

(p187)

按照约定,所有传递给事件处理程序的用于存放事件信息的类都应该继承自 system.eventargs ,并且类的名称应该以 eventargs 结束。

(p188)

定义一个不需要传递任何额外信息的事件时,可以直接使用 eventargs.empty ,不用构建一个新的 eventargs 对象。

事件成员使用 c# 关键字 event 定义。每个事件成员都有一个给定的可访问性 (通常都为 public ,以便于其他代码也可以访问这个事件成员) 、一个表示即将被调用方法的原型的委托类型以及一个名称 (可以是任意有效的标识符) 。

(p189)

事件模式要求所有的事件处理程序的返回类型都为 void 。

按照约定,类应定义一个受保护的虚方法,当引发事件时,这个类及其派生类中的代码可以调用这个虚方法。

(p190)

设计一个对外提供事件的类型 :

第一步 : 定义一个类型用于存放所有需要发送给事件通知接收者的附加信息;

第二步 : 定义事件成员;

第三步 : 定义一个负责引发事件的方法,来通知已订阅事件的对象事件已经发生;

第四步 : 定义一个方法,将输入转化为期望事件;

(p193)

c# 要求代码使用 += 和 -= 操作符在链表上添加和移除委托。

(p196)

事件必须同时拥有 add 和 remove 访问器方法。

【第11章】

(p201)

在 .net framework 中,字符总是表示成 16 位 unicode 代码值,这简化了全球应用程序的开发。一个字符表示成 system.char 结构 (一个值类型) 的一个实例。

(p202)

可以使用三种技术实现各种数值类型与 char 实例的相互转换 : 转型 (强制类型转换) 、使用 convert 类型、 使用 iconvertible 接口。

(p204)

system.string 是任何一个应用程序使用得最多的类型之一。

一个 string 代表一个不可变的顺序字符集。

string 类型直接派生自 object ,这使其成为一个引用类型。因此,string 对象 (它的字符数组) 总是存在于堆上,而不在线程的堆栈上。

在 c# 中,不能通过 new 操作符在一个直接量字符串的基础上构造一个 string 对象。

公共语言运行库 (clr) 事实上采取一种特殊的方式来构造直接量 string 对象。

(p205)

在字符串之前添加 @ 符号,使编译器知道字符串是一个逐字字符串。

(p206)

要想高效地执行大量字符串操作,请使用 stringbuilder 类。

string 类必须是密封类 (sealed) 。

(p217)

用 stringbuilder 对象构造好字符串之后,为了将 stringbuilder 的字符数组 “转换” 成一个 string ,只需调用 stringbuilder 的 tostring 方法。在内部,该方法只是返回对 stringbuilder 内部维护的字符串字段的一个引用。这使 stringbuilder 的 tostring 方法可以非常快地执行,因为字符数组不需要复制。

(p218)

数组的动态扩容会损害性能。要避免这个危害,需要设置一个合适的初始容量。

stringbuilder 只在以下两种情况下分配一个新的对象 :

1. 试图动态构造一个字符串,它的长度超过了事先设置的 “容量” ;

2. 试图在调用 stringbuilder 的 tostring 方法之后修改数组;

(p220)

system.object 定义了一个 public 、 virtual 、无参数的 tostring 方法,所以在任何类型的一个实例上都能调用这个方法。

tostring 的 system.object 实现的是返回对象所属类型的全名。

(p222)

iformatprovider 接口的基本思路是 : 假如一个类型实现了该接口,就认为类型的一个实例能够提供依赖于语言文化的格式化信息,而与调用线程关联的语言文化应被忽略。

(p224)

在内部, format 方法会调用每个对象的 tostring 方法来获取对象的一个字符串表示。

采取在大括号中指定格式信息的方式,可以对一个对象的格式化进行更多的控制。

(p227)

能解析一个字符串的任何类型都提供了一个名为 parse 的 public static 方法。该方法获取一个 string 对象,并返回类型的一个实例。从某个角度来说, parse 相当于一个 factory 方法。

(p229)

在 clr 中,所有字符都是以 16 位 unicode 代码值的形式来表示的,而且所有字符串都由 16 位 unicode 代码值构成。

(p231)

encoding 是一个抽象基类,它提供了几个静态只读属性,每个属性都返回从 encoding 派生的一个类的实例。

(p232)

一旦获得从 encoding 派生的一个对象之后,就可以调用 getbytes 方法,将一个字符串或者一个字符数组转换成一个字节数组 (该方法有几个重载版本) 。要将字节数组转换成字符数组,需要调用 getchars 方法或者更有用的 getstring 方法 (这两个方法都有几个重载版本) 。

【第12章】

(p240)

枚举类型不能定义任何方法、属性或事件。

枚举类型只是一个在其中定义一系列常量字段和实例字段的结构。

【第13章】

(p247)

所有数组类型都隐式地从 system.array 抽象类派生,后者又派生自 system.object 。这意味着数组始终为引用类型,是在托管堆上进行分配的,应用程序的变量或字段中包含的是对数组的引用,而不是对数组本身所含元素的引用。

【第14章】

(p262)

在 clr 中,类始终继承自一个而且只有一个类 (最终肯定继承自 object) 。

clr 还允许开发人员定义一个接口,接口实际只是为一组方法签名指定一个名称的方式。

类继承的一个重要特性是,在希望出现基类型实例的任何地方,都可以替换成派生类的实例。

接口继承允许在希望出现已命名接口类型的实例的任何地方,都可以替换成实现接口的一个类型的实现。

接口是一组已命名的方法签名。

接口还可以定义事件、无参数的属性和参数化的属性 (在 c# 中是索引器) ,因为无论怎样,所有这些在本质上都是方法。

一个接口不能定义任何构造器方法。

接口也不允许定义任何实例字段。

(p263)

在定义接口类型时,可以随心所欲地指定 可视性 / 可访问性 (public , protected , internal 等) 。

(p264)

c# 编译器要求将实现了接口的方法标记为 public 。 clr 要求将接口方法标记为 virtual 。

(p267)

注意,用 c# 定义一个显式接口方法时,不允许指定访问性 (比如公共或私有) 。但是,在编译器生成方法的元数据时,其访问性被设置为私有,目的是防止使用类实例的任何代码直接调用接口方法。要想调用接口方法,只能通过一个接口类型的变量来进行。

泛型接口提供了出色的编译时类型安全性。

(p268)

泛型接口的第二个好处是在操作值类型时,不需要太多装箱操作。

有的泛型接口继承了非泛型版本,所以我们所写的类必须实现接口的泛型和非泛型版本。

泛型接口的第三个好处是,类可以实现同一个接口若干次,只要使用不同的类型参数。

【第15章】

(p280)

对于一个通过委托来调用另一个类型的私有成员的类型而言,这样做不会损害其安全性或可访问性,只要这个委托对象是由具有足够安全性或可访问性的代码来创建的。

将一个方法绑定到一个委托时, c# 和 clr 都允许引用类型的协变 (covariance) 和反协变 (contra-variance) 。协变指的是一个方法能返回从委托的返回类型派生的一个类型。反协变指的是一个方法的参数类型可以是委托的参数类型的基类。

注意,协变与反协变只能用于引用类型,不能用于值类型或 void 。

(p282)

所有委托类型都继承自 multicastdelegate 。

system.multicastdelegate 类继承自 system.delegate ,后者本身继承自 system.object 。

委托类可以在一个类型内部 (即嵌套在另一个类型内) 或在全局范围内定义。简单地说,因为委托是类,在可以定义类的任何地方,都可以定义委托。

(p285)

链式委托指的是由一系列委托对象组成的集合,它允许调用集合中各个委托所表示的所有方法。

delegate 类的公共静态方法 combine 用于添加一个委托到委托链。

(p288)

c# 编译器自动为委托类型的实例提供了运算符 += 和 -= 重载。这些运算符分别调用 delegate.combine 和 delegate.remove 。

(p292)

当 c# 编译器看到期望收到委托对象引用的地方使用了 delegate 关键字,就会自动在类中定义一个新的私有方法。这个新方法叫做匿名方法 (anonymous method) ,因为编译器自动为我们创建了方法名,而且通常情况下,我们并不需要知道这个名称。

(p294)

如果回调代码引用了任何一个参数,在 delegate 关键字后面必须包含括号、参数类型和变量名称。返回类型仍然可以从委托的类型推断出来,而且如果返回类型不为 void ,还必须在内联的回调代码内部包含一个 return 语句。

【第16章】

(p301)

定义一个泛型类型或方法时,它为类型指定的任何变量 (比如 t) 都称为 “类型参数” (type parameter) 。 t 是一个变量名,在源代码中能够使用一个数据类型的任何位置,都能使用 t 。

由于在能够指定一个数据类型的任何地方使用 t 变量,所以在方法内部定义一个局部变量时,或者在一个类型中定义字段时,也可以使用 t 。

使用一个泛型类型或者方法时,指定的具体数据类型被称为 “类型实参” (type argument) 。

(p307)

具有泛型类型参数的一个类型被称为 “开放式类型” (open type) , clr 禁止构造开放式类型的任何实例。

(p311)

没有泛型接口,每次试图使用一个非泛型接口来操纵一个值类型时,都会进行装箱,而且会丢失编译时的类型安全性。这会严重限制泛型类型的应用。

(p312)

clr 支持泛型委托,目的是保证任何类型的对象都能以一种类型安全的方式传给一个回调方法。

(p313)

定义一个泛型引用类型、值类型或者接口时,这些类型中定义的任何方法都可以引用类型指定的一个类型参数。类型参数可以作为方法的参数,作为方法的返回值,或者作为方法内部定义的一个局部变量来使用。然而, clr 还允许一个方法指定它独有的类型参数。这些类型参数可用于参数、返回值或者局部变量。

(p317)

类型参数可以指定零个或者一个主要约束 (primary constraint) 。主要约束可以是一个引用类型,它标识了一个没有密封的类。

指定一个引用类型约束时,相当于向编译器承诺 : 一个指定的类型实参要么是与约束类型相同的类型,要么是从约束类型派生的一个类型。

(p318)

class 约束向编译器承诺一个指定的类型实参是引用类型。任何类类型、接口类型、委托类型或者数组类型都满足这个约束。

struct 约束向编译器承诺一个指定的类型实参是值类型。包括枚举在内的任何值类型都满足这个约束。

一个类型参数可以指定零个或者多个次要约束,次要约束代表的是一个接口约束。指定一个接口类型约束时,是向编译器承诺一个指定的类型实参是实现了接口的一个类型。由于能指定多个接口约束,所以为类型实参指定的类型必须实现所有接口约束 (以及所有主要约束,如果指定了的话) 。

(p319)

一个类型参数可以指定零个或者一个构造器约束。指定构造器约束相当于向编译器承诺 : 一个指定的类型实参是实现了一个 public 无参数构造器的一个非抽象类型。注意,如果同时指定了构造器约束和 struct 约束, c# 编译器会认为这是一个错误,因为这是多余的;所有值类型都隐式提供了一个 public 无参数构造器。

(p320)

将一个泛型类型变量转型为另一个类型是非法的,除非将其转换为与一个约束兼容的类型。

将泛型类型变量设为 null 是非法的,除非将泛型类型约束成一个引用类型。

(p321)

microsoft 的 c# 团队认为有必要允许开发人员将一个变量设为一个默认值。所以, c# 编译器允许使用 default 关键字来实现这个操作。

【第17章】

(p323)

声明式编程是指使用数据而不是写源代码来指示 应用程序 / 组件去做某事。

“自定义特性” 是一种特殊的技术,它允许和命令式编程 (c# 源代码) 配合使用声明式编程。这种组合式编程为编程人员提供了极大的灵活性,并允许以一种非常简洁的方式表达开发人员的编码意图。

(p326)

为了保持与 “公共语言规范” (cls) 相容,自定义特性类必须直接或间接地从公共抽象类 system.attribute 派生。

可以将多个特性应用于单个目标元素。

将多个特性应用于单个目标元素时,注意特性的顺序是无关紧要的。

在 c# 中,可以将每个特性都封闭到一对方括号中,也可以在一对方括号中封闭多个以逗号分隔的特性。

假如特性类的构造器不获取参数,圆括号就是可有可无的。

【第18章】

(p344)

c# 提供了一个所谓的 “空结合操作符 (null-coalescing operator)” ,即 “??” 操作符,它要获取两个操作数。假如左边的操作数不为 null ,就返回这个操作数的值。如果左边的操作数为 null ,就返回右边的操作数的值。利用空结合操作符,可以方便地设置变量的默认值。

空结合操作符的一个妙处在于,它既能用于引用类型,也能用于可空值类型。

【第19章】

(p352)

try 块中包含的代码通常要求执行公共资源清理操作,或者要求执行异常恢复操作,或者两种操作都要求。

资源清理代码应放在一个单独的 finally 块中。

try 块同样可以包含可能会抛出异常的代码。

异常恢复代码应该放在一个或多个 catch 块中。

我们应该为应用程序可以从中恢复的每一种异常都创建一个 catch 块。

一个 try 块必须至少有一个 catch 块或者 finally 块与其关联,单独一个 try 块是没有任何意义的,而且 c# 也会阻止我们这样做。

catch 块中包含的是响应异常时需要执行的代码。一个 try 块可以有 0 个或者多个 catch 块与其关联。如果 try 块中的代码没有抛出异常,那么 clr 永远不会执行与该 try 块相关联的所有 catch 块中的代码。这时,线程就会跳出所有的 catch 块,直接执行 finally 块中的代码 (如果存在的话)。 在 finally 块中的代码执行完毕后,线程就会继续执行 finally 块后面的语句。

(p354)

一个 try 块并不要求必须有一个 finally 块与其关联,有时候 try 块中的代码并不需要任何清理工作。但是,如果确实有 finally 块,那么它必须出现在所有的 catch 块之后。而且,一个 try 块最多只能有一个与其关联的 finally 块。

当线程执行完 finally 块中包含的代码后,线程立即开始执行紧跟在 finally 块后的语句。记住, finally 块中的代码是清理代码,该代码只有在 try 块中发起的操作需要进行清理时才被执行。应该避免将可能抛出异常的代码放在 finally 块中。

(p376)

system.exception 类型提供了一个公共的只读属性 stacktrace 。 catch 块可以读取该属性来获取异常的堆栈跟踪,异常的堆栈跟踪指出了异常经过的路径中所发生的事件,它对于我们检测异常原因、进而修正代码来说非常有用。

【第20章】

(p397)

finalize 方法在垃圾收集结束时被调用,有 5 种事件会导致一个对象的 finalize 方法被调用 :

1. 第 0 代对象充满;

2. 代码显式地调用 system.gc 的静态方法 collect ;

3. windows 报告内存不足;

4. clr 卸载应用程序域;

5. clr 被关闭;

(p399)

一个对象要成为可终结的对象,那么在它的类型及其基础类型中 (除 object 之外) ,必须至少有一个类型重写了 object 的 finalize 。

(p401)

finalize 方法非常有用,因为它可以确保托管对象在释放内存的同时不会泄露本地资源。但是 finalize 方法的问题在于我们并不能确定它会在何时被调用,而且由于它并不是一个公共方法,我们也不能显式地调用它。

要提供确定释放或者关闭对象的能力,一个类型通常要实现一种释放模式 (dispose pattern) 。释放模式定义了开发人员在实现类型的显式资源清理功能时要遵循的一些约定。如果一个类型实现了释放模式,那么使用该类型的开发人员将能够知道当对象不再被使用时如何显式地释放掉它所占用的资源。

(p406)

记住 close 方法并不是释放模式正式定义的一部分,有些类型提供了 close 方法,而有些类型则不会提供 close 方法。

(p409)

filestream 类型只支持字节的读写操作。如果我们希望支持字符或者字符串的读写操作,可以使用 system.io.binarywriter 类型。

注意 streamwriter 的构造器接受一个 stream 对象的引用作为参数,允许 filestream 对象的引用作为参数进行传递。 binarywriter 对象内部会保存 stream 对象的引用。当我们向一个 streamwriter 对象写入数据时,它会将数据缓存在自己的内存缓冲区中。当 streamwriter 对象的内存缓冲区充满时, streamwriter 对象才会将数据写入 stream 对象。

【第21章】

(p438)

clr 初始化时创建的第一个应用程序域称为默认程序域 (default appdomain) ,该应用程序域只有在进程终止时才会被销毁。

(p443)

在 windows 中,线程通常在进程的上下文中创建,而且线程的生存期与该进程的生存期相同。但是线程和应用程序域之间没有一对一的关系。

【第22章】

(p466)

gettype 方法在运行时返回对象的类型 (晚绑定) ;

typeof 方法返回指定类的类型 (早绑定) ;

(p476)

调用 getmembers 方法返回的数组中的每一个元素都是前面反射类型层次结构中的一个具体类型 (除非指定了 bindingflags.declaredonly 标记) 。尽管 type 的 getmembers 方法可以返回所有类型的成员,但 type 还提供有一些方法可以返回特定的成员类型,例如 getnestedtypes , getfields , getconstructors , getmethods , getproperties 以及 getevents 方法。这些方法返回的都是一个数组,其元素分别为下述对象的引用 : type 对象、 fieldinfo 对象、 constructorinfo 对象、 methodinfo 对象、 propertyinfo 对象以及 eventinfo 对象。

【第24章】

(p511)

除了缩短并简化了代码外, lock 语句还可以保证对 monitor.exit 方法的调用,因此确保了即使在 lock 块内发生异常时也可以释放同步块。

(p535)

永远不要为 monitor.enter 方法或者 c# 的 lock 语句传递值类型的变量。因为未装箱值类型实例没有同步块索引成员,因此它们不能用于同步。