**《深入理解 C#》 (第2版)

========== ========== ========== [作者] (英) Jon Skeet [译者] (中) 周靖 朱永光 姚琪琳 [出版] 人民邮电出版社 [版次] 2012年01月 第1版 [印次] 2012年01月 第1次 印刷 [定价] 79.00元 ========== ========== ==========

【关于本书】

具体地说, C# 作为一种语言,它的基础是各种各样的 “框架库” (.NET Framework 中的各种库) 以及一个强大的运行时 (runtime) 。借助它们,我们可以将抽象的东西转变成现实。

【第01章】

(P013)

LINQ (Language Integrated Query , 语言集成查询) ,是 C# 3 的核心之所在。顾名思义。 LINQ 是关于查询的,其目的是使用一致的语法和特性,以一种易阅读、可组合的方式,使对多数据源的查询变得简单。

(P014)

查询表达式唯一的好处就是 where 子句显得更简单。

【第02章】

(P023)

实际上,委托在某种程度上提供了间接的方法。换言之,不需要直接指定一个行为,而是将这个行为用某种方式 “包含” 在一个对象中。

可以选择将委托类型看做只定义了一个方法的接口,将委托的实例看做实现了那个接口的一个对象。

(P024)

为了让委托做某事,必须满足4个条件 :

  1. 声明委托类型;

  2. 必须有一个方法包含了要执行的代码;

  3. 必须创建一个委托实例;

  4. 必须调用 (invoke) 委托实例;

(P026)

如果有一个委托类型的变量,就可以把它视为方法本身。

(P028)

委托实例实际有一个操作列表与之关联,这称为委托实例的调用列表 (invocation list) 。

System.Delegate 类型的静态方法 Combine 和 Remove 负责创建新的委托实例。其中, Combine 负责将两个委托实例的调用列表连接到一起,而 Remove 负责从一个委托实例中删除另一个的调用列表。

委托是不易变的,创建了一个委托实例后,有关它的一切就不能改变。这样一来,就可以安全地传递委托实例,并把它们与其他委托实例合并,同时不必担心一致性、线程安全性或者是否有其他人试图更改它的操作。

(P029)

事件不是委托类型的字段。

对于事件来说,必须是一个委托类型。

(P030)

对于一个纯粹的事件,你所能做的事情就是订阅 (添加一个事件处理程序) 或者取消订阅 (删除一个事件处理程序) 。

类内的代码能看见字段;类外的代码只能看见事件。

(P037)

数组类型是引用类型,即使元素类型是值类型。

枚举是值类型。

委托类型是引用类型。

接口类型是引用类型,但可由值类型实现。

(P039)

变量的值是在它声明的位置存储的。

只有局部变量 (方法内部声明的变量) 和方法参数在栈上。

(P040)

当你调用类型变量值的 GetType() 方法时总是伴随着装箱过程,因为它不能被重载。

(P041)

引用类型作为方法参数使用时,参数默认是以 “值传递” 方式来传递的 —— 但值本身是一个引用。

【第03章】

(P047)

所谓 “函数化” 的编程风格,是指鼓励开发者更多地利用委托。

(P048)

从根本上说,泛型实现了类型和方法的 “参数化” ,就像在普通的方法调用中,经常要用参数来告诉它们使用什么值。

(P052)

类型参数是真实类型的占位符。

在泛型声明中,类型参数要放在一对尖括号内,并以逗号分隔。

使用泛型类型或方法时,要用真实的类型代替,这些真实的类型称为类型实参 (type argument) 。

如果没有为任何类型参数提供类型实参,声明的就是一个未绑定泛型类型 (unbound generic type) 。如果指定了类型实参,该类型就称为一个已构造类型 (constructed type) 。

我们知道,类型 (无论是否是泛型) 可以看做是对象的蓝图。同样,未绑定泛型类型是已构造类型的蓝图。

在 C# 代码中,唯一能看见未绑定泛型类型的地方 (除了作为声明之外) 就是在 typeof 操作符内。

类型参数 “接收” 信息,类型实参 “提供” 信息,这个思路与 方法参数 / 方法实参 是一样的。只不过类型实参必须为类型,而不能为任意的值。只有在编译时才能知道类型实参的类型,它可以是 (或包含) 相关上下文中的类型参数。

(P054)

泛型类型可以重载,只需改变一下类型参数的数量就可以了。

(P057)

将 API 的一部分变成泛型后,以前强类型的方法调用就需要进行强制类型转换。

(P062)

调用泛型方法时,指定类型实参常常会显得很多余。

类型推断只适用于泛型方法,不适用于泛型类型。

(P063)

在 C# 语言规范中,只提供了数量有限的推断步骤。

虽然很少需要用到默认值,但它偶尔还是有用的。

(P064)

输出参数也称为 out 参数,如果希望由方法本身初始化参数,允许向方法传递一个未初始化的实参,那么在声明参数时,就要附加 out 关键字作为前缀。

(P066)

共有 4 个主要的泛型接口可用于比较。 IComparer<T> 和 IComparable<T> 用于排序 (判断某个值是小于、等于还是大于另一个值) ,而 IEqualityComparer<T> 和 IEquatable<T> 通过某种标准来比较两个项的相等性,或查找某个项的散列 (通过与相等性概念匹配的方式) 。

如果换一种方式来划分这 4 个接口, IComparer<T> 和 IEqualityComparer<T> 用于那些能够比较两个不同值的类型,而 IComparable<T> 和 IEquatable<T> 的实例则用于它们本身和其他值之间的比较。

(P073)

一个基本的原则是,如果没有问题,泛型接口都应该继承对应的非泛型接口,这样可以实现协变性。

(P074)

实现接口所规定的的方法或属性时,附加接口名作为前缀,即称为 “显式接口实现” 。

(P075)

反射的一切都是围绕 “检查对象及其类型” 展开的。

(P078)

泛型不支持协变性 —— 它们是不变体。

【第04章】

(P091)

Nullable<T> 最重要的部分就是它的属性,即 HasValue 和 Value 。

(P092)

Nullable<T> 引入了一个名为 GetValueOrDefault 的新方法,它有两个重载方法,两者均在存在一个实例的前提下返回该实例的值,否则返回一个默认值。其中一个重载方法没有任何参数 (在这种情况下会使用基础类型的泛型默认值) ,另一个重载方法则允许你指定要返回的默认值。

【第05章】

(P114)

C# 2 支持从方法组到一个兼容委托类型的隐式转换。

(P117)

根据约定,事件处理方法应具有包含两个参数的签名。第 1 个参数是 object 类型,代表事件的来源;第 2 个参数则负责携带与事件有关的任何额外信息,该参数是从 EventArgs 派生的一个类型。

返回类型协变性和参数类型逆变性可以同时使用,虽然这样做几乎没有任何实际的用处。

(P119)

.NET 2.0 引入了一个泛型委托类型 Action<T> ,我们将在例子中使用委托。它的签名非常简单 (除了它是泛型这一事实以外) : public delegate void Action<T>(T obj) 。

(P120)

匿名方法的语法 : 先是 delegate 关键字,再是参数 (如果有的话) ,随后是一个代码块,其中包含了对委托实例的操作进行定义的代码。

基本上,在普通方法主体中能做的事情,在匿名方法中都能做。同样,匿名方法的结果是一个委托实例,可以像使用其他委托实例那样使用它。

逆变性不适用于匿名方法,必须指定和委托类型完全匹配的参数类型。

(P121)

Action<T> 委托的返回类型是 void ,所以不必从匿名方法返回任何东西。

.NET 2.0 中的 Predicate<T> 委托类型,下面列出了它的签名 : public delegate bool Predicate<T>(T obj) 。

(P122)

谓语通常在需要筛选和匹配操作中使用。

Predicate<T> 类型声明的返回类型恰好是 bool 。

(P124)

匿名方法是 C# 2 以被捕捉的变量的形式来实现,在别的地方成为闭包的一个特性。

(P125)

匿名方法能使用在声明该匿名方法的方法内部定义的局部变量。

(P127)

简单地说,捕获变量能简化编程,避免专门创建一些类来存储一个委托需要处理的信息 (作为参数传递的信息除外) 。

FindAll 的参数是一个 Predicate<T> 委托。

【第06章】

(P138)

如果方法声明的返回类型是非泛型接口,那么迭代器块的生成类型 (yield type) 是 object ,否则就是泛型接口的类型参数。

在迭代器中不允许包含普通的 return 语句 —— 只能是 yield return 。

(P141)

迭代器代码块不能实现具有 ref 或 out 参数的方法。

(P143)

只要调用者使用了 foreach 循环,迭代器块中的 finally 将按照你期望的方式工作。

(P147)

迭代器方法中的 using 语句扮演了 try / finally 块的角色。

(P148)

LINQ 的核心特性之一,是使用 Where 方法进行筛选。

【第07章】

(P154)

创建分部类型是非常容易做的事情 —— 你只需在涉及的每个文件的类型的声明部分附加一个上下文关键字 partial 。

(P158)

分部方法的声明方式与抽象方法相同 : 只使用 partial 修饰符提供签名而无须任何实现。同样,实际的实现还需要 partial 修饰符,不然就和普通方法一样了。

由于方法可能不存在,分部方法必须具有 void 返回类型,不能获取 out 参数。它们必须是私有的,不过可以是静态的 并且 / 或是 泛型的。如果方法没有在任何文件中实现,那么整个调用语句就会被移除,包括任何参数的计算语句。

(P159)

实际上如果不存在可见的构造函数 (包括受保护的) ,那么类实际上也就是密封的了。

(P160)

如果你不为类提供任何构造函数,那么 C# 1 编译器总是会提供一个公有的默认的无参数构造函数。

我们不希望出现任何可见的构造函数,所以不得不提供一个私有的。

C# 2 编译器知道静态类不用包含任何构造函数,所以它也不会提供默认的。实际上,编译器在类定义上执行了大量的约束 :

  1. 类不能声明为 abstract 或 sealed ,虽然两者都是隐含声明的;

  2. 类不能设定任何要实现的接口;

  3. 类不能设定基类型;

  4. 类不能包含任何非静态成员,包括构造函数;

  5. 类不能包含任何操作符;

  6. 类不能包含任何 protected 或 protected internal 成员;

应当注意,即使所有成员都必须为静态的,你还是要把它们都显式地声明为静态的,除了嵌套类型和常量。虽然嵌套类型是外围类的隐式静态成员,不过如果不要求的话,嵌套类型本身可以不用是静态的。

(P161)

让类成为静态的,就是在说你绝对不会创建该类的任何实例。

在 C# 1 中取值方法和赋值方法必须具有相同的可访问性 —— 它作为属性声明的一部分进行声明,而不是作为取值方法或赋值方法声明的一部分进行声明的。

(P162)

在 C# 的其他地方,在给定的情况下,默认的访问修饰符可能大部分都是私有的。换句话说。如果某些内容能被声明为私有,那么省略的访问修饰符就被完全默认为私有。这是一种很好的语言设计元素,因为这样很难意外地发生错误 : 如果你希望某些内容更加公开,在你使用它的时候会注意到。

如果你不设定任何东西,那么默认情况下 取值方法 / 赋值方法 和属性本身整体上保持一致的访问修饰符。

还要注意,你不能把属性本身声明为私有,而让取值方法是公有的 —— 你只能设置比属性更私有的特殊 取值方法 / 赋值方法 。

C# 1 的 using 指令能够用于两种情况 —— 一种是为命名空间和类型创建一个别名,另外一种就是将一个命名空间引入到当编译器查找某个类型时可以搜索到的上下文列表中。

在 C# 2 中,有 3 种别名种类 : C# 1 的命名空间别名、全局命名空间别名和外部别名。

(P166)

对 pragma 指令进行描述通常都非常简单 : pragma 指令就是一个由 #pragma 开头的代码行所表示的预处理指令,它后面能包含任何文本。

【第08章】

(P178)

不能在所有情况下为所有变量都使用隐式类型。只有在以下情况下才能用它 :

  1. 被声明的变量是一个局部变量,而不是静态字段和实例字段;

  2. 变量在声明的同时被初始化;

  3. 初始化表达式不是一个方法组,也不是一个匿名函数 (不进行强制类型转换) ;

  4. 初始化表达式不是 null ;

  5. 语句中只声明了一个变量;

  6. 你希望变量拥有的类型是初始化表达式的编译时类型;

  7. 初始化表达式不包含正在声明的变量;

(P184)

集合初始化列表并非只能应用于列表。任何实现了 IEnumerable 的类型,只要它为初始化列表中出现的每个元素都提供了一个恰当的公有的 Add 方法,就可以使用这个特性。

(P192)

如果你要创建的一个类型只在一个方法中使用,而且其中只包含了字段和普通属性,就考虑一下能否使用匿名类型。

(P193)

匿名类型允许你只保留特定情况下需要的数据,这些数据采取的是适用于那种情况的形式,不必每次都单调重复地写一个新的类型。

隐式类型的数组和匿名类型只有在与其他 C# 3 特性配合的时候才会体现它们的价值。

【第09章】

(P194)

LINQ 的基本功能就是创建操作管道,以及这些操作需要的任何状态。这些操作表示了各种关于数据的逻辑 : 如何筛选、如何排序以及如何将不同的数据源联接到一起,等等。当 LINQ 查询在 “进程内” 执行时,那些操作通常用委托来表示。

LINQ to Objects 处理的是同一个进程中的数据序列。相比之下,像 LINQ to SQL 这样的 provider 将工作交给 “进程外” 的系统 (比如数据库) 去处理。

(P195)

执行委托只是 LINQ 的众多能力之一。

从许多方面, Lambda 表达式都可以看做是 C# 2 的匿名方法的一种演变。

匿名方法能做的几乎一切事情都可以用 Lambda 表达式来完成。

匿名方法可以简明地忽略参数,但 Lambda 表达式不具备的这一特性。

在 .NET 3.5 的 System 命名空间中,有 5 个泛型 Func 委托类型。 Func 并无特别之处 —— 只是它提供了一些好用的预定义泛型类型,在很多情况下能帮助我们处理问题。每个委托签名都获取 0 ~ 4 个参数,参数类型是用类型参数来指定的。最后一个类型参数用作每种情况下的返回类型。

(P196)

当你想使用 void 为返回类型时,可使用 Action<...> 系列的委托,其功能相同。

Action 的单参数的版本在 .NET 2.0 中就有了,但其他都是 .NET 3.5 新增的。

如果 4 个参数还嫌不够, .NET 4 将 Action<...> 和 Func<...> 家族扩展为拥有 16 个参数,因此 Func<T1, ..., T16, TResult> 拥有让人欲哭无泪的 17 个类型参数。

Lambda 表达式最冗长的形式是 : { 显式类型参数列表} => { 语句 } 。

=> 部分是 C# 3 新增的,它告诉编译器我们正使用一个 Lambda 表达式。

(P197)

在阅读 Lambda 表达式时,可以将 => 部分看成 “goes to” 。

匿名方法中控制返回语句的规则同样适用于 Lambda 表达式 : 如果返回类型是 void ,就不能从 Lambda 表达式返回一个值;如果有一个非 void 的返回类型,那么每个代码路径都必须返回一个兼容的值。

对于没有返回类型的委托,如果只有一条语句,也可以使用这种语法,基本上省略分号和大括号。

表达式,不使用大括号,不使用 return 语句,也不添加分号 : { 显式类型的参数列表 } => 表达式 。

编译器大多数时候都能猜出参数类型,不需要你显式声明它们。在这种情况下,可以将 Lambda 表达式写成 : ( 隐式类型的参数列表 ) => 表达式 。

隐式类型的参数列表就是一个以逗号分隔的名称列表,没有类型。但隐式和显式类型的参数不能混合和匹配 —— 要么整个列表都是显式类型的,要么全部都是隐式类型的。除此之外,如果有任何 out 或 ref 参数,就只能使用显式类型。

(P198)

如果 Lambda 表达式只需一个参数,而且那个参数可以隐式指定类型, C# 3 就允许省略圆括号。这种格式的 Lambda 表达式是 : 参数名 => 表达式 。

是否为 Lambda 表达式的主体使用较短的形式 (指定一个表达式来取代一个完整的代码块) ,以及是使用显式还是隐式参数,这两个决定是完全独立的。

(P202)

.NET 3.5 的表达式树提供了一种抽象的方式将一些代码表示成一个对象树。

C# 3 对于将 Lambda 表达式转换成表达式树提供了内建的支持。

(P205)

Lambda 表达式能显式或隐式地转换成恰当的委托实例。

并非所有 Lambda 表达式都能转换成表达式树。不能将带有一个语句块 (即使只有一个 return 语句) 的 Lambda 转换成一个表达式树 —— 只有对单个表达式进行求值的 Lambda 才可以。

(P208)

没有 Lambda 表达式,表达式树几乎没有任何价值。

从一定程度上说,反过来说也是成立的 : 没有表达式树, Lambda 表达式肯定就没那么有用了。

(P218)

匿名函数是匿名方法和 Lambda 表达式的统称。

(P219)

Lambda 表达式要想被编译器理解,所有参数的类型必须为已知。

在 C# 3 中, Lambda 表达式几乎完全取代了匿名方法。当然,为了保持向后兼容,匿名方法仍是支持的。

【第10章】

(P220)

C# 3 引入了扩展方法的概念,它既有静态方法的优点,又使调用它们的代码的可读性得到了增强。使用扩展方法,可以像调用完全不同的类的实例方法那样调用静态方法。

(P223)

并不是任何方法都能作为扩展方法使用 —— 它必须具有以下特征 :

  1. 它必须在一个非嵌套的、非泛型的静态类中 (所以必须是一个静态方法) ;

  2. 它至少要有一个参数;

  3. 第一个参数必须附加 this 关键字作为前缀;

  4. 第一个参数不能有其他任何修饰符 (比如 out 或 ref) ;

  5. 第一个参数的类型不能是指针类型;

(P226)

实例方法肯定会先于扩展方法使用。

在 C# 中,在空引用上调用实例方法是不允许的。

可以在空引用上调用扩展方法。

(P227)

在框架中,扩展方法最大的用途就是在 LINQ 中使用。

(P229)

Where 扩展方法是对集合进行筛选的一种简单但又十分强大的方式 : 它接受一个谓词,后者应用于原始集合中的每个元素。 Where 同样返回一个 IEnumerable<T> ,但这一次所有与谓词匹配的元素都被包括到结果集合中。

(P230)

“返回相同的引用” 模式用于易变类型,而 “返回新实例 (该实例为原始实例更改后的副本)” 模式则用于不易变类型。

(P233)

LINQ 操作符是无副作用的 : 它们不会影响输入,也不会改变环境。

(P236)

C# 3 只支持扩展方法而不支持扩展属性,这稍微限制了流畅接口。

(P239)

我们学东西不应过于急功近利 —— 每次都是为了解决当前的一个实际问题才去学习。

在软件工程领域,新的模式和实践准则层出不穷,来自一些系统的设计思想经常会 “流窜” 到另一些系统中。这其实是让软件开发始终保持新鲜感的原因之一。

【第11章】

(P241)

序列是 LINQ 的基础。

(P251)

Cast 通过把每个元素都转换为目标类型 (遇到不是正确类型的任何元素的时候,就会出错) 来处理,而 OfType 首先进行一个测试,以跳过任何具有错误类型的元素。

(P254)

编译器会故意生成一个对 Select 方法的调用,即使它什么都没有做。

查询表达式的结果和源数据从来就不是同一个对象,除非 LINQ 提供器的代码有问题。从数据集成的角度看,这是很重要的 —— 提供器能返回一个易变的结果对象,并知道即使面对一个退化查询,对返回数据集的改变也不会影响到 “主” 数据。

(P255)

OrderBy 和 ThenBy 不同之处非常简单 : OrderBy 假设它对排序规则起决定作用,而 ThenBy 可理解为对之前的一个或多个排序规则起辅助作用。

(P256)

尽管你能使用多个 orderby 子句,但每个都会以它自己的 OrderBy 或 OrderByDescending 子句作为开始,这意味着最后一个才会真正 “获胜” 。

let 子句只不过引入了一个新的范围变量,它的值是基于其他范围变量。语法是极其简单的 : let 标识符 = 表达式 。

(P258)

let 子句使用对 Select 的另一个调用,为结果序列创建匿名方法,并最终创建了一个新的范围变量 (它的名称在源代码中从未看到或用到) 来实现目标。

(P260)

如果你打算把一个巨大的序列联接到一个极小的序列上,应尽可能把小序列作为右边序列。

(P261)

通常,你希望过滤序列,而在联接前进行过滤比在联接后过滤要有效得多。

内联接可用在 SQL 的所有地方,它们实际上是从某个实体导航到相关联的实体上的一种方式,通常是把某个表的外键和另外一个表的主键进行联接。

(P264)

交叉联接不在序列之间执行任何匹配操作 : 结果包含了每个可能的元素对。

(P268)

分组表达式通过分组键决定了序列如何分组。整个结果就是一个序列,序列中的每个元素本身就是投影后元素的序列,还具有一个 Key 属性,这就是那个用于分组的键;这样的组合是封装在 IGrouping<TKey, TElement> 接口中的,它扩展了 IEnumerable<TElement> 。

(P270)

查询延续提供一种方式,把一个查询表达式的结果用作另外一个查询表达式的初始序列。它可以应用于 group...by 和 select 子句上,语法对于两者是一样的 —— 你只需使用上下文关键字 into ,并为新的范围变量提供一个名称就可以了。范围变量接着能用在查询表达式的下一部分。

【第12章】

(P277)

LINQ to SQL 需要有关数据库的元数据,来知道哪个类与哪个数据库表相对应等信息。可以用几种不同的方式来表示这种元数据,而我这里使用的是 Visual Studio 内嵌的 LINQ to SQL 设计器。

【第13章】

(P312)

参数 (也称为形式参数) 变量是方法或索引器声明的一部分,而实参是调用方法或索引器时使用的表达式。

(P313)

如果某个操作需要多个值,而有些值在每次调用的时候又往往是相同的,这时通常可以使用可选参数。

(P314)

指定了默认值的参数为可选参数。

(P315)

可选参数包含一些规则。所有可选参数必须出现在必备参数之后,参数数组 (用 params 修饰符声明) 除外,但它们必须出现在参数列表的最后,在它们之前为可选参数。参数数组不能声明为可选的,如果调用者没有指定值,将使用空数组代替。可选参数不能使用 ref 或 out 修饰符。

(P316)

基本上,你必须使用永远不会改变的真正常量作为可选参数的默认值。

(P318)

如果要对包含 ref 或 out 的参数指定名称,需要将 ref 或 out 修饰符放在名称之后,实参之前。

(P319)

实参是按照参数的名称来匹配的,而不再是参数的位置。

未命名的实参称为位置实参。

所有命名实参都必须位于位置实参之后,两者之间的位置不能改变。位置实参总是指向方法声明中相应的参数 —— 你不能跳过参数之后,再通过命名相应位置的实参来指定。

(P331)

可变性有两种类型 : 协变性和逆变性。

协变性用于向调用者返回某项操作的值。

逆变性则相反。它指的是调用者向 API 传入的值,即 API 是在消费值,而不是产生值。

(P332)

在泛型接口或委托声明中, C# 4 能够使用 out 修饰符来指定类型参数的协变性,使用 in 修饰符来指定逆变性。

任何使用了协变和逆变的转换都是引用转换,这意味着转换之后将返回相同的引用。它不会创建新的对象,只是认为现有引用与目标类型匹配。

如果类型参数只用于输出,就使用 out ,如果只用于输入,就用 in 。

(P337)

只有接口和委托可以拥有可变的类型参数。即使类中包含只用于输入 (或只用于输出) 的类型参数,仍然不能为它们指定 in 或 out 修饰符。

【第14章】

(P345)

事实上, dynamic 并不代表一个特定的 CLR 类型,它实际上只是包含 System.Dynamic.DynamicAttribute 特性的 System.Object 。

(P347)

dynamic 可用来声明类型的字段、参数和返回值。这与 var 形成了鲜明的对比,后者只能用于局部变量。

(P355)

如果两个方法的签名包含的参数类型相同,编译器将选择非泛型重载,而不是泛型重载。

(P362)

dynamic 类型的参数将被视为 object 类型 —— 如果查看编译后的代码,将发现它确实为 object 类型的参数,只不过应用了额外的特性。这还意味着在你声明的方法中,它们的签名不能只以 dynamic / object 参数类型来进行区分。

(P369)

你不能声明一个基类为 dynamic 的类型,你同样不能将 dynamic 用于类型参数的约束,或作为类型所实现的接口的一部分。你可以将其用于基类的类型实参,或在声明变量时将其用于接口。 **