C#精要 - 类内成员篇
类成员初始化顺序
一般初始化顺序:
- 子类静态字段内联
- 子类静态构造
- 子类实例字段内联
- 父类静态字段内联
- 父类静态构造
- 父类实例字段内联
- 父类实例构造
- 子类实例构造
原则就是:先内联后构造;先静态后实例;先子类后父类(除了实例构造器)。
方法
abstract抽象方法、virtual方法、隐式实现接口方法,它们本质上、在IL代码层中,都是virtual方法。
IL提供两种方式去调用方法:
call:可用于调用实例方法、虚方法和静态方法。// 我个人测下来感觉只有静态方法是用call…
callvirt:可用于调用实例方法、虚方法。过程会比call复杂一些,事前会check null,执行时也会去查虚函数表。
非虚方式调用 call
call 调用的,是编译时确定的类型,也就是申明类型。
如果变量申明的类型没有对应的方法,就检查基类型来查找匹配方法。
虚方法调用 callvirt
callvirt 调用的,是运行时确定的类型,也就是变量指向对象的实际类型(new的类型)。
上面说的是结果,但如果深究过程,在IL代码层的话,多态方法全部都是 callvirt 最初父类的同一个方法。
c++版本 虚表、虚函数调用整体流程:
1.编译器发现一个类中有虚函数时,便为该类生成虚函数表,虚表各表项为指向对应虚函数的指针。父类虚函数地址在前,子类在后,按照声明顺序。
2.生成子类时,如果发现子类中函数重写了父类中的虚函数,则用子类虚函数的地址覆盖掉对应父类虚函数的地址。
3.虚函数调用过程:
查自己类型的虚函数表,找到对应位置的虚函数。
有覆盖:该指针指向子类函数,调用子类的函数。
无覆盖:调用父类自己的函数。
4.每一个有虚函数的类都有一个虚函数表(V-Tablle),每个这些类的对象都会生成一个指向虚函数表的指针。
c#版本 虚表、虚函数调用整体流程:
网上很难找到c#版本的虚函数实现,全只有猜测。我自己整理了一下,目前理解是:
可以确定的是,在IL代码层,多态方法全部都是 callvirt 最初父类的同一个方法。
因此估计JIT是根据 推上栈的实际变量指向的对象 + callvirt最初父类的同一个方法 来获取偏移量、再根据偏移量确定具体调用方法,内部则可能和c++一样是用V-Table。
那么最终结论就是,每个带有虚函数的类型,都会有一张V表;他们的每个子类,也有自己的V表,起始布局和父类一致,如果override了就替换自己的V表的方法指针;这些子类在堆上的对象,都有一个指针指向子类类型的V表。
自己实现虚方法
用静态方法就可以实现,将对象自己作为参数传入静态方法,然后根据其type来switch-case就可以简单实现一个。
总的来说,虚方法这么复杂的内部机制就是为了实现多态。
字段
常量,const关键字,是指值从不变化的符号,它的值必须能在编译时确定,最后会在元数据中嵌入。它总是隐式static的。
const 与 static readonly 的区别是,const要求必须能在编译时确定,readonly只是后续不能修改,是可以跟运行时确定的 = new A()
的。
C#关键字 | 含义 | 说明 |
---|---|---|
默认 | 实例字段 | 该字段只与对象的一个实例关联,而不是与类型本身关联 |
static | 静态字段 | 该字段是类型状态的一部分,而不是对象状态的一部分 |
readonly | 只读字段 | 该字段只能由一个构造器方法中的代码写入(但是可以通过反射修改) |
属性
属性的本质是方法(get、set访问器),是对字段的封装。C#提供{get;set;}
语法糖,编译时自动实现创建一个字段。
无参属性:我们常规说的属性。
有参属性:get、set访问器接受一个或多个参数,就叫有参属性。有参属性一般用处是索引器,索引器的实现是通过对this[]
操作符进行重载。
1 | // 定义 |
事件
具体见《C#精要 - 委托与事件篇》。
泛型
避免拆装箱的最佳选择。最好的例子就是 ArrayList ( Object[ ] ) => List< T >;Action< T > (T arg)、Func<in T, out TResult> (T arg)。
它可以用where约束。
- **逆变量(contravariant)**,意味着泛型类型参数可以从一个类更改为它的某个派生类。C#中用
in
来标记,只能出现在输入位置,比如入参。 - **协变量(covariant)**,意味着泛型类型参数可以从一个类更改为它的某个基类。C#中用
out
来标记,只能出现在输出位置,比如返回值。
这两个概念看着挺复杂,其实就是 Func<in T, out TResult> (T arg)。