.NET之我见系列 - 类型系统(上)

1.         概览

较之以往任何一种开发语言来说,.NET在类型系统上的创新设计都是无与伦比的。强大的通用类型系统CTSCommon Type System)奠定了整个.NET体系的基石。这套类型系统是贯穿于.NET Framework和各种中间语言之间的。因此需要从两个方面来理解.NET的类型系统。

总体来说,.NET的类型是一种完全的面向对象的类型。它由最底层的object类型开始,逐步扩展,上面再分支为值类型Value Type以及引用类型Reference Type。由值类型由分支出基础值类型、用户定义值类型以及枚举类型。由引用类型分支出自描述类型、指针类型、接口类型。由自描述类型又分支出类 类型、数组类型等。

下图展示了.NET的类型体系分支:

   

这是一基于自演化的体系,由一个根类型逐渐分支。其结构体系完全符合自然发展规律,符合面向对象的思想。这种思想早在中国古代经典著作中 就奠定了理论基础,在《易经》中提到这样的思想:太极生两仪、两仪生四象、四象生八卦、八卦演万物。 而.NET的类型体系正是符合这种发展的思想观。它所带来的优势是不言而喻的:

架构清晰

整个树形架构划分明确,便于程序的设计,便于理解。

通用性强

这种明确的类型系统有效的保证了.NET实现的多语言开发,中间语言转换,统一编译的特性。

便于检测

正是基于它清晰的架构,便于在程序出现错误时,按不同的类型需求检测错误。

扩展性好

统一的设计保证了类型的可扩展性。

2.         从源头说起

       前面已提到过,.NET类型系统全部来源于一个统一的基础即System.Object类型。它定义于.NET Framework下,在C#中对应的类型为object类型。.NET实行了一种语言架构分离的机制,它的基本类型并没有定义于语言中,而是内置 在.NET Framework内。这样的设计进一步保障了公共语言系统的成功及工作效率。而我们在语言中也可以方便的使用助记符来代替,例 如System.Object在语言中可使用object替换,System.Int32在语言中可使用int替换。值得一提的是,这种替换名字虽有细小的差别,但仍是基于基本类型的。因此并不像网上某些文章提到的会损失性能。因为其在代码编译之 前就会在MSIL中完成类型的转换。请看以下示例。

static void Main(string[] args)
{
    
int intA = 123;
    Console.WriteLine(intA.ToString());
    
}

  这段程序描述了一个int型变量的定义。当该脚本转换成IL后,其代码如下:

 

 其中红色区域为int型变量在转换后的类型,由此可见,它仍是.NET Framework中定义的基本类型。

System.Object中拥有几个最基本的方法,包括实例方法:

 

 静态方法:

 

这几个简单的方法为object所有的分支类型所共有。其中使用最普遍的就是ToString()方法。用于返回对象的字符串形式。在调试程序时,经常会运用它来判断当前对象是否正确。获得当前值。< /p>

EqualsReferenceEquals用于对对象的实例进行相等比较。

GetType:用于运行时获取对象的运行时类型。

GetHashCode用于获取对象的散列码。

除此之外还包括了MemberwishClone方法,用于实现对象实例的浅拷贝,它是base基类中的一个受保护级别的方法。

 

最后还有一个非常特殊的Finalize方法,用于垃圾回收时处理资源的清理工作。该方法无法在自定义类中显示重写。要实现它,只需为类定义析构函数即可。但要注 意,Finalize对系统的开销非常大,因此请尽量少的使用它。

3.         从内存结构谈起

以上简单的介绍了.NET类型系统的划分和设计基础。但要真正了解.NET类型的细节问题,就需要弄清楚类型在内存中的表现形式。因为类型最重要的作用,即它的核心价值就在于为应用程序的各个元素开辟相应的内存 空间,指定其运行的位置。合理分配的内存空间可保证程序稳定有序的运行,也是决定程序性能的一项硬指标。

.NET类型系统的设计源自JAVA,其数据在内存的存储区域被划分成两个不同的部分,堆栈区(Strack)和托管堆(Manage Heap),堆栈区用于存储值类型,而引用类型则依赖于托管堆。这个过程是这样进行的:

32位的操作系统上,当用户执行编译好的应用程序时,操作系统会在内存中为程序创建一个进程,同时为其分配4GB的内存空间(此空间是通过内存地址映射实现的虚拟空间),这块空间即为托管堆区,一般引用类型的实际数据都存储在此,而在堆栈上存储的则是引用类型的地址指针。

3.1值类型

而对于值类型来说,通常是存储于线程的堆栈上。堆栈是一种先进后出,并从高地址向低地址扩展的数据结构,它是一块连续的内存的区域。在系 统分配时会被指定大小。若存储的数据超出了这块指定区域就会发生“溢出”错误。下图表明了堆栈在内存中的存储结构。< /p>

 

这个概念非常重要,理解了这一点,在后面谈到数据类型转换时的重重问题就可以迎刃而解了。打一个不恰当的比喻来说,堆栈就好比酒店内的房 间,不同类型、不同数量的客人被安排在不同大小的房间内,有单间、双人间、三人间、豪华间还有总统套房。酒店前台会根据客人的不同需要进行分配。这里的酒 店前台好比堆栈中的地址指针。此指针指向堆栈中下一个自由地址空间。

下面的程序使用.NET的指针,定义了3int型变量,分别获取它们的内存地址和值,从结果可以看出,值类型的内存分配方式:

Code

  运行结果如下:

 

这个程序很好的表明了值类型在地址中是如何进行存储的。4个值从第1242220的高地址位开始一直向低地址位延伸。每次根据数据类型的不同分配不同长度的内存单元,用于存储所需数据。当然地址的起始位置是根据系统当 前的资源情况而分配的。

另外我们还需要了解的是值类型的作用域也有严格的规定。值类型的作用域被规范在一个代码块中,例如上面的例子程序 中,abcd四个变量的作用域就只在main主函数中存在,当程序运行到主函数的最后一个}符号时,四个变量被依次释放内存,这一操作是由系统自动完成的,并不需要人为去进行干预。< /p>

当然有些情况下值类型的作用域也被延伸。例如使用refout来按引用传递参数时,值类型的作用域则可被扩展到程序块之外。

3.2引用类型

说完值类型,让我们再回到引用类型上。首先要了解,为什么需要引用类型。实际上,相对于引用类型来说,值类型的执行效率要高得多,并且后 者的内存开销也要比引用类型小。那么是不是仅需要它就行啦?我们前面也谈到,类型的核心价值就是提高程序的性能,这样看来引用类型似乎是违背了这一原 则。

事实是,我们不仅需要引用类型,而且它的作用往往比值类型显得更加重要。因为值类型虽使用简便,但最大的缺点就是受到语句块作用域的限 制,并且只能存储一些小的数据类型,以至于使用上欠缺灵活性。而引用类型克服了这些缺点,首先是它的存储位置被分为两个区域。它在堆栈上声明并被分配空 间,但此空间存储的仅是实际数据在托管堆中的地址的引用。真正的数据被存储在托管堆中,托管堆的内存存放类似于堆栈,但它有一个专门的工具来负责内存的清 理工作,这个工具就是垃圾收集器GC。垃圾收集器会定期检查堆栈中的数据占用情况,若发现不用的对象(有一种算法来负责),或用户提出了申请,则开 动GC,回收内存中相应的资源。

引用类型使用运算符new进行创建,方法如下:

Test test = new Test();

Test是类型的名称,这里可以是用户自定义类型、也可以是系统内置类型。这行语句与普通的值类型定义相比仅是等号右边有所不同,但它本身包含 了以下几个步骤。

3.2.1       声明类型,在堆栈开辟内存空间

等号左边和值类型一样,首先指明了数据的类型为Test类型,此时编译器将会在同一命名空间下查找是否存在Test类型,若没有则在引 用中查找是否有using指向不同命名空间下的这一对象,若不存在则返回一个错误提示:“找不到类型或或命名空间名称”。当然这一步骤会在源代码编译前就 完成。但也有一种情况就是编译成功后,系统中注册的动链意外丢失,也会造成编译后的错误。

若类型存在,则根据此类型的需要在内存的堆栈区开辟空间。因此,即使是引用类型,仍然需要消耗堆栈区的空间。和值类型不同的时,此时, 堆栈空间中存储的不是引用类型的数据,而是引用类型在托管堆中的地址。

3.2.2       在托管堆开辟内存空间

当运行到new操作符时,系统开始在托管堆上分配内存,用于存放引用类型的实际数据。New不仅是用于创建对象,还有一个重要的作用就 是调用类构造函数。在IL中,new被newobj命名所定义,但new并不是为引用类型所独有的。值类型也有使用new的情况,看下面的示例程 序。

Code

  在此程序中,定义了一个int型的值类型,一个结构体,一个类。我们可以看出,在声明值类型时,也可以使用new操作符,也可以不使用 new。而声明引用类型时,必须使用new操作符,因为需要new为引用类型在托管堆中分配资源。但new并不为值类型在托管堆中开辟内存 区。

3.2.3       调用构造函数

new的最后一个作用就是调用类或结构体的构造函数。构造函数是与类名同名的方法成员,由类在初始化后自动运行,用来完成一些数据的初始化工 作。

posted on 2008-12-14 22:47  刑天  阅读(2783)  评论(35编辑  收藏  举报

导航