随笔- 7  文章- 0  评论- 16 

 索引

1 简介

2 用户自定义类型(值类型和引用类型)

    --存储单元

    --内存布局

    --实例化

    --变量和GC roots

    --构造函数

    --继承(一种区分值类型和引用类型的方式)

    --相等性和散列值

    --(方法参数按值传递及变量分配)

    --'this' 指针

    --装箱和拆箱

    --非虚实例方法调用

    --虚方法调用

    --接口方法调用
     3 委托

    --基础方法调用的委托

    --委托链

 4 枚举

 5数组

 6泛型

简介:

。。。略去一些简要介绍。。。

.NET中所有类型主要分为以下两类,且所有类型都直接继承或者间接继承自System.Object (本身是引用类型):

--值类型

    用户自定义值类型(结构)

    枚举

--引用类型

    用户自定义类型(类)

    数组

    委托

区分值类型和引用类型,可以从它们的实例的分配位置(栈或者堆)--值类型分配在栈上,引用类型分配在堆上(一般而言),但值类型和引用类型在定义、行为、类型实例上是有本质区别的,编译器和CLR一起构造和维持了值类型和引用类型在编译时和运行时的区别。了解了CLR如何实现各种类型以及各种类型都是如何工作的,将对你有莫大的帮助。

2、用户自定义类型(值类型和引用类型)

      --内存位置

     值类型分配在栈上,主要目的是为了减少对GC堆的占用(值类型一般为小型但频繁使用的类型--INT CHAR等等),如若值类型分配在GC堆上,必然会增加内存分配,GC调用,向OS请求动态内存的次数,如此会影响程序的性能;引用类型分配在GC堆上。

--内存布局(按下图说明)

值类型的实例仅包含其字段的值,相比,引用类型的实例包含一些用于GC,同步、应用程序标识和类型信息的额外信息。这些额外的信息占用8字节内存(Syncblk #2和TypeHandle各占4字节)。值类型实例变量表示分配在栈上的值类型实例的开始地址(比如 int i =8 ,i就表示i中8的地址)。值类型变量的地址被称为托管指针

引用类型变量被称为对象引用,它指向引用类型值的开始位置再加上4字节(引用类型变量指向TypeHandle(Instance Fields加上4字节)),所有引用类型实例的起始位置都是4字节的同步块(Syncblk #2),它指向一个进程级的同步目录表,用于引用类型实例的同步访问。下面4字节(TypeHandle)包含该类型方法表的地址,相应的,方法表包含一个指针(EEClass),该指针包含该引用类型运行时的一些类型信息。

指向类型方法表的指针(TypeHandle)的存在使得该类型成为“自描述类型”,使得CLR能够在运行时确定引用类型实例的真正类型--类型转换、多态、反射便是由此(运行时类型)帮助实现的。相比而言,值类型实例便没有指向方法表的指针,仅仅是一块包含值类型中值得一块内存(没有同步块、TypeHandle信息,仅包含值),因此而言,值类型不是‘自描述类型’。

下面的图1显示了引用类型实例所包含的信息,左边的一块(FinalizerQueue---Other Oject Instance)是GC Root。

下面的图2图3(为.net 1.1布局,在2.0及以后版本中可能有所不同,仅供说明原理)展示了方法表及EEClass的详细信息,


 

 



实例化:

值类型实例在声明时创建,值类型没有默认构造函数。实例化时,值类型的所有字段被初始化为0(值类型字段)或者null(引用类型字段)。理论上,值类型实例的字段只有在用NEW操作符实例化以后才能访问或使用,但技术上这(NEW实例化)不是必须的,因为在值类型实例声明的时候字段已经被CLR给置0了(也算初始化吧,只不过是CLR做的),但有些语言(C#)规定:只有在显示赋值以后,或者用NEW操作符实例化变量以后,值类型实例的字段才可被访问,这就使得一些频繁使用的值类型实例化时不需要调用构造函数了(int i =5)。值类型可以有带参数的构造函数,可被显示调用以创建实例,如果调用带参构造函数,那么构造函数必须初始化值类型里的所有字段。

引用类型实例必须用NEW操作符实例化并且必须有一个默认构造函数或者有参构造函数。创建引用类型实例是,CLR首先初始化引用类型的所有字段,接着将调用默认构造函数或者有参构造函数.

变量和GC Roots

1 值类型变量直接表示在栈上的值类型实例的地址

2 引用值类型实例的变量被称为"托管指针",它是一个指向栈上值类型实例开始地址的指针

3 引用类型变量是一个指向堆中引用类型实例的指针

4 CPU寄存器可包含托管指针或对象引用

5 应用程序级的句柄表含有GC句柄,句柄包含指向内存中pinned Reference Type instances 的指针。这些句柄表也含有指向静态值类型实例的托管指针或指向静态引用类型实例的对象引用。

6 Thread Local Storage (TLS) 可能包含对象引用

7 终结可达队列包含引用类型的对象引用,这些引用不在以上变量中存在且finalize方法未被调用

CLR的垃圾回收器通过以上变量(也叫GC Roots)在垃圾回收时跟踪对象引用(用以确定哪些对象可以回收,哪些对象尚在使用)。如果一个引用类型的变量不在以上7种类型中,那么该对象实例将被考虑回收,如果引用类型实现了Finalize方法,那么该类型的对象引用将被加入终结可达队列,随即将在单独的Finalzer线程中调用其Finalize方法,一旦Finalizer被调用,相应对象实例将从GC堆中移除。

构造函数

正如上面所说,值类型不能并且也没有默认构造函数。值类型可包含有参构造函数且需用NEW操作符显式调用。构造函数,无论对于值类型还是引用类型,只不过是类型的一个实例方法(名字特殊点),它们被编译器隐式用来初始化类型的字段、做一些初始化操作,但实例化以后,编译器不允许你显式调用构造函数。但技术上,构造函数可以在实例化以后再次被调用(C#是不行了,别的语言是可以的 IL便行),这可以通过欺骗编译器,自己写调用构造函数的IL代码实现。你可以用ILDASM反编译程序,修改IL代码(多次调用构造函数),再编译成应用程序。通常你是不需要这么做的,如果实际编程中需要多次初始化一个类型实例,你可以定义一个公有方法来实现。

值类型和引用类型的构造函数的区别如下:引用类型的构造函数总是默认的调用父类的构造函数(到Object为止),原因是:CLR不会自动调用父类构造函数,因此调用父类构造函数便成了引用类型自己的责任(这里我的理解是:C#语言本身并不能看出对父类构造函数的调用,但编译生成的IL代码包含了对父类构造函数的调用--这显然是通过代码实现的,而不是CLR自动实现的)。值类型则无需显式调用构造函数,因为值类型无默认构造函数且值类型不能作为基类。对于引用类型,编译器会在生成的IL中自动加入调用父类构造函数的代码。

继承(区别值类型和引用类型)

值类型:

值类型继承自System.VlaueType, System.VlaueType继承自System.Object。CLR将继承自System.VlaueType的类型当作值类型对待;引用类型的继承层次中并不包括System.VlaueType类型。实际上,一些编译器不允许任何类型直接继承自System.VlaueType类型,而是提供间接继承的方式,并强制一些约束条件。对应于这些强制性的约束条件,编译器会为值类型产生相应合适的IL代码-----这是必要的,因为在IL层面,任何类型都能继承自System.VlaueType类型,甚至包括默认构造函数和错误的IL指令----这些都是不能用于值类型的(因为默认构造函数从来不会被调用)。

值类型不能作为其他类型的基类。确切的说,值类型不能作为引用类型的基类,因为值类型没有默认构造函数,而且值类型的内存布局和引用类型是不一样的。但为什么值类型不能作为其他值类型的基类呢?答案在于值类型实例的内存布局,.NET借助方法表实现运行时的多态(虚方法调用),值类型实例是不包含方法表的,所以CLR不能正确的实现虚方法调用(方法调用将在下部分讨论)。由于这个原因(无方法表).NET不能呢个为值类型提供运行时多态,没有运行时多态,从OO设计的观点看,继承性就是不完整的。所以所有的编译器(包括ILASM)将把继承自System.VlaueType的类型标记为sealed。所有标记为sealed的类型都不能作为基类----CLR在运行时加载类型的时候强制,如果加载类型的时候,CLR发现一个作为基类的类型被标记为sealed了,它就拒绝加载该类型,并抛出System.TypeLoadException的异常。


 posted on 2008-09-08 14:37 红泥 阅读(...) 评论(...) 编辑 收藏