类型的各种成员
包括了:
- 常量
- 字段
- 实例构造器
- 类型构造器
- 方法
- 操作符重载
- 转换操作符
- 属性
- 事件
- 嵌套类型
类型可见性
我们定义一个类型的时候经常在前面用public
修饰,这代表了这个类型可以被该程序集以及其他程序集的代码都可见。实际上除了public
以外,还有一个internal
修饰符,代表着某个类型只可以被当前程序集使用。
当没有指定为public的时候,会被默认指定为internal。
友元程序集
使用System.Runtime.CompilerServices
命名空间中的InternalsVisibleTo
特性表明友元程序集。
1 | using System; |
像上面这么写,就是规定了名字为Wintellect,公钥为PublicKey的程序集为友元集合。
成员的可访问性
- private
- protected
- public
没啥好说的了。
静态类
使用static修饰一个类,这个类不能被实例化,只能拥有静态成员。注意这个关键字不能用于值类型,因为CLR总是允许值类型实例化。
还有以下限制:
- 静态类必须直接从
System.Object
派生 - 静态类不能实现任何接口
- 静态类只能定义静态成员
- 静态类不能字段、方法参数或局部变量
1 | public static class StaticClass |
使用ILDasm可以看到一个静态类实际上是一个抽象(意味着不可被实例化)密封(意味着不可作为基类)类,而且看不到实例构造器.ctor
方法。
分部类、结构和接口
分部类的关键字是partial
,意味着一个类、结构或者接口的定义可以被分散到不止一个源代码中去。
当这些文件被编译到一起的时候,编译器会合并代码,最终运行的CLR对partial是一无所知的。
组件、多态和版本控制
CLR如何调用虚方法、属性和事件
IL中有两个指令可以调用一个方法:
- call 可以调用静态方法、实例方法和虚方法。call假定了调用的对象不为null,常 用以非虚方式调用虚方法。
- callvirt 不能调用静态方法,可以调用实例方法和虚方法。CLR会调查发出调用的对象的实际类型,然后以多态的方式去调用方法。为了能够确定类型(通过
GetType()
),所以调用的对象绝对不能是null,否则会抛出NullReferenceException
异常。由于要做这个额外的检查,所以执行速度要比call指令慢。
看一下几种意想不到的情况。
为什么有时候对一个非虚方法要调callvir
:
1 | public class Program |
在这里为啥不直接生成call命令呢?因为GetFive()
这个方法里面没有用到任何的成员,如果使用call
,当实例是null的时候,内部是不会抛空引用异常的。而使用了callvir
就可以抛出空引用异常。
为什么有时候又要用call
:
1 | class SomeClass |
1 | .method public hidebysig virtual instance string |
可以看到SomeClass.ToString()
里又调用了base.ToString()
。里面这句base.ToString()
的调用是用call
指令调用的。这是为了错误的避免递归调用。
不要误解为IL后面跟着基类的名字才会用call
。假如有外部调用SomeClass.ToString()
方法,其IL语句后面也是Object。
1 | SomeClass s = new SomeClass(); |
1 | IL_0008: callvirt instance string [mscorlib]System.Object::ToString() |
另外,值类型也倾向于使用call
。有两个原因:
- 值类型都是sealed的,不用考虑多态
- 值类型不可能是空引用,不需要验证是否为空引用
设计类的时候尽量减少虚函数
虚函数都需要使用callvir
,速度比较慢,性能比较低,能少一些就少一些。
尽可能定义封闭类
封闭类中的虚函数会被call调用,这些虚函数会被「特化(Specialization)」。
由于封闭类不用考虑多态,会被编译器做一次优化,使得调用封闭类中的虚函数的时候使用call
就行了,可以提高性能。