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

.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
using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication5
{
    
class Program
    {
        
unsafe static void Main(string[] args)
        {
            
int a = 1;
            
int* addInt = &a;
            Console.WriteLine(
"指针地址为:{0},内容为{1}。", (uint)addInt, addInt->ToString());
            
byte b = 2;
            
byte* addByte = &b;
            Console.WriteLine(
"指针地址为:{0},内容为{1}。", (uint)addByte, addByte->ToString());
            
decimal c = 3m;
            
decimal* addDecimal = &c;
            Console.WriteLine(
"指针地址为:{0},内容为{1}。", (uint)addDecimal, addDecimal->ToString());
            
bool d = true;
            
bool* addBool = &d;
            Console.WriteLine(
"指针地址为:{0},内容为{1}。", (uint)addBool, addBool->ToString());
        }
    }
}

  运行结果如下:

 

 

这个程序很好的表明了值类型在地址中是如何进行存储的。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的情况,看下面的示例程序。

ContractedBlock.gif Code
using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication1
{
    
class Program
    {
        
static void Main(string[] args)
        {
            
//使用new声明值类型
            int intA = new int();
            intA 
= 2;
            Console.WriteLine(intA.ToString());
            
//声明结构体时,并不一定需要new操作符
            StrTest strTest;
            strTest.intA 
= 123;
            Console.WriteLine(strTest.intA.ToString());
            
//引用类型必须用new实例化
            ClaTest claTest = new ClaTest();
            claTest.intA 
= 123;
            Console.WriteLine(claTest.intA.ToString());


        }

    }

    
struct StrTest
    {
        
public int intA;
    }

    
class ClaTest
    {
        
public int intA;
    }
}

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

3.2.3       调用构造函数

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

转载于:https://www.cnblogs.com/youngtsinghua/archive/2008/12/15/1355171.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值