# 前言

本来计划的是 3 月份就看完,结果 4 月 2 号了还剩两章,虽然这跟到后面看得更仔细有关,但是公司上个月突然要求加起班来也有关系,导致每周上班时间只有两天可以提前回家看书。

不过,在昨天 清明节 总算把这本书看完了!

看了下语雀的笔记创建记录:

已经记不起什么时候开始看的,笔记是 2 月 27 日创建的,以这个时间点开始也一个月出头了,基本上回家就是看这本书

刚拿到手感觉是真厚,最开始做笔记只按照分类记录了重点:后面觉得这样不大行,越记就越庞大

而且书的章节本身其实可以看做就是有根据功能分门别类的,于是从第九章开始,以章节划分开始记录

  • 因为是用手机记录的,所以有的英文字母用的小写

  • 《CLR VIR C#》 太厚了,由于记录太多,分成了成两篇上传

总的来说,值得一看,获益良多。

# 基本名称

  • FCL:framework class library
  • CTS:common type system
  • CLI:cts 和其它组件提交给 ECMA 形成(公共语言基础结构)
  • CLS:公共语言规范,common language specification,语言互操作的标准规范,为支持不同语言互操作,详细定义了一个最小功能集,是 clr/cts 的子集。规定类型成员要么是字段(数据),要么是方法(行为)
    • CLSCompliantAttribute 可以标记类中公共成员必须符合 cls 规范(但是反射可以强行访问非公共、不合 cls 的自字段)
  • CIL:Common Intermediate Language 通用中间语言,是介于源代码和本机机器指令中间的代码
  • 蓝色体 代表关键字,浅蓝色体 代表类型,比如大写 string 表示基元类型
  • 值类型 equals 最好重写,内置反射调用
  • 友元程序集
  • dynamic 基元类型
    • 实际上就是 Object
    • 字段、参数或返回类型若使用 dynamic,编译器会附加 dynamicattribute 特性(局部变量使用不会附加特性,因为限制在方法内部使用)
    • 所有表达式都能隐式转为 dynamic:因为所有表达式最终都生成从 Object 派生的类型
    • 虽然 Object 不允许隐式转换为其它类型,但是 dynamic 可以,不过运行时会额外验证转型保证类型安全性
  • 分部类(partial)
  • 分部方法
    • 若没有对分部方法 实现,调用分部方法的代码会直接被编译器优化掉,也不会生成分部方法的元数据:包括调用方法传递参数造成的计算也会被优化掉,可以提高性能
    • 只能在分部类或结构中声明
    • 分部方法只能返回 void,参数不能用 out 标记:因为方法在运行时可能不存在
    • 如果附加了特性,编译器会合并两个方法的特性(参数特性也一样)
    • 分部方法被视为 private(不过编译器简直添加访问修饰符?)
  • csp(component software programming) 是 oop 发展极致结果
  • system.appdomain
  • 显式接口方法实现 (EIMI):将定义方法的接口名称作为方法名前缀(explicit interface method implementation)
  • 基类还是接口?
    • 『属于』 和 『能做』 的关系

# 其它

  • clr 基于栈
  • 操作符重载:public static (type) operator(操作符)
  • 转换操作符重载:一种类型转换为另一种类型
  • 要求参数类型和返回类型二者其一必有定义转换方法类型相同
  • 隐式:public static implicit operator (return type)(in type)
  • 显式:public static explicit operator (return type)(in type)
  • c# 编译器检测到代码正在使用的某个类型对象实际期望的是另一种类型对象,则查找能隐式转换操作符方法,若找到则生成调用方法代码(显式也一样)
  • 使用强制类型转换表达式时,c# 生成代码调用显式操作符重载方法,使用 c# as is 时则不会调用
  • 扩展方法:
    • 实际上是对一个静态方法的调用(所以不会对调用实例做 null 检查,扩展方法可能接受 null 实例)
    • 可以为接口定义扩展方法
    • 可以为委托类型定义扩展方法
    • 可以为枚举添加扩展方法
    • 扩展方法是静态方法,若作为委托,编译器是耍了小花招的,生成特殊代码来处理引用的静态方法传递第一个 this (调用实例) 参数
    • 编译器会先搜索类型自身或基类是否存在对应参数实例方法,没有找到就会搜索所有静态文件找到对应扩展方法,并生成 il 代码调用静态方法
    • 在 c# 中,一旦用 this 关键字标记了某个静态方法的第一个参数,编译器就会在内部向该方法应用 extensionattribute 特性,并持久化存储在元数据中,任何静态类只要包含了至少一个扩展方法,其元数据也会应用该特性,同理任何程序集只要包含了一个符合上述特点的静态类,元数据也会应用该特性 —— 以加快编译器搜索速度:可以只扫描指定程序集 - 指定静态类 - 指定扩展方法
    • 扩展一个类型时,其派生类也会被扩展
    • 扩展方法不能引用它们正在扩展的类的私有成员或受保护成员,因此不能完全替代更传统的类继承
  • decimal 是个类
  • 内联代码?
  • 委托实际只是提供了 4 个方法的一个类定义
    • 构造器、invoke 方法、begininvoke 方法、endinvoke 方法
  • 协变指定返回类型的兼容性
  • 逆变指定参数的兼容性
  • convert.changetype

# IL

  • call:可调用静态方法,实例方法和虚方法
    • 调用实例方法和虚方法必须指定引用了对象的变量,该指令假定该变量不为 null
  • callvirt:可调用实例方法和虚方法,不能调用静态方法(需要对变量做 null 检查,因此比 call 慢)
    • 并且调用虚实例方法时,还需要检查发出调用对象的实际类型,然后以多态方式调用
  • 编译器会在程序集方法定义表中用标志 (flag) 指明方法是实例方法、虚方法、静态方法
    • 调用这些方法时,编译器会判断对应方法定义的标志 (flag) 生成对应调用指令
  • c# 用 callvirt 指令调用所有实例方法
  • 编译器调用值类型定义的方法时,倾向于使用 call 指令
    • 因为值类型实例的本质保证永不为 null
    • 并且要是以虚方式调用值类型中虚方法,会导致值类型装箱
  • 无论 call 还是 callvirt 都接受隐藏的 this 实参作为方法第一个参数(操作的对象)
  • 设计类型应该尽量减少虚方法数量:
    • ~ 调用虚方法比非虚方法更慢:clr 必须在运行时查找对象类型,以判断要调用的方法由哪个类型定义(密封类可由 jit 优化调用,因为不需要判断是否还有派生类重载)
    • 密封类型的虚方法调用,会被优化,性能更好
    • 一旦将某个方法、属性或事件设置为 virtual,基类就会丧失对其行为和状态的部分控制权
    • ~jit 不能内嵌(inline)虚方法
    • new virtual 可以重写虚方法
  • 内联函数:
    • MethodImpl(MethodImplOptions.AggressiveInlining)

#

  • 字段定义时直接赋值,称为 内联初始化

  • 内联初始化:指在代码中直接赋值来初始化,而不是将对构造器的调用写出来

  • 实际上 c# 是在构造器中初始化的,这只是提供的一种简化语法

  • 使用内联语法而非构造器中赋值,会有一些性能问题需要考虑

  • 有多个构造器时,每个构造器都会被插入内联变量初始化代码,然后调用基类构造方法,最后插入构造方法自身的代码(因此存在 代码膨胀效应)—— 若是在构造函数中自己初始化,可以复用同样的初始化构造函数,以减少这种情况

  • 对引用类型标记 readonly,只是单纯这个地址引用不可变,其内部数据依然可变

  • 反序列化使用 getuninitializedobject 为对象分配内存,避免调用构造器(另外,memberclone 也不会导致调用)

  • 避免在构造函数中调用虚方法:因为父类构造函数初始化更早,子类若重写了虚方法的行为不可预测

  • 声明为密封类可提高性能,JIT 就不再需要考虑其他的可能性而放开手脚进行优化:

    • 密封类型的虚方法调用,会被优化,性能更好
    • 数组是支持协变的:JIT 在将一个项目分配到数组之前必须检查对象的类型。当使用密封类型时,JIT 可以取消检查
    • 数组转换成 Span:与数组同理
    • 当对象类型转换时:当转换到一个非密封的类型时,运行时必须检查层次结构中的所有类型
  • 编译器自动生成的代码都是 sealed 密封类

# 值类型

  • 值类型没有默认无参构造函数(所以也不支持内联初始化语法)
  • 并且不允许定义无参构造函数(但是 clr 允许)
  • 值类型的构造函数只有显式调用才会执行
  • 结构体构造函数中,可以允许直接:this=new xxx () 初始化所有字段,然后再覆盖

# 静态构造方法(类型构造器)

  • 只能定义一个无参的静态构造方法,由 clr 在类型第一次被访问时调用
  • 只能访问静态字段(用来初始化它们)
  • 结构体(值类型)静态字段也可以使用内联初始化语法
  • 类型构造器调用比较麻烦:jit 在编译一个方法时,会查看代码中引用了哪些类型,任何一个类型定义了静态构造器,编译器会检查当前 appdomain 是否已经执行过,若没有执行,则添加执行代码调用
    • 初始化调用时,clr 会使用互斥线程同步锁保证同一个 appdomain 只会初始化一次
    • 所以是线程安全的,因此单例可以这样整?饿汉模式?
  • !值类型的构造方法可能不会被调用

# 工具

  • *.rsp:响应文件,包含了一组编译器命令行开关的文本文件,使用 csc.exe @my.rsp 代码.cs

全局 csc.rsp

  • AL.exe:程序集链接器
  • assemblyversion:非常重要,存储于 assemblydef 清单元数据表,唯一性标识程序集

# 第九章 参数

可选参数和命名参数

  • 一旦为参数分配了默认值,编译器会对参数应用 optionalattribute 特性及 defaultparametervalueattribute 特性。生成对应元数据
  • 后续编译器发现某个方法调用缺失部分实参,就可以确定是否是可选实参,并从元数据提取默认值自动嵌入调用中

隐式类型局部变量

  • var:只能声明局部变量,由编译器推断

传引用方式向方法传递参数

  • out
  • ref
  • 两者生成 il 代码一样,只有元数据有一个 bit 差异:用于记录是 out 还是 ref
  • 从 il 和 clr 来看:都是导致传递指向实例的一个指针(主要差别在于编译器的验证准则)
  • 大的值类型可避免复制开销

可变参数:

  • params:必须位于参数最后一位
  • 编译器会应用 paramarrayattribute 特性
  • 调用时,若没有找到对应的普通方法,则生成代码构造一个数组来调用它
  • 会有额外性能开销,例如造成数组分配:数组必须在堆上分配、数组元素必须初始化、数组内存最终会被垃圾回收(传递 null 不会分配)

参数和返回类型设计规范

  • 参数尽量指定最弱类型:例如接口比基类更好
  • 返回类型:最好声明为最强类型 (防止受限特定类型)

# 第十章 属性

属性看起来和字段类似,但本质是方法
取决于属性定义,编译器在程序集生成以下两项或三项:

  • 代表 get 访问器的方法
  • 代表 set 访问器的方法
    • set 编译出来的方法会有定义类型的 value 默认参数
  • 程序集元数据会有属性定义 (必然生成)
    • 包含一些标志 (flag) 及属性类型等信息
    • 用于在 属性 这个抽象概念与其访问器方法之间建立一个联系
    • clr 不使用这种元数据信息,主要是给编译器或其它工具通过 PropertyInfo 来获得信息

自动属性(Automatically Implemented Property)简称 AIP

  • 其私有字段名由编译器自动生成,因此序列化数据可能出问题

与字段对比:

  • 属性不能作为 out 或 ref 传引用
  • 属性方法可能抛出异常,字段访问永远不会
  • 属性方法可能花较长时间运行,字段访问总是立即完成
  • 连续多次调用,属性方法每次可能返回不同值
  • 属性方法可能需要额外内存,或返回对象并非指向对象状态一部分,造成对返回对象的修改作用不到原始对象上(使用会返回一个拷贝的属性很容易引起混淆)

如果属性实现了 IEnumerable,属性就被认为是集合

可以这样初始化:

Prop={"1","2","3"} dic={{"1",1},{"2",22},{"3",33}}

匿名类型

  • 与匿名函数一样,会生成一个新的类
  • 编译器定义匿名类型时,若发现源代码中定义了多个具有相同结构的匿名类型,会只生成一份类型定义,虽然还是会创建多个实例
    • 相同结构:相同名称和类型,指定顺序相同

Tuple

  • Tuple.Create 简化代码,自动推断类型

System.dynamic.ExpandObject
有参属性

  • C# 称为 索引器
  • 可以将 索引器 看作对 [] 符号的重载
  • 一个类型可定义多个索引器
  • 默认索引器名称为 set_Item,使用 IndexerName 特性 可以让编译器为索引器 (属性) 自动生成的名字进行自定义重命名
    • 特性本身不进入元数据
    • 例如 string 的索引器名称是 Chars

性能

  • 对于简单的 get、set 访问器方法,JIT 会将代码内联(调试版本不会优化)

# 第十一章 事件

  • 委托是不可变的
  • 事件参数传递:EventArgs
  • 事件成员:EventHandler
  • 事件编译出的添加、移除方法利用原子操作,以线程安全的一种模式更新值
  • 试图删除从未添加的方法,Delegate 内部不会做任何事情(不会抛出异常或警告)
  • 除了自动生成的三个构造代码,编译器还会在元数据附加记录项,用于建立 "事件" 抽象概念 与 add remove 访问器方法的联系(flag、基础委托类型、访问器方法)
    • 不过 clr 本身是不会用这些元数据信息的,只编译器等工具会利用这些元数据信息, EventInfo
  • 通过 +=、-= 移除添加事件,会被编译器编译为调用自动生成的 add、remove 方法
  • 移除事件时,需要扫描委托列表寻找一个恰当匹配:包装方法和传递方法相同
  • 可以显式实现事件,避免编译器自动生成代码:事件访问器

# 第十二章 泛型

  • 面向对象 代码重用,泛型 算法重用
  • 泛型引用类型、泛型值类型、泛型接口、泛型委托、泛型方法(不允许泛型枚举类型)
  • 优势
    • 源代码保护
    • 类型安全
    • 更清晰代码:编译器强制类型安全性,减少强制类型转换次数
    • 更好的性能:省略强制类型转换消耗;避免值类型装箱消耗
  • 泛型同样会创建类型对象(type object)内部数据结构:开放类型,开放类型不允许创建实例
  • 代码引用泛型类型时,指定泛型参数后成为 封闭类型;可仅指定部分泛型参数,会导致创建新的开放类型对象
  • 泛型的静态构造方法和静态字段,针对每个封闭类型都有一份,不共享
  • 使用泛型类型并指定类型实参时,实际是在 clr 中定义了一个新的类型对象,新类型派生自泛型父类
  • 缺点
    • 代码爆炸:clr 优化方式是对引用类型进行共享 (都属于指针操作),但是值类型实参不行,因为可能要用不同的本机 cpu 指令操纵这些值
  • 委托实际只是提供了 4 个方法的一个类定义
    • 构造器、invoke 方法、begininvoke 方法、endinvoke 方法
  • 协变量:泛型类型参数可以从一个类更改为它的某个基类
  • 逆变量:泛型类型参数可以从一个类更改为它的某个派生类
  • 不变量:泛型参数不能更改
  • 泛型方法
    • 作为 out/ref 实参传递的变量必须具有与方法参数相同类型
    • 编译器支持在调用泛型方法时进行 类型推断
    • 有重载时,编译器 (类型推断) 先考虑明确匹配,再考虑泛型匹配 (也可指定泛型实参明确调用泛型方法)
  • 泛型约束
    • 限制能指定成泛型实参的类型
    • 可用于泛型类型的类型参数、泛型方法类型参数
    • 派生类不能修改重写的虚方法在父类指定过的约束
    • 主要约束:非密封类的一个引用类型
      • 指定类型要么是约束类型相同类型,要么是约束类型的子类
      • 特殊主要约束:class (引用类型)、struct (值类型)
    • 次要约束:可指定零个或多个次要约束,代表接口类型
    • 类型参数约束 (裸类型约束):指定类型实参要么是约束类型,要么是约束类型子类
      • Exp<T,TBase> where T:TBase
    • 构造器约束:new (),约束类型实参是实现了公共无参构造方法的非抽象类型
  • default (T):引用类型设为 null,值类型将所有位设为 0
  • 泛型类型转型
  • 与 null 比较(值类型未约束为 struct 情况才能用但始终非 null)
  • 不允许将类型参数约束为具体值类型
    • 因为不可能存在从值类型派生的类型,如果允许约束成具体值类型,还不如不要泛型
  • 泛型类型变量作为操作数使用 (操作符运算)
    • 值类型不允许

# 第十三章 接口

  • 终极基类 System.Object 定义 4 个公共实例方法:tostring、gethashcode、equals、gettype
    • 因此接口也允许调用 Object 方法
  • Object 派生类继承:
    • 方法签名:使代码认为自己是在操作 Object 类实例,但实际操作的可能是其它类实例
    • 方法实现:使开发人员定义 Object 派生类时不必手动实现 Object 的方法
  • 接口:对一组方法签名进行统一命名
    • 因此还可定义事件、无参属性和有参属性 (索引器),因为它们本质是方法
  • 在 clr 看来,接口定义就是类型定义:clr 会为接口类型对象定义内部数据结构,并能通过反射机制查询接口类型功能
  • 实现接口的方法若没有显式标记为 virtual,编译器会自动标记为 virtual+sealed
    • 派生类不能重写 sealed 方法,但可以重新继承同一接口并提供自己的实现
  • 值类型也可实现零个或多个接口,但值类型的实例转换为接口时会导致装箱
    • 因为接口变量是引用,必须指向堆上对象,使 clr 能检查对象的类型对象指针,从而判断对象的确切类型
    • 调用已装箱值类型方法时,clr 会跟随对象的类型对象指针找到类型对象的方法表,从而调用正确方法
  • 类型加载到 clr 时,会为对象创建并初始化一个方法表
    • 类型引入的每个新方法都有对应记录项
    • 继承的所有虚方法也会添加记录项 (有继承层次基类定义的,也有接口类型定义的)
  • 显式接口方法实现 (EIMI):将定义方法的接口名称作为方法名前缀
    • 不允许指定访问性,编译器生成元数据时,会自动设置为 private
      1. 没有文档解释具体实现,没有智能感知支持、2. 值类型实例转换为接口时发生装箱、3. EIMI 不能由派生类调用
  • 接口方法理想情况下应该是强类型的
  • C# 编译器为接口约束生成特殊 il 指令 (constrained),导致在值类型上调用接口方法而不装箱(不用接口约束便无法生成,调用接口方法就会导致装箱)
    • 指:约束了泛型参数一定得实现某个接口时,实现了接口的值类型可以不装箱放进去?(待测试,搞不清楚)
    • 例外:值类型实例上调用

# 第十四章 字符、字符串和文本处理

  • 字符 (Char):总是表示成 16 位 Unicode 代码值
  • Char getunicodecategory 方法返回一个枚举,标志该字符是由 Unicode 定义的控制字符、货币符号、小写字母、大写字母、标点符号、数学符号或者其它字符
    • 同时也提供了一些静态方法用于判断
  • ToLowerInvariant、ToUpperInvariant:以忽略语言文化方式将字符串转换为小写或大写形式
    • 微软对执行大写比较的代码做了优化
  • Covert 类型转换内部使用了 checked 检查
  • 字符串
    • 直接使用字面值 (literal) 会被编译至元数据,并在运行时加载和引用
    • C# 不允许使用 new 操作符从字面值字符串构造 String 对象
    • il 中使用 ldstr 从元数据获得字面值字符串构造 String 对象
    • 字面值字符串连接会被编译时直接连接并放入元数据,非字面值连接则在运行时进行
  • 字符串比较
    • equals
    • compare
    • startswith
    • endswith
    • currentculture.compareinfo.compare
    • StringComparer:大量不同字符串反复执行同一种比较
  • 字符串留用
    • clr 初始化会创建一个内部哈希表,key 为字符串,value 则为托管堆中的 string 对象引用
    • intern、isinterned
    • 加载时默认留用所有字面值字符串 (可设置,并取决于 clr)
    • 除非显式调用 intern,否则不要以 字符串已留用 前提写代码
  • Char 代表一个 16 位 Unicode 码值,但是不一定等同于一个抽象的 Unicode 字符
    • 可能需要多个字符表示一个 Unicode 字符
    • 有的 Unicode 字符需要两个 16 位值表示,称为 高位代理项 和 低位代理项,使其能表示 100 万个以上的字符
  • stringbuilder
    • 字符数组,超过分配大小后,会分配新的字符数组,前一个字符数组会被回收
    • 只有超出容量或 tostring 会导致分配新的对象
  • string.format:替换参数花括号中可以使用『:』指定格式化信息
  • 定制格式化器
    • IFormatProvider
  • 安全字符串:System.Security.SecureString
    • 会在内部分配非托管内存,包含一个字符数组,字符经过加密
    • 调用操作接口时,会先 解密 - 操作 - 加密,因此性能一般

# 第十五章 枚举类型和位标志

  • 结构:简单来说只是一个结构,其中定义了一组常量和一个实例字段
  • 枚举类型定义的符号是常量值,编译时会用数值替换符号
  • getunderlyingtype:获取容纳枚举类型值的基础类型
  • Type.getenumunderlyingtype:同上
  • 操作符实际作用于内部的 value 实例字段
  • IsDefined:通过反射查找,很慢
  • Flag 标志:ToString 会自动拼接(不定义该标志,ToString ("F") 也可以)

# 第十六章 数组

  • 一维 0 基数组 (向量) 性能最好
    • 有专用 il 指令处理
    • 访问元素时不需要从指定索引减去偏移量
    • 索引检查会在循环外判断
  • [][]:交错数组 (性能其次)
  • [,]:多维数组 (性能不好,最好以交错数组代替)
  • Array.Copy:不仅仅是将元素从一个数组复制到另一个
    • 方法还能正确处理内存重叠区域
    • 方法还能在复制每个数组元素时进行必要的类型转换 (如自动装箱拆箱、转型、加宽基元类型)
    • BlockCopy:比之更快,不过仅支持基元类型且不提供转型能力
  • 结构体中可使用 fixed 嵌入 (内联) 值类型一维 0 基数组,可在栈上分配(unsafe 代码)

# 第十七章 委托

  • 自定义委托会导致编辑器定义一个完整的类,该类继承 MulticastDelegate
    • 包含 4 个方法:构造器、invoke 方法、begininvoke 方法、endinvoke 方法
    • MulticastDelegate
      • _target (Object):当委托包装静态方法时该字段为 null,实例方法为回调方法要操作的实例对象
      • _methodPtr (IntPtr):clr 用来标识要回调的方法
      • _invocationList (Object):通常为 null,构造委托链时引用一个委托数组
  • 所有委托都有一个接受两个参数的构造器:一个是对象引用,另一个是引用了回调方法的整数
  • 方法地址 IntPtr 值:从 MethodDef 或 MemberRef 元数据 token 获得
  • 健壮性:一个系统对于参数变化的不敏感性
  • 可靠性:一个系统的正确性,固定一个参数可以产生稳定、可预测的输出
  • 委托 target 能取到?
  • 除非内置 Action、Func 满足不了需求,如:需要利用委托传引用、可变参数,否则不要自定义委托
  • 不引用成员变量、局部变量的匿名方法被编译为静态匿名函数并在第一次调用时缓存委托,性能更好,因为还不需要额外 this 参数

# 第十八章 定制特性

  • 定制特性其实是一个类型的实例
    • 将特性应用于目标元素时,语法类似于调用类的某个实例构造器
  • 特性类型本质上还是类,而类是可以应用特性的(attributeusage)
    • 不将 allowmultiple 明确设置为 true,特性就只能向选定目标元素应用一次
    • inherited:指出特性在应用于基类时,是否同时应用于派生类和重写的方法
    • 若未设置 attributeusage,则编译器和 clr 会假定该特性能应用于所有目标元素,每个目标元素最多一次,且可继承
  • 可想象为:它是类的实例,被序列化成驻留在元数据中的字节流,运行时可对元数据中字节进行反序列化,从而构造出类的实例
  • 如果只想判断目标是否应用特性,使用 isdefined 更高效:它不会构造特性对象,不会调用构造器,也不会设置字段和属性
    • getcustomattribute:会构造特性对象,每次调用都会构造指定特性类型的新实例
    • 不管调用哪个,内部都必须扫描托管模块元数据,执行字符串比较来定位指定的定制特性类,可考虑缓存结果
  • 两个特性实例的相互匹配:可在自己的定制特性类中重写 equals 来移除反射的使用,以提高性能(默认的 equals 会在类型一致时,再反射字段比较)
  • customattributedata:在查找特性的同时禁止执行特性类中的代码
  • conditionalattribute:条件特性类
    • 编译器如果发现向目标应用了该特性,仅具有指定预定义时才会在元数据中生成特性信息
    • 应用于方法时,若无预定义,则调用方法处会代码会直接去掉

# 第十九章 可空值类型

  • 操作可空值类型的速度慢于非可空值类型
  • &:按位与 (相同为 1,不同为 0)
  • |:按位或 (有 1 则 1,无 1 则 0)
  • 对于自己的值类型重载的操作符,编译器能自动调用其可空实例的
  • ??:如果左边为空,则返回右边操作数的值
  • 可空值类型装箱
    • 当 clr 对可空值类型实例进行装箱时,若其为空,则不装箱任何东西
    • 若不为空,则取出其中的值进行装箱
  • 通过可空值类型调用 gettype,会返回内部实际值类型

# 第二十一章 托管堆和垃圾回收

  • 托管堆:clr 维护 NextObjPtr 指针
  • 类型对象指针、同步块索引
    • 32 位:各自 32 位
    • 64 位:各自 64 位,因此每个对象额外 16 字节开销
  • NextObjPtr:在此放入新对象,初始化对象后,指针加上对象占用字节数得到新值
  • GC
    • 暂停所有线程,进入标记阶段
    • 遍历堆中所有对象,将同步块索引字段中的一位设为 0
    • 检查所有活动根,查看其引用对象
    • 任何根若引用了堆上对象,则标记该对象:同步块索引中的位设为 1,被标记后,继续检查该对象中的根,若其中对象被标记则不重新检测
    • 标记完成后,进入压缩阶段,压缩存活对象使其占用连续空间(大对象堆默认不压缩)
      • 提升将来访问性能
      • 解决堆内存空间碎片化问题
    • 修改指针指向新地址 (所以使用指针需要 fixed)
  • 分为三代:0、1、2 代,每代容量会自动计算,运行时也会进行调节,根据应用程序要求的内存负载自动优化
  • GC 触发条件
    • 手动调用 GC(不推荐)
    • 系统报告内存过低
    • CLR 卸载 AppDomain
    • CLR 正在关闭
  • 大对象:超过 85000 字节,处于第二代,默认不压缩(可能会导致内存碎片)
  • 分为 工作站 服务器 两种 GC 模式
  • 子模式
    • 并发(默认):内存足够的情况下,可能不压缩内存,消耗内存通常比非并发更多
    • 非并发
  • 手动调用回收(不推荐),会导致代的预算发生调整,最好让程序根据应用程序行为调整各代预算。
  • Finalize:终结器,实现其的对象成为垃圾后,在垃圾回收完毕后才调用
    • 可终结对象在垃圾回收时必须存活,导致被提升到下一代,临时复活后会活得比正常时间长,所以一定程度会增加内存消耗
    • 执行时间、顺序控制不了(不要访问另外实现了终结器的对象)
    • clr 用一个专用的、高优先级线程执行终结器方法避免死锁(因此若有终结器方法阻塞、等待信号量会导致线程无法再调用其它终结器 -> 内存泄露)
  • 终结器内部原理
    • 创建对象实例时,若发现对象定义了终结器方法,则加入终结器列表
    • 垃圾回收时,扫描到回收对象处于终结器列表,则移除列表并添加至 f-reachable 队列
    • 一个特殊的高优先级线程专门调用 F-reachable (可达的) 队列中的终结器方法(因此不要对执行代码的线程做出任何假设)
    • reachable (可达的) 使其指向对象保持可达(复活)—— 标记时,将同时递归标记对象引用的对象,所有引用对象也将保持复活
    • 标记完成后,复活对象也会被提升到老的一代(并不理想)
    • 最后,特殊的终结器线程清空 freachable 队列,执行每个终结器方法
    • 下一次对老一代进行垃圾回收时,才能发现终结器对象成为真正的垃圾
    • 所以,回收一个终结器对象需要不止一次垃圾回收,甚至也不止两次
  • 创建封装了本机资源的托管类型时,应该先从 SafeHandle 派生出一个类
  • C# 内置的托管资源操作类,如果不调用 dispose,就得等到它被垃圾回收通过终结器释放资源了
  • StreamWriter:会将数据缓存在自己的内存缓冲区,缓冲区满时才会将对象数据写入 Stream
    • 该类没有实现终结器,不显式调用 dispose 会导致数据丢失
  • 如果一个类要包装可能很大的本机资源,可使用对应方法提示垃圾回收器实际情况以便处理:
    • GC.AddMemoryPressure
    • GC.RemoveMemoryPressure
  • 包装数量有限的本机资源:HandleCollector,计数太大就强制回收
  • clr 为每个 appdomain 都提供了一个 GC 句柄表
    • 对托管堆对象一个引用
    • 指出如何控制或监视对象的标记
  • GCHandle(结构体)用于 添加或删除 GC 句柄表记录项
    • weak:可检测垃圾回收器在什么时候判定该对象在应用程序代码中不可达
    • weaktrackresurrection:同上,不过对象终结器 (若有的话) 已执行,内存已回收
    • normal:即使应用程序没有根引用该对象,该对象也必须留在内存
    • Pinned:即使应用程序没有根引用该对象,该对象也必须留在内存,且发生垃圾回收时,该对象内存不能压缩(需要将内存地址交给本机代码时,这个功能比较好用)
    • 注 1:使用 clr 的 P/Invoke 机制调用方法时,clr 会自动帮忙固定实参,并在本机方法返回时解除固定。
    • 注 2:只有将托管对象指针传递给本机代码,且本机代码方法返回后仍需使用该对象时,才需要显式使用 GCHandle 类型
  • fixed
    • 比分配一个固定 GC 句柄高效很多
    • 编译器在局部变量上生成一个特殊 已固定 标志,垃圾回收期间检测根内容,若根不为空则知道在压缩阶段不要移动变量引用对象
  • WeakReference
    • 其实是包装了一个 GCHandle 实例的面向对象包装器
    • 实例必须在堆上分配,比 GCHandle 实例更重
  • ConditionalWeakTable
    • 内部存储了对作为 key 传递对象的弱引用(一个 WeakReference 对象),保证不会因为表的存在而使对象 被迫 存活
    • 保证只要 key 所标识的对象在内存,值就肯定在内存中
    • 可以实现类似 xaml 依赖属性 机制

# 第二十二章 CLR 寄宿和 AppDomain

  • AppDomain
    • clr 初始化时会创建第一个默认 appdomain,只有进程结束才会被销毁
    • 相互之间不能直接访问
      • 相互有清晰的分隔和边界,容易单独卸载而不影响其它 appdomain
      • 想要相互访问对象,必须 按引用封送 (marshal-by-reference),或 按值封送 (marshal-by-value)
    • 可以被卸载:不支持卸载程序集,但是能卸载包含程序集的 appdomain
    • 可以单独保护:可以设置运行权限
    • 可以单独配置:appdomain 创建后会关联一组配置设置(涉及搜索路径、版本绑定重定向、卷影复制及加载器优化)
  • windows 每个应用程序都在自己的进程地址 (虚拟地址) 空间运行,即进程隔离
    • 创建进程开销很大,如需要大量内存虚拟化进程地址空间
    • appdomain 同样提供了清晰隔离(保护、配置和终止每一个应用所需的隔离),使得可以提供一个进程运行多个托管程序
  • Loader 堆
    • 每个 appdomain 都有自己的 loader 堆,每个 loader 堆都记录了自 appdomain 创建以来已访问过的哪些类型
    • loader 堆中每个类型都有一个方法表
    • 方法表中的每个记录项都指向 jit 编译的本机代码 (前提是方法执行过)
  • 进程
    • appdomain 1
      • loader 堆
      • 程序集
    • appdomain 2
      • loader 堆
      • 程序集
    • appdomain 中立 的程序集
      • loader 堆
      • 程序集:MSCorLib.dll
  • jit 编译的代码不共享,每个 appdomain 都有一份
  • 多个 appdomain 使用的类型在每个 appdomain 都有一组静态字段
  • appdomain 中立的程序集:以 中立 方式加载的程序集会共享,不过以 中立 方式加载的程序集,永远不能卸载,回收其资源的唯一方法是终止进程
  • 跨越 appdomain 边界访问对象(跨域?)
    • 同步执行的:一个 appdomain 方法执行完毕,才能执行另一个的方法,不能多个 appdomain 方法并发执行
    • 按引用封送(继承 marshalbyrefobject)
      • 生成代理类型,并创建代理类型实例
      • 调用方法时,切换至另一个 appdomain 执行
      • 实例字段通过反射设置或获取
      • 使用 租约管理器 保持原始对象存活
      • 性能较差
      • 应该避免静态成员(总是在调用 appdomain 的上下文访问)
    • 按值封送(继承普通 object)
      • 标记 Serializable 特性
      • clr 将对象的实例字段序列化成一个字节数组
      • 从源 appdomain 复制到目标 appdomain
      • 反序列化字节数组,这会强制 clr 将定义了 被反序列化类型 的程序集加载到目标 appdomain (若未加载)
      • 创建对象实例,并利用字节数组中的值初始化对象字段
      • 调用方法时,由于是真实对象,因此不会发生 appdomain 线程切换(卸载源 appdomain 也不再影响该对象)
  • 卸载 appdomain:步骤
  • 监视 appdomain
    • appdomain 静态 MonitoringEnabled:设置为 true 后便不能关闭
    • 开启后,可查询(只保证在上一次垃圾回收时是准确的):
    • MonitoringSurvivedProcessMemorySize:当前 clr 所有 appdomain 使用的字节数
    • MonitoringTotalAllocatedMemorySize:特定 appdomain 已分配字节数
    • MonitoringSurvivedMemorySize:特定 appdomain 当前正在使用的字节数
    • MonitoringTotalProcessorTime:特定 appdomain 的 cpu 占用率
  • appdomain firstchance 异常通知
    • 每个 appdomain 都可关联一组回调方法,clr 开始查找 appdomain 中的 catch 块时被调用
    • 可利用此做日志记录操作
  • 代码运行时会访问其它类型,引用另一个程序集中类型时,clr 会定位所需程序集,并将其加载到同一个 appdomain 中
  • 关闭进程:Environment.Exit

# 第二十三章 程序集加载和反射

  • clr 不提供卸载单独程序集的能力
  • 程序集加载
    • 引用另一个程序集中类型时,clr 会定位所需程序集,并将其加载到同一个 appdomain 中
    • clr 内部也是调用 assembly 静态 load 方法
    • appdomain 也提供了 load 方法
      • 不过是实例方法
      • clr 通过发出调用的 appdomain 设置定位和加载(可能找不到)
      • 存在问题,应该避免
    • loadfrom
      • 首先调用 assemblyname 的 静态 getassemblyname 方法打开指定文件
      • 找到 assemblyref 元数据表的记录项,提取程序集标识信息
      • 以 assemblyname 形式返回(并关闭文件)
      • 随后 loadfrom 方法内部调用 assembly 静态 load 方法,将 assemblyname 传入
      • clr 应用版本绑定重定向策略,并在各个位置查找匹配程序集
      • 若找到匹配程序集则加载,并返回代表程序集的 assembly 对象
      • 若没有找到,则 loadfrom 会加载传递路径中的程序集
      • 若已加载具有相同标识程序集,则会直接返回代表程序集的 assembly 对象
      • 注 1:loadfrom 允许传入 url
      • 注 2:由于一台机器可能存在相同标识的多个程序集,且 loadfrom 会在内部调用 load,因此 clr 可能加载的不是指定文件
    • loadfile
      • 从任意路径加载程序集,而且可以将相同标识程序集多次加载到同一 appdomain
      • clr 不会自动解析任何依赖性问题
      • 代码必须向 appdomain 的 assemblyresolve 事件登记,并让事件回调显式加载依赖程序集
  • 若代码只想反射分析程序集元数据
    • 使用 assembly reflectiononlyfrom 或 reflectiononlyload(少见)
    • 使用上述方法加载程序集时,clr 禁止程序集中任何代码运行
    • 依赖:reflectiononlyassemblyresolve 事件
  • 在 appdomain 的 assemblyresolve 事件登记后,可以手动处理依赖程序集加载
  • 反射
    • 严重依赖字符串
    • 编译时无法保证类型安全性
    • 速度较慢
      • 字符串名称标识类型及成员
      • 使用 reflection 命名空间中类型扫描程序集元数据时,反射机制会不停地执行字符串搜索
      • 通常字符串搜索不区分大小写,因此进一步影响速度
      • 反射调用成员(方法)时,首先必须将实参打包成数组,然后在内部将实参解包到线程栈上
      • 在调用方法前,还必须检查实参是否有正确的数据类型
  • 因此,晚期绑定不推荐用反射调用,可以:
    • 让类型从已知基类型派生,运行时构造派生类型实例,调用虚方法
    • 让类型实现已知接口,在运行时构造实例,再调用接口方法 (推荐)
    • 这样访问对象成员可以获得更好的性能,并确保编译时的安全性
  • 在一个 appdomain 中,每个类型只会有一个 type 对象
  • 反射方法字符串为这些名称定义了 巴克斯 - 诺尔范式(Backus-Naurform,BMF) 语法
  • typeof(早期绑定) 获取类型比 gettype(晚期绑定) 更快
  • Type 对象是轻量级引用,更多信息必须通过 gettypeinfo (不过性能更低) 获取 typeinfo(其也可以转型为 type)
  • 反射构造类型实例
    • Activator.CreateInstance
      • 运行在不调用值类型构造器的情况下创建值类型的实例
    • Activator.CreateInstanceFrom
    • AppDomain.CreateInstance
    • AppDomain.CreateInstanceAndUnwrap
    • AppDomain.CreateInstanceFrom
    • AppDomain.CreateInstanceFromAndUnwrap
    • Reflection.ConstructorInfo:Type 对象引用可获取构造方法信息,调用后构造类型实例并调用构造器
    • 创建数组类型:Array.CreateInstance
    • 创建委托:MethodInfo.CreateDelegate
    • 构造泛型类型实例首先要获取开放类型的引用,然后调用 MakeGenericType 传递生成封闭类型参数
  • 反射类型成员:MemberInfo (抽象基类)
    • TypeInfo
    • FieldInfo
    • MethodBase
      • ConstructorInfo
      • MethodInfo
    • PropertyInfo
    • EventInfo
  • 反射时传引用
    • 类型 &(巴克斯 - 诺尔范式)
    • type.makebyreftype
  • 若为了提高性能,缓存 Type 和 MemberInfo 派生对象会消耗更多内存
    • clr 内部用更精简的方式表示这种信息,clr 不需要这些大对象就,能运行,之所以创建这些对象是为了方便开发人员
    • 精简方式:
      • 可以使用运行时句柄代替对象以减小内存
      • RuntimeTypeHandle
      • RuntimeFieleHandle
      • RuntimeMethodHandle
      • 上述都是值类型,只包含一个 IntPtr 字段,引用 appdomain 的 loader 堆中一个类型、字段或方法
    • 使用方法:
      • type.gettypehandle(gettypefromhandle 转回去)
      • fieldinfo.fieldhandle(转换同上)
      • methodinfo.methodhandle(转换同上)
      • 获得 handle 后原对象可被释放(猜测?存疑)

# 第二十四章 运行时序列化

  • 类型默认是不可序列化的,必须应用定制特性:Serializable
    • 特性可以应用于 引用类型、值类型、枚举和委托类型
    • 枚举和委托默认总是可序列化(所以不必显式调用)
    • 该特性不会被派生类继承
    • 注:自动属性序列化会有问题,因为其字段值由编译器自动生成,名称每次编译可能不同,因此在允许序列化的类型中不要用
  • 序列化会读取对象所有实例字段,包括私有
    • NonSerializable 特性可以标记指定字段不被序列化
  • 序列化、反序列化可允许自动调用方法
    • OnSerializing 特性:序列化前首先调用
    • OnSerialized 特性:序列化完毕调用
    • OnDeserializing 特性:反序列化前首先调用
    • OnDeserialized 特性:反序列化完毕调用
    • 定义的方法必须获取一个 StreamingContext 参数
  • 反序列化类型存在新增字段会报错
    • 可以为新增字段应用 OptionalField 特性
  • System.Runtime.Serialization.Formatters.Binary:二进制序列化
    • 其实现 System.Runtime.Serialization.IFormatter 接口
    • 默认输出程序集完整标识:文件名、版本号、语言文化、公钥信息
      • 反序列化时,通过 assembly load 确保程序集已加载
      • 查找匹配类型(找不到则抛出异常)
      • 找到则创建实例,并通过反射用流数据初始化字段(若不完全匹配则抛出异常,可选 OptionalField 忽略)
  • 格式化器如何序列化
    • 调用 FormatterServices 提供的静态方法
    • GetSerializableMembers:反射获取未标记 NonSerualized 的实例字段
    • 反射获取值
    • 写入程序集标识和类型完整名称
    • 遍历反射获取的数组,写入每个成员名称和值
  • 格式化器如何反序列化
    • 从流中读取程序集和完整类型名称
    • 确保程序集加载
    • gettypefromassembly 获取反序列化对象类型
    • 调用 FormatterServices 提供的方法
    • GetUnInitializedObject:构造对象,但不调用构造方法,对象所有字段初始化为 0 或 null
    • GetSerializableMembers:反射获取字段信息
    • 根据流中信息初始化值的数组
    • 调用 PoulateObjectMembers 方法反射初始化值
  • 为提高更多操作,并避免反射开销
    • 可以实现 ISerializable 接口,该接口会由格式化器使用
    • 若一个类型实现了该接口,格式化器将忽略前面说的定制特性
    • 实现接口的类型需要定义带两个参数的特殊构造器:name (SerializationInfo info,StreamingContext context),这个构造器在反序列化时被调用
  • 类型序列化为不同类型以及对象反序列化为不同对象
    • 序列化单例(贪婪模式),并保证反序列化后也只有一个:实现的 ISerializable 接口方法 序列化实现 IObjectReference 的类型,该类型的方法直接返回单例对象
    • 在反序列化时,会调用其静态构造方法构成其单例对象
    • 反序列化时,若发现类型实现了 IObjectReference 接口,会调用其 GetRealObject 方法
  • 序列化代理
    • 必须实现 ISerializationSrurogate 接口
    • 允许开发人员序列化最初没有设计成要序列化的类型
    • 运行开发人员提供一种方式将类型一个版本映射到另一个版本
    • 通过 SurrogateSelector 在 IFormatter (格式化器) 注册
  • 反序列化和序列化时不同类型
    • SerializationBinder
    • 可以重写 类型
      • BindToType
      • BindToName
    • 继承 SerializationBinder 实现自己的类,然后在格式化器注册
  • Xml 序列化
    • XmlSerializer
    • DataContractSerializer
  • 可以通过二进制序列化简单实现对象的 深拷贝