CLR-via-CSharp-02 第4章 类型基础

所有类型都从System.Object派生

System.Object的几个方法

运行时要求每个类型最终都从System.Object类型派生。

System.Object提供了4个public实例方法:

  • Equals
  • GetHashCode 当某个类型的对象要在哈希表集合汇总作为key使用,就应该重写这个方法
  • ToString 默认返回类型的完整名称(this.GetType().FullName)
  • GetType 是一个非虚方法,这是为了避免类重写该方法

同时,System.Object还有2个protected方法:

  • MemberwiseClone 非虚方法,创建于this对象的实例完全一致的实例(浅复制),并返回新实例的引用
  • Finalize 虚方法,在GC的时候,回收该实例之前,会被调用

new操作符

CLR要求所有对象都用new操作符创建。当使用new操作符时,CLR做了以下事情:

  1. 计算类型及其所有基类中定义的所有实例字段需要的字节数,除了实例字段以外,还要计算一些额外的成员(overhead成员),包括类型对象指针同步块索引
  2. 托管堆中分配类型要求的字节数,从而分配对象的内存。
  3. 初始化对象的类型对象指针同步块索引
  4. 调用类型的实例构造器(.ctor)

new执行了所有以上操作之后,返回一个引用。

类型转换

推荐使用isas来判断类型转换是否可行,以及转换类型。

CLR允许将对象转换为它的实际类型或者它的任何基类类型。

当一个对象要转换成基类类型时,可以隐式转换,但是如果开发者想要把基类转成派生类,就一定要显式转换。

命名空间和程序集

命名空间的作用在于逻辑分组,类似于Java中的包名。

使用using关键字可以让程序员少打很多字。当编辑器在查找一个类型的时候,会现在源代码或者引用的任何程序集(通过/reference编辑器开关实现)中找指定名称的类型,找不到后就会把using的命名空间加到类型名字前边。

CLR对命名空间一无所知,访问一个类型的时候,CLR需要知道的是类的完整名称(命名空间+类名)。

歧义

在不同的命名空间定义了同一个名称的类型,又在使用的时候分别using了这几个命名空间,那么使用这个类的时候就要打入全称。

命名空间别名

1
using WintellectWidget = Wintellect.Widget;

可以只引入命名空间中部分类,且赋予其别称。

命名空间与程序集的关系

同一个命名空间中的类型可能在不同程序集中实现,例如System.IO.FileStream在MSCorLib.dll程序集中实现,而System.IO.FileSystemWatcher类型在System.dll程序集中实现。

当然反过来,一个程序集可以定义多个命名空间。

查询api文档的时候能够查到类型所在的程序集。

运行时的相互关系

本节探讨类型、对象、线程栈和托管堆在运行时的相互关系,并解释静态方法、实例方法和虚方法的区别。

一个方法运行时线程栈的情况

这里以一个例子来做说明。

下图展示了已加载CLR的一个Windows进程。一个进程可能有多个线程,每个线程创建时会分配到1MB的空间。栈空间用于向方法传递实参,方法内部定义的局部函数变量也在栈上。栈从高位内存向低位内存构建。下图中的阴影部分是一些已执行过的代码使用的数据(可无视)。假定线程即将执行M1方法。

序幕(prologue)代码在方法开始做工作之前对其进行初始化,M1方法执行时,它的序幕代码在线程栈上分配局部变量name。之后栈上的情况如下图:

接着,M1调用M2方法,这需要把局部变量name作为实参传递——把name局部变量中的地址压入栈。除了实参之外,调用方法的时候还会把返回地址压入栈,以便在M2执行完之后继续M1的工作。如下图:

这之后M2的序幕代码运行,在线程栈中分配lengthtally的内存,然后执行M2的内部代码。

执行完M2的内部代码之后,CPU的指令指针被设置成栈中的返回地址,M2的栈帧(可以简单地理解M2所占用的栈空间)被unwind(反绑,展开),恢复成4-2的样子。

然后M1继续执行。

栈帧 执行线程的过程中,进行的每个方法调用都会在调用栈中创建并压入一个StackFrame。

Explain the concept of a stack frame in a nutshellIf I remember correctly, the function return address is pushed onto the stack first, then the arguments and space for local variables. Together, they make the “frame,” although this is likely architecture-dependent. The processor knows how many bytes are in each frame and moves the stack pointer accordingly as frames are pushed and popped off the stack.

unwind意味着当一个方法执行完毕的时候把这个方法在栈上的数据都pop掉。

方法、虚方法、静态方法

假设有代码:

1
2
3
4
5
6
7
8
9
10
11
internal class Employee
{
public Int32 GetYearEmployed() {...}
public virtual string GetProgressReport() {...}
public static Employee Lookup(string name) {...}
}

internal sealed class Manager : Employee
{
public override string GetProgressReport() {...}
}

现在我们要执行M3方法,来观察线程栈和托管堆的情况。

1
2
3
4
5
6
7
8
9
void M3()
{
Employee e;
int32 year;
e = new Manager();
e = Employee.Lookup("xxx");
year = e.GetYearsEmployed();
e.GetProgressReport();
}

初始状态如下图

JIT编译器在将M3的IL代码转成本机CPU指令时,会注意到M3内部饮用到的所有类型,CLR要确保定义了这些类型的程序集都已经被加载。

由于int32和string对象是很常用的,我们可以假定它们在执行M3之前已经被加载了。

下图中xxx类型对象在的位置应该是加载堆(Load Heap),不是GC堆。在M3执行之前就要保证这些类型对象都已经被加载好。

接下来开始执行M3,首先CLR会调用序幕代码,把局部变量初始化为null或0。注意这里是隐式初始化。如果后面的代码视图访问尚未显示初始化的局部变量,C#会报错:使用了未赋值的局部对象。
在运行时,CLR总是知道对象的类型是什么——通过调用GetType就可以知道对象的确切类型。

图4-7中分配了局部变量。

之后调用了一个new,造成了对象的生成,如图4-8。

接下来调用了静态方法,返回一个Manager对象。

然后调用了一个非虚方法GetYearsEmployed。调用非虚实例方法时,JIT编译器找到与发出调用的那个类型(Employee持有了该对象,因故这里就是Employee)对应的类型对象,来找这个方法。如果在本类型对象中找不到该方法,就一路回溯基类,直到找到该方法。在该类型对象的方发表中查找该方法的记录项,对其进行JIT编译吗,然后运行,最后把结果返回到局部变量year中。

接下来是重点了,代码调用了一个虚实例方法GetProgressReport。首先会在Manager对象的类型对象指针所指向的Manager类型对象中直接查找虚方法的新实现。

类型对象

类型对象的类型都是System.Type。CLR开始在一个进程中运行时,会立即为MSCorLib.dll中定义的System.Type类型创建一个特殊的类型对象。任何类型对象都是System.Type的实例,因此上面例子提到的EmployeeManager类型对象的类型对象指针会指向Type类型对象。

另外,这个特殊的Type类型的类型对象的类型指针对象,指向自身。

我们调用GetType方法,返回的都是对象的真实类型。

用自己的话总结一下上面的道理

假设有一个类Person

1
2
3
4
5
6
7
8
public class Person
{
private string _name;
public string GetName
{
return _name;
}
}

当我们new出一个person实例之后,内存里是这样的:

Person e指向了GC堆中的Person对象。Person对象中的类型对象指针指向了Person类型对象(Person类型对象如字面上理解,本身也是个对象,它的类型是Type)。Person类型对象又指向了Type类型对象,这是一个特殊的类型对象,它的类型对象指针指向自己。

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