类成员初始化顺序

一般初始化顺序:

  1. 子类静态字段内联
  2. 子类静态构造
  3. 子类实例字段内联
  4. 父类静态字段内联
  5. 父类静态构造
  6. 父类实例字段内联
  7. 父类实例构造
  8. 子类实例构造

原则就是:先内联后构造;先静态后实例;先子类后父类(除了实例构造器)。

方法

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表。

文章推荐1文章推荐2

自己实现虚方法

用静态方法就可以实现,将对象自己作为参数传入静态方法,然后根据其type来switch-case就可以简单实现一个。

总的来说,虚方法这么复杂的内部机制就是为了实现多态。

字段

常量,const关键字,是指值从不变化的符号,它的值必须能在编译时确定,最后会在元数据中嵌入。它总是隐式static的。

const 与 static readonly 的区别是,const要求必须能在编译时确定,readonly只是后续不能修改,是可以跟运行时确定的 = new A()的。

C#关键字 含义 说明
默认 实例字段 该字段只与对象的一个实例关联,而不是与类型本身关联
static 静态字段 该字段是类型状态的一部分,而不是对象状态的一部分
readonly 只读字段 该字段只能由一个构造器方法中的代码写入(但是可以通过反射修改)

属性

属性的本质是方法(get、set访问器),是对字段的封装。C#提供{get;set;}语法糖,编译时自动实现创建一个字段。

无参属性:我们常规说的属性。

有参属性:get、set访问器接受一个或多个参数,就叫有参属性。有参属性一般用处是索引器,索引器的实现是通过对this[]操作符进行重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义
public class Students
{
private string[] name = new string[10];

//索引器必须以this关键字定义,其实这个this就是类实例化之后的对象
public string this[int index]
{
get { return name[index]; }
set { name[index] = value; }
}
}

// 使用
...
Console.WriteLine(Students[0]);

事件

具体见《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)。