重读Csharp本质论一书,发现上面已经记录1500多条注释笔记,整理一下分享出来,里面会有对笔记的复制整理,也有一些新的思考。
PS:该书真是神书,除了基础语法以外,还讲解了“为什么”“如何做”这种问题,让读者知其然,也知其所以然。每个章节的内容深入浅出,覆盖面较广,有时还会提醒开发者,应该养成什么样的编码风格,无论是初学者还是有一定Csharp基础的开发者,都能有所收获。
第一篇基础内容写了13000多字,花费了很多精力。复看一下也没发现太多重要的点。
后续做个改进,精简一些,只汇总有必要的部分。
带笔记版本的书https://download.youkuaiyun.com/download/lanazyit/12881254
类型
复看时发现,类型这一章的所有内容,都提炼在开篇的这句话。

封装
封装有两层含义
- 权限控制,或者叫隐藏细节,以减少误修改和误调用


- 逻辑规整,将行为和数据封装到一个类型中,方便拓展和调用

继承
派生类(子类)是基类(父类)的更具体的类型。"更具体"这个词初看时觉得很生涩,但是反复理解后觉得很精妙。
多态
多态总结着说,其实就是同样的方法签名实现不同的功能,一般通过重写+继承实现。

并不是只有类型继承这里可以体现多态的思想,C#中的委托也可以理解为多态思想的体现。甚至可以说,一次声明,多次实现,多种用法,的编程行为都可以称之为"多态"。实际编程中要善用’‘多态’'的思想,不要拘泥死的定义。
对象和类型的区别
类型是若干行为相似,属性相同的实体的抽象,是具体物品的模版。
对象是类型的实例化,或者说具象化。
属性的作用
属性其实就是用来装饰存取的方法。比如在存取时进行校验或者其他"再处理"操作。

属性的设计编码规范

只读属性
C#6.0新特性规定,编译器要求必须要在构造器中初始化或者通过初始化器初始化只读的自动实现属性。也就说我们可以在字段声明时初始化一次,在构造器中再初始化一次。

属性的注意事项
- 属性和方法调用不允许作为ref和out参数值使用,因为ref和out要求做内存地址的传递,而属性有可能是指向一个虚字段,如要使用必须要将属性和方法的值复制到一个变量中 ,然后传递该变量。
- 所谓的属性作为虚字段,其实是属性不跟某个字段强关联,而是作为一系列数据处理的方法(可能Set是将一个值处理后存给多个数据,也可能Get是个返回多个数据处理后的结果.)PS:注意 这是自己理解性的东西,不太严谨
- 属性编译到CIL中还是编译成了方法。
- 属性的内部声明要比外部声明严格。


调用构造器时New操作符的实现细节

对象初始化器
对象初始化器允许在构造器后在对任意可以访问的属性和字段进行初始化,但是要注意对象初始化器只是一个语法糖,编译到CIL中等同于 A.a = 1这种的直接赋值代码。对象初始化器中的赋值顺序与字段的声明顺序相同。
(更多用到的是集合初始化器,写法与功能对象初始化器完全一样。只是用于集合的初始化。)

终结器
终结器由垃圾回收识别和调用。

使用This另外一个构造器


匿名类型Var
不常用,此处留个坑。
静态构造器

初始化流程
1: 子类静态变量
2: 子类静态构造函数
3: 子类非静态变量
4: 父类静态变量
5: 父类静态构造函数
6: 父类非静态变量
7: 父类构造函数
8: 子类构造函数
这里有个疑问, 如果子类和父类都有某个字段声明,那么该实例字段在会反复声明么?
经查询资料,这里跳转,应该是会重复声明,基类和派生类都持有一个同名字段,但是一般情况下基类的字段会被派生类覆盖,除非使用base调用。但是又出了一个新的疑问,在具体构造过程中,是先把所有类型的字段的声明执行完,再调用静态构造器么?等待后续查证。
静态类
既然类无法实例化,那实例成员也没有了意义,所以不能声明。

拓展方法
使用this修饰第一个类型参数(这也是this的另外一个用法)。
要注意,如果签名一样,拓展方法会被原类型覆盖。

常量const

只读readonly
总结一下:
01 readonly和只有get属性的"只读"效果是一样的
02 readonly字段只能初始化一次(声明,初始化器,构造器)
03 除了在构造器中,其他地方最好通过属性来访问readonly字段
04 相比较Const ,readonly更好用
05 readonly无法修饰局部变量,因为局部变量用完很快就被释放掉了,没有太大的只读意义。
使用readyonly锁定数组实例

嵌套类
只有嵌套类(内部类)才可以声明为private的,其他类声明为private毫无意义,所以被C#禁止了。
分部类
使用partial修饰后可以将类拆分到多个文件中去,但是编译后实际上还是一个类。主要用于当一个类代码过多时进行拆分,但是其实合理的设计不应该会出现这种情况。
类的隐式显示转换


突然没看懂,挖个坑,在223页

尽量不要在构造器中调用虚方法(Virtual)
因为它可能被子类重写,一旦被子类重写,构造链中将会出现子类字段未完整初始化时,便调用子类方法的情况,虽然不会出现编译错误,但是这样做是有风险的。

使用New关键字隐藏父类方法
只是隐藏,不会完全重写,如果由基类直接调用则会使用基类的实现.如果是派生类直接调用,会使用派生类的实现。

使用base调用基类后调用链的持有者将发生改变

抽象类中虚拟成员(abstract)不能使用Private
注意:虚拟成员是让继承类来override的,如果是private,继承类都不能看见,怎么override?所以private虚拟成员毫无意义。
IS和AS区别
As能避免异常,不能转换会返回Null,而且如果能类型之间可以隐式转换,as也会正确的返回。
IS会验证一个数据项是否属于特定类型,会返回true和false。



接口不允许使用Static
接口有个重要的目的就是不同的实例有不同的实现, 所以接口不允许使用static,因为毫无意义


显示与隐式实现接口的比较
显示实现意味着,该成员依然属于接口,其他类型无法直接调用该成员,需要转型为接口后调用。
隐式实现意味着,该成员属于派生类,其他成员可以直接访问。
- 显示实现接口无法使用public、virtual等修饰符,因为所谓显示实现,就是成员与接口强关联, 让所有的修饰符之类的与接口声明时完全保持一直就可以了,完全没必要继续添加, 故而CIL给禁止了
- 隐式实现接口成员必须是public的,因为接口的规范,一般是需要外部调用的,另外可以选择virtual。
显示与隐式实现接口如何选择?
- 判断是不是核心功能?
- 接口成员作为类成员是否恰当?
- 是否有相同签名的方法?


接口的多继承的理解
所谓继承不如说是遵循某种契约,多继承就是遵循多种契约。一个人一生可以遵循很多契约,但是只会有一个爸爸。
尽量不要为已交付的接口添加成员
因为接口中的方法是必须要实现的,新增成员会造成大面积报错。
抽象类与接口的比较

良性结构
重写Hashcode的注意事项
其实也是C#内置hashcode的特性

Equals与ReferenceEquals
ReferenceEquals表示引用相同,且值类型调用时永远返回False
Equals 如果是重写的Equals,我们在设计上可以允许在引用不同的情况下值相同也可能会返回true,另外要注意,System.Object的Equals 只是简单的调用了ReferenceEquals

重写Equals注意事项
除了以下操作符,其他的都可以重载
特别要注意不能重写 =,容易跟!=等操作符产生错误联系。

重写二元操作符其中一个参数要是其包容类型
首先,二元操作符是 +,-,*,/,%,&,|,^,<<,>>这些。重写时使用这种格式static ClassA operator +(ClassA a,ClassB b )
要记住,重写二元操作符的目的是让包容类支持某种二元操作.没有其他目的.
其中重写二元操作符时有个经典的报错就是,参数要求其中一个是包容类型.正是上述说法的佐证

重写二元操作符后会自动重写+=,!=等等操作符

还可以重写隐式转换和显示转换操作符
用的不多,挖个坑。
命名空间在C#中会编译成完整名称
比如Sytem_ClassA
XML注释会可以被编译器解析

垃圾回收
垃圾回收简述
垃圾回收时从根节点按照对象的引用关系进行遍历,所有能遍历到的对象都是正在使用的,遍历不到的即标记为可回收,然后将这些对象的内存覆盖。该操作比较耗时,可能会造成程序的卡顿。
垃圾回收分次世代
简单的说,最新(近)创建的对象,为0代,以更高的频率进行检测。在上一次检测中存活下来的对象,为1代,以更低的频率进行检测。总共有0,1,2三代。
Unity的垃圾回收存在明显的缺点,即不分次世代,无法合并随便内存。这些问题在I2CPP的版本中得到解决。
如何避免在关键时刻调用垃圾回收
这里有个说法是错误的,Collect方法是主动调用一次垃圾回收,先释放一些空间出来,以促使.net在短时间内不再垃圾回收。而不是阻止垃圾回收。

Unity的GC是一个很庞大的知识点。可以参考https://www.cnblogs.com/zblade/p/6445578.html
可以垃圾回收的引用–弱引用
简单的说,弱引用就是,某个对象在引用时,不阻止垃圾回收,允许被垃圾回收给清理。如果尚未被垃圾回收则可以重复使用,类似一个缓存机制。一般用于实例化性能消耗较大,且维护消耗较大的对象。实际开发时,使用较少。

终接器
终接器
- 使用 ~ClassName()来声明
- 终接器无法显示调用,只有垃圾回收才能调用终接器,也就说它将在对象最后一次调用后调用
- 终接器一般用于释放文件句柄和数据库连接等
- 一定要避免在终接器中抛出异常
- 终结器的代码应该简单明了,只清理一些引用。
确定性终接器
使用try/finally语句块,在finally中调用终接器的逻辑(是调用逻辑,而不是调用终接器),既可以保证在想要的时机调用终接器,如果忘记调用,终接器也会在回收时调用。
延迟初始化
使用时判空,若空再创建,类似单例,但极少使用。

异常处理
不要在构造器中引发异常
构造失败也会占用内存,但是该内存会被垃圾回收,不过还是不要造成这种情况。

空Catch块的内部原理
空catch块的CIL实现是,catch(object)



异常的处理规范
- 只捕获能处理的异常
- 不要忽略未处理的异常
- 最好不要直接使用Exception和空catch,因为捕获到了这种基类型的异常,我们一般认为程序崩溃,希望程序直接退出。
- 尽量不要在较低的调用堆栈向玩家抛出异常,因为较低的堆栈包含的错误信息大多不够完善。
- 谨慎的在catch块中,重新thow引发异常
- 向玩家抛出异常时,注意不要暴露核心信息,比如调用堆栈,或者一些本地化的字符串等。
自定义异常的规范

泛型
泛型这一章怎么这么多。。。。套娃式知识点。
这一章核心我记得不多,这里侧重优劣势比较,和语法比较。
泛型优点
- 类型安全,减少类型检查,和类型检查带来的异常
- 可读性好,便于维护
- 减少装箱拆箱,减少转换时的性能消耗,同时也节约了装箱消耗的性能
- 支持IntelliSense智能感知代码编辑器
- 这个图片啥意思,忘了,挖个坑。

一些使用泛型的建议和不常见语法
- 避免在类型中实现同一个泛型接口的多个构造。
- 要将只是类型参数数量不同的多个泛型类放到同一个文件中去,类似重载的写法。(只是一种写法建议)
-
- C#不支持参数可变的泛型类型(方法才支持)。
- 嵌套类可以自动获取包容类的泛型声明,但是如果自己也声明了同名的泛型,会将包容类的声明覆盖。
- C#4.0提供了默认的泛型,元组(Tuple),其实就是语言设计者给我们预留的一些默认的泛型类,类似后面出现的默认委托,和默认事件

泛型约束(比较重要)
泛型约束的简单声明
##### 泛型约束的一些要点
-
不允许多个类型约束,因为不可能从多个不相关的类派生。
-
如果类型约束和接口约束同时出现,类型约束要放到第一个。多个约束之间用","隔开。但是多个where之间没有逗号。

-
类型约束指向的类型,不能是密封类,因为密封类是不能被继承的。(无法显示访问构造器的类型是否可以呢,按照这个设计应该是不行的)

-
空构造器约束,面试中常见的New的另外一种用法。

限定泛型约束为引用类型或值类型(特殊语法)
- Where class 限定为引用类型
- Where struct 限定为值类型,但是可空类型Nullable不符合条件,具体看图片

泛型的继承
泛型约束不能被继承,原因是继承本意就是继承类的成员
01 类的泛型约束不被继承,因为泛型约束不是成员
02 方法的泛型约束被继承,因为方法是成员
这种设计也是为了让程序员在派生类中显示声明约束,也必须要显示保留基类的约束。以防止继承后,不知约束从哪里来,造成迷惑。
虚方法约束会隐式继承且不允许修改

那些泛型约束是不允许的
- 不支持操作符约束

- 约束之间只能是And关系。
- 委托,枚举,数组类型,不能作为基类约束,也就说不能放到where后面,因为它们是密封seal的。
- 构造器约束只有new()一个默认构造约束。
泛型方法
基础声明

C#会将泛型方法的泛型推断到"最合适"的类型
比如同时支持int和double时,传入字面量1,将会被推断为double,因为int可以隐式转为double。
但是如果使用了泛型,我们最好传入明确的类型,因为我们很多情况下我们不确定double是否是可用的。

要避免使用泛型覆盖了"正在发生"的类型转换

再挖个坑,泛型的协变性与逆变性
泛型本没有协变性,但是可以out协变,in逆变。。。
本文深入探讨C#编程中的核心概念,包括类型、封装、继承和多态的详细解释。作者强调理解这些概念的重要性,不仅限于基础知识,还涉及编码风格和最佳实践。此外,文章还涵盖了对象、属性、构造器、静态类、异常处理、泛型等多个方面,提供了丰富的实例和思考,帮助开发者提升编程技能。
741

被折叠的 条评论
为什么被折叠?



