深入理解C# 第三版学习笔记
第一章
P1
本书的关键主题之一就是进化(或者说演变)。在将任何特性引入语言之前,设计团队都会把这个特性放到现有的环境中,同时还会就其总体发展目标,对其进行严格的考察。
P4
(在P3展示了C#1.0中产品类及列表的定义方法后)
C# 1的代码存在如下3个局限:
- ArrayList没有提供与其内部内容有关的编译时信息。完全有可能不慎在Product类型的ArrayList列表中添加一个字符串类型的对象后,编译器还当做无事发生过
- 代码中为属性提供了公共的取值方法,这意味着如果添加对应的赋值方法,那么赋值方法也必须是公共的。
- 用于创建属性和变量的代码过于复杂
P5-7
(展示了C#2、3版本中对上述三个问题的解决方法)
- 使用支持泛型的List取代类型不安全的ArrayList。后者考虑到兼容性依然保留,但不再推荐使用
- public属性可以有private的赋值方法
- 支持自动实现与简化对象的属性
P8 排序和过滤的代码示范
在C#1的代码中,通过实现ArrayList的IComparer接口对列表进行排序。期间必须实现两次强制类型转换,将两个在ArrayList中以Object类型存储的,待比较的对象转化为Product,然后再比较。不仅涉及到性能问题,也有类型不安全的隐忧
P9 使用泛型与匿名函数优化后的代码
使用泛型使我们跳过了强制类型转换,而匿名函数则精简了接口的实现过程,代码变得更为简洁、高效
P13 可选参数与默认值:
有时你并不想给出方法所需的所有东西,比如对于某个特定参数,你可能总是会使用同样的值。传统的解决方案是对该方法进行重载,现在C# 4引入的可选参数(optional parameter)可以简化这一操作。
P24 对于C#行为最权威的资源是语言规范
规范有两种重要的形式——ECMA国标标准规范和微软规范。在撰写本书时,ECMA规范
(ECMA-334和ISO/IEC 23270)尽管已是第4版,却只涵盖了C# 2。谁也不知道它是否会更新,以及什么时候更新,但微软的版本是完整的,Visual studio也自带了一份。(微软好像已经把VS自带的本地规范文档砍掉了,但是呆胶布,神圣的F1连接着我们)
第二章 C# 1所搭建的核心基础
P30 为什么不直接调用方法?
答案存在于我们最开始那个让律师执行遗嘱的例子中。不能仅仅由于你希望某事发生,就意味着你始终会在正确的时间和地点出现,并亲自使之发生。有时,你需要给出一些指令,将职责委托给别人。因此委托的实质是间接完成某种操作
P32
你可能对事件有了一个直观的概念,它的基本思想是让代码在发生某事时作出响应。开发者经常将事件和委托弄混,但要记住,事件不是委托类型的字段。虽然它被声明为一个委托,触发后能够调用某个方法do some thing,写法看上去也差不多。但是事件实际上是一个委托的封装层。两者的区别就跟属性与方法一样:看上去在获取属性(通过事件调用委托实例)但你实际上是在调用获取属性的方法(使用事件而非委托)
P39 C#1类型系统的特征
- C# 1是静态类型的——编译器知道你能使用哪些成员;
- C# 1是显式的——必须告诉编译器变量具有什么类型;
- C# 1是安全的——除非存在真实的转换关系,否则不能将一种类型当做另一种类型;
- 静态类型仍然不允许一个集合成为强类型的“字符串列表”或者“整数列表”,除非针对不同的元素使用大量的重复代码;
- 方法覆盖和接口实现不允许协变性/逆变性。
P40 值类型和引用类型的差异
- NET中的大多数类型都是引用类型,你以后创建的引用类型极有可能比值类型多很多。除了以下总结的特殊情况,类(使用class来声明)都是引用类型,而结构(使用struct来声明)是值类型。
- 数组类型是引用类型,即使其中的元素类型是值类型
- 枚举是值类型
- 委托类型是引用类型
- 接口类型是引用类型,但可由值类型实现
P41 误区1
“结构是轻量级的类”。结构同样能拥有方法和对自身值进行计算的能力,结构在特定条件下也不一定比类更“轻”,例如向一个函数传递数据时,作为引用类型的类可以只传递地址,而结构会将自己复制一份
P42 误区2
“引用类型保存在堆上,值类型保存在栈上”。第一部分是正确的——引用类型的实例总是在堆上创建的。但第二部分就有问题了,变量的值是在它声明的位置存储的。只有局部变量(方法内部声明的变量)和方法参数在栈上。
“对象在C#中默认是通过引用传递的”。现在请记住,引用类型变量的值是引用,而不是对象本身。不需要按引用来传递参数本身,就可以更改该参数引用的那个对象的内容。
P43 装箱和拆箱
C#和.NET提供了一个名为装箱(boxing)的机制,它允许根据值类型创建一个对象,然后使用对这个新对象的一个引用。要注意这一操作带来的性能损益
第三章 用泛型实现参数化类型
P53
有了泛型,用户在程序中用错误的参数调用库时,就无法通过编译。泛型的好处主要有两个:方便进行静态检查,以免出现类型不安全的情形;降低运行时转换或装箱操作的成本与风险。官方文档表示,非泛型集合已不推荐使用,只是出于兼容性考虑而继续保留
P56
泛型有两种形式:泛型类型(包括类、接口、委托和结构——没有泛型枚举)和泛型方法。两者都是表示API的基本方法(不管是指单独的泛型方法还是完整的泛型类型),在平时期望出现一个普通类型的地方,用一个类型参数
P63
我们经常需要调用类型参数实例的方法,创建新实例,或者想确保只接受引用类型(或者只接受值类型)。换言之,我们想制定规则,从而判断哪些是泛型类型或泛型方法能接受的有效类型实参。在C# 2中,这用约束来实现
P76
从理论上而言,泛型的使用可能对性能造成轻微的负面影响,因为JIT需要考虑生成代码的共享问题以及回收的时机,要JIT编译的代码也增加了。但从更普遍的情形而言,泛型本身有相当大的性能优势,减少了装箱、开箱的消耗
P83
泛型的限制:数组存在着协变性——引用类型的数组可以被视为它的基类型的数组,或者被视为它所实现的任何接口的数组。实际上这一概念有两种形式,称为协变性和逆变性,或统称为可变性。泛型不支持可变性——它们是不变体(invariant)。这是为类型安全性着想,但它有时也会带来不便。这是框架和语言的设计者在仔细考虑后做出的选择。(所以为什么数组是协变的?因为因为C#的前辈Java的数组支持,虽然这个feature在Java里也是个瑕疵,是为了在没有泛型的条件下使数组用到类似泛型的功能而设的)
P90 与Java泛型的对比
Java从1.5开始正式引入了泛型。在为泛型类型生成的Java字节码中,包含一些额外的元数据,来表示这是一个泛型。但是,在编译之后负责调用的代码时根本就发现不了曾有泛型出没的迹象。事实上严格来说Java并没有实现真正的泛型。泛型代码在编译时被编译器添加了强制类型转换(类型擦除),因此实际上Java运行时并不知道泛型的存在,它们是被翻译到具体的类型的。所以如果我们反编译Java代码,很难发现泛型的痕迹。当然Java的“残废”泛型也有好处:能够以很小的代价实现代码向前兼容(没错,它逆练了微软的向后兼容神功),不需要像.NET一样另起炉灶再搞个完全不一样的泛型容器,旧特性得以利用,并通过通配符支持协变和逆变。这个话题还是蛮有意思的,感兴趣的可以去知乎上翻一翻
第四章 可空类型
P94:为什么值类型的变量不能是null?
如第2章所述,对于一个引用类型的变量来说,其值是一个引用。而值类型变量的值是它本身的真实数据。可以认为,一个非空引用值提供了访问一个对象的途径。然而,null相当于一个特殊的值,它意味着我不引用任何对象。
P96
可空类型的核心部分是System.Nullable
P100
Nullable
P102
在C#语言规范中,可空类型是指可以包含空值的类型——如引用类型和Nullable
P105
可空转换的问题:假如一个非可空的值类型支持一个操作符或者一种转换,而且那个操作符或者转换只涉及其他非可空的值类型时,那么可空的值类型也支持相同的操作符或转换,并且通常是将
非可空的值类型转换成它们的可空等价物。例如int到long存在着一个隐式转换,这就意味着int?到long?也存在一个隐式转换
P107
作者在4-4展示了一个非空类型birth与可空类型death的运算,最后用death.value获取到可空类型的值,然后进行计算。因为如果直接将二者一同计算,会把最终的结果也变为可空类型,或者发生一次强制转换,造成额外的性能损耗
P117
可空类型解决的是一个非常具体的问题。在C# 2以前,这个问题只能用一些比较“难看”的方案来解决。C# 1的老方案虽然可行,但执行起来比较花时间。现在利用新的特性,可以获得更好的支持。将泛型(为避免代码重复)、CLR支持(提供合适的装箱和拆箱行为)以及语言支持(提供简练的语法、方便的转换和操作符)相结合,使现在的方案变得更引人注目
第五章 进入快速通道的委托
P120
在C# 1中,如果要创建一个委托实例,就必须同时指定委托类型和要执行的操作。作为一个独立的表达式使用时,它并不“难看”。即使在一个简单的事件订阅中使用,它也是能够接受的。但是,在作为某个较长表达式的一部分使用时,看起来就有点“难看”了。为此,C# 2支持从方法组到一个兼容委托类型的隐式转换。
P122
在静态类型的情况下,如果能调用一个方法,而且在能调用一个特定委托类型的实例并使用其返回值的任何地方都能使用该方法的返回值,就可以用该方法来创建该委托类型的一个实例。在C#2中,我们可以使用派生自同一个父类的参数创建委托实例,比如说5-2就用派生键盘、鼠标事件参数的父类EventArgs创建的方法,把5-1里的几个专门处理键盘和鼠标事件的方法替代了
P125
由于C#2中新特性的加入,委托有了更灵活的运用方式,函数能够在实例化时自动进行可变性处理。但也造成了一个问题:当派生方法对基类方法进行重载时,该执行哪一个?C#1的编译器选择了基类方法,而C#2的编译器会选择派生方法。如5-4所示,SampleDelegate接受的参数为string。在C#1没有引入可变性的情况下,编译器会选择执行与此类型相符的方法,即基类中接受string x参数的CandidateAction。但在C#2中,因为string是自object中派生的,所以编译器按照可变性原则,会把SampleDelegate接受的参数从string转为object,派生类Derived中重载的方法反而能够用了。当然,这种搞脑子的问题只在很少的情况下出现
P126
通过匿名方法,我们无需再为某些一次性、不需要复用的代码进行复杂的定义和委托创建工作了
P129
在少数情况下,你实现的委托可能不依赖于它的参数值。可以完全省略参数列表,只需使用一个delegate关键字,后跟作为方法的操作而使用的代码块
P131
运用闭包概念设计的匿名方法能够捕获在声明该匿名方法的方法内部定义的局部变量,进而对外部环境产生影响。像在5-10里,匿名方法便能从其定义域外部捕获根本没有进行传值操作的capturedVariable变量并输出
P133
捕获变量能简化避免专门创建一些类来存储一个委托需要处理的信息
P137
(来自作者的恶意)共享和非共享变量混合使用可能造成不可预料的后果。例如这里的5-14中定义了两个delegate,每个delegate执行后都会分别让定义delegate的for循环内的变量inside及外围的outside变量自增。外围的outside变量因为只定义了一次,所以只有一个变量实例,而inside变量则因为内部循环迭代而反复实例化,每个实例都有自己的inside变量,所以使用这两个定义的方法输出时会比较鬼畜:前一个委托先输出(0,0)然后(1,1),再然后(2,2)——委托被实例化时不会被执行,所以虽然委托内部有outside的自增操作,但是在执行委托前不会变。后一个委托执行输出为(3,0)、(4,1),因为后一个委托调用的变量inside和前一个委托不同
P138
一个匿名委托能够截获作用域外的外部变量,甚至同一个作用域内可以并存多个同名不相同的变量。这显然已经超出了一般程序语言对于定义域的规定了。事实上匿名委托所调用的这些变量并不是存活在作用域的寻常变量,它们在编译时是被单独生成到一个额外的类中的
P139:使用捕获变量时的规则
- 如果用或不用捕获变量时的代码同样简单,那就不要用。
- 捕获由for或foreach语句声明的变量之前,思考你的委托是否需要在循环迭代结束之后延续,以及是否想让它看到那个变量的后续值。如果不是,就在循环内另建一个变量,
- 用来复制你想要的值。(在C# 5中,你不必担心foreach语句,但仍需小心for语句。)
- 如果创建多个委托实例(不管是在循环内,还是显式地创建),而且捕获了变量,思考一下是否希望它们捕捉同一个变量。
- 如果捕捉的变量不会发生改变(不管是在匿名方法中,还是在包围着匿名方法的外层方法主体中),就不需要有这么多担心。
- 如果你创建的委托实例永远不从方法中“逃脱”,换言之,它们永远不会存储到别的地方,不会返回,也不会用于启动线程——那么事情就会简单得多。
- 从垃圾回收的角度,思考任何捕获变量被延长的生存期。这方面的问题一般都不大,但假如捕获的对象会产生昂贵的内存开销,问题就会凸现出来。
第六章 实现迭代器的捷径
P141:可以将迭代器想象成数据库的游标,即序列中的某个位置。迭代器只能在序列中向前移动,而且对于同一个序列可能同时存在多个迭代器操作
第七章 结束C#2的讲解:最后的一些特性
P166
有些时候,我们希望能在手动创建的文件中指定某种行为,并在自动生成的文件中使用该行为。例如,在一个具有很多自动生成属性的类中,我们希望能够指定执行代码,作为某些属性接受新值的验证。另外一个常见的情况是,为代码生成工具包含构造函数——手写的代码经常想挂钩到对象构造过程中,以设定默认值,执行某些日志记录,等等。在C# 2中,这样的需求只能通过使用能够订阅事件的手动生成代码,或者通过创建自动生成代码来满足。C#3的分部方法提供了一些可选的钩子
第八章 用智能的编译器来防错
P186
C#中的var并不是JS里那样声明一个无类型变量,而是把变量的类型声明推迟到编译之时,让编译器根据上下文推定一个静态的,具体的变量
P187
对隐式类型的限制——事实上这理解起来不难,因为编译器需要根据上下文将var转为特定的静态类型,所以不符合这一条件(如变量未被初始化)的情况自然不被支持
- 被声明的变量是一个局部变量,而不是静态字段和实例字段;
- 变量在声明的同时被初始化;
- 初始化表达式不是方法组,也不是匿名函数(如果不进行强制类型转换);
- 初始化表达式不是null;
- 语句中只声明了一个变量;
- 你希望变量拥有的类型是初始化表达式的编译时类型;
- 初始化表达式不包含正在声明的变量
P188
之所以使用隐式类型(暂不考虑匿名类型的问题),主要原因是它不仅减少了代码的输入量,还减少了屏幕上显示的代码量(这就意味着可读性增强)。读取代码时,相同的长类型名称在同一行上没必要出现两次——如果它们显然应该是同一个类型的话。如果在屏幕上看不见声明,是否使用了隐式类型就没有什么区别(用于查看变量类型的所有方法仍然有效)。如果在屏幕上看得见声明,那么用于初始化变量的表达式无论怎样都能告诉你类型是什么。使用var还改变了代码的重心。有时,你希望读者将注意力放到确切的类型上,因为它们更重要。
P198
隐式类型、对象和集合初始化程序以及隐式数组类型均有或大或小的用处。然而,它们也可以组合起来使用,从而服务于一个更高的目标,也就是本章最后一个要讲述的特性——匿名类型。而匿名类型又服务于一个还要高的目标——LINQ。
第十五章 使用async/await进行异步编程
P407
C# 5引入了异步函数(asynchrnous function)的概念。通常是指用async修饰符声明的,可包含await表达式的方法或匿名函数。从语言的视角来看,这些await表达式正是有意思的地方:如果表达式等待的值还不可用,那么异步函数将立即返回;当该值可用时,异步函数将(在适当的线程上)回到离开的地方继续执行。此前“在这条语句完成之前不要执行下一条语句”的流程依然不变,只是不再阻塞