CLR-via-CSharp-04 第6章 类型和成员基础

类型的各种成员

包括了:

  • 常量
  • 字段
  • 实例构造器
  • 类型构造器
  • 方法
  • 操作符重载
  • 转换操作符
  • 属性
  • 事件
  • 嵌套类型

类型可见性

我们定义一个类型的时候经常在前面用public修饰,这代表了这个类型可以被该程序集以及其他程序集的代码都可见。实际上除了public以外,还有一个internal修饰符,代表着某个类型只可以被当前程序集使用。

当没有指定为public的时候,会被默认指定为internal。

友元程序集

使用System.Runtime.CompilerServices命名空间中的InternalsVisibleTo特性表明友元程序集。

1
2
3
4
5
6
using System;
using System.Runtime.CompilerServices;

[assembly:InternalsVisibleTo("Wintellect, PublicKey=12346dfsadfsda6f5123")]

internal sealed class SomeInternalType {}

像上面这么写,就是规定了名字为Wintellect,公钥为PublicKey的程序集为友元集合。

成员的可访问性

  • private
  • protected
  • public

没啥好说的了。

静态类

使用static修饰一个类,这个类不能被实例化,只能拥有静态成员。注意这个关键字不能用于值类型,因为CLR总是允许值类型实例化。

还有以下限制:

  • 静态类必须直接从System.Object派生
  • 静态类不能实现任何接口
  • 静态类只能定义静态成员
  • 静态类不能字段、方法参数或局部变量
1
2
3
4
5
6
7
8
9
public static class StaticClass
{
public static UInt32 UIntValue;

public static void Func()
{
Console.WriteLine(UIntValue);
}
}

使用ILDasm可以看到一个静态类实际上是一个抽象(意味着不可被实例化)密封(意味着不可作为基类)类,而且看不到实例构造器.ctor方法。

分部类、结构和接口

分部类的关键字是partial,意味着一个类、结构或者接口的定义可以被分散到不止一个源代码中去。

当这些文件被编译到一起的时候,编译器会合并代码,最终运行的CLR对partial是一无所知的。

组件、多态和版本控制

CLR如何调用虚方法、属性和事件

IL中有两个指令可以调用一个方法:

  • call 可以调用静态方法、实例方法和虚方法。call假定了调用的对象不为null,常 用以非虚方式调用虚方法。
  • callvirt 不能调用静态方法,可以调用实例方法和虚方法。CLR会调查发出调用的对象的实际类型,然后以多态的方式去调用方法。为了能够确定类型(通过GetType()),所以调用的对象绝对不能是null,否则会抛出NullReferenceException异常。由于要做这个额外的检查,所以执行速度要比call指令慢。

看一下几种意想不到的情况。

为什么有时候对一个非虚方法要调callvir

1
2
3
4
5
6
7
8
9
public class Program
{
public Int32 GetFive() { retur -5; }
public static void Main()
{
Program p = null;
Int32 x = p.GetFive(); // NullReferenceException
}
}

在这里为啥不直接生成call命令呢?因为GetFive()这个方法里面没有用到任何的成员,如果使用call,当实例是null的时候,内部是不会抛空引用异常的。而使用了callvir就可以抛出空引用异常。

为什么有时候又要用call

1
2
3
4
5
6
7
class SomeClass
{
public override String ToString()
{
return base.ToString();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.method public hidebysig virtual instance string 
ToString() cil managed
{
// 代码大小 12 (0xc)
.maxstack 1
.locals init ([0] string CS$1$0000)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: call instance string [mscorlib]System.Object::ToString()
IL_0007: stloc.0
IL_0008: br.s IL_000a
IL_000a: ldloc.0
IL_000b: ret
} // end of method Program::ToString

可以看到SomeClass.ToString()里又调用了base.ToString()。里面这句base.ToString()的调用是用call指令调用的。这是为了错误的避免递归调用。

不要误解为IL后面跟着基类的名字才会用call。假如有外部调用SomeClass.ToString()方法,其IL语句后面也是Object。

1
2
SomeClass s = new SomeClass();
s.ToString();
1
IL_0008:  callvirt   instance string [mscorlib]System.Object::ToString()

另外,值类型也倾向于使用call。有两个原因:

  1. 值类型都是sealed的,不用考虑多态
  2. 值类型不可能是空引用,不需要验证是否为空引用

设计类的时候尽量减少虚函数

虚函数都需要使用callvir,速度比较慢,性能比较低,能少一些就少一些。

尽可能定义封闭类

封闭类中的虚函数会被call调用,这些虚函数会被「特化(Specialization)」。

由于封闭类不用考虑多态,会被编译器做一次优化,使得调用封闭类中的虚函数的时候使用call就行了,可以提高性能。

Buy Me A Coffee / 捐一杯咖啡的钱
分享这篇文章~
0%
//