C# 类型详解

来源:互联网 发布:js 骰子的转动效果 编辑:程序博客网 时间:2024/05/01 19:36

前言:

      C#中的类型有两种:值类型(Value Type)和引用类型(Reference Type)。值类型继承自System.ValueType类,主要包括结构、枚举和简单类型。而引用类型直接继承自System.Object基类,主要包括类、接口、委托、String等。在讲值类型和引用类型之前,我们必须先了解这两种类型在内存中的存储方式,也就是栈(Stack)和堆(Heap)的存储。

一、栈与堆

通常一个进程的内存空间在逻辑上可以分成三部分:

(1)代码区:用于存放所有的程序代码。

(2)静态数据区:用于存放全局变量,静态数据,常量。

(3)动态数据区:用于存储局部变量等,又分为栈区和堆区。

1、栈(Stack):

  • 栈是一种线性结构。
  • 栈中存储值类型(所有值类型的变量都是在线程堆栈中分配的)。
  • 栈分配是从高地址位指向低地址位分配的,释放时恰反,从低地址位指向高地址位释放(满足先进先出原则)
  • 栈空间不由垃圾回收器管理,当值类型变量出作用域时,该变量在栈中的存储就会自动立即删除。
  • 存取速度比堆要快,仅次于直接位于CPU中的寄存器。
  • 每个线程中都有一个私有的栈,大小为1M,用于保存自身的一些数据。

2、堆(Heap):

  • 堆是一种链式结构。
  • 堆中存储引用类型(所有引用类型变量所引用的对象都是在托管堆中分配的,但是引用是在栈中分配的)。
  • 当进程初始化时,系统会自动为进程创建一个默认堆,这个堆默认所占内存的大小为1M。
  • 堆是低地址位向高地址位分配的。
  • 堆空间由垃圾回收器管理,当引用类型变量出作用域时,该变量在栈中的地址存储会立即被删除,但在堆中的变量数据会一直到程序结束或该数据不被任何变量使用时,由垃圾回收器进行回收,微软建议用户最好不要自行释放内存。
  • 在运行时动态分配内存,存取速度较慢,但堆的内存空间比栈大得多(这就是string类型被设计成引用类型的原因)。
  • 在.NET 这种托管环境下,堆由CLR 进行管理,所以又称为“托管堆(managed heap)”。

二、值类型与引用类型:

       CTS(公共类型系统)定义了一种类型层次结构,该结构不仅仅描述了不同的预定义类型,还指出了用户定义类型在层次结构种的位置。



1、值类型:

  • 结构体:struct(直接派生于System.ValueType)。
  • 数值类型:
    • 整型:sbyte(System.SByte),short(System.Int16),int(System.Int32),long(System.Int64),byte(System.Byte),ushort(System.UInt16)
    • uint(System.UInt32),ulong(System.UInt64),char(System.Char)。
    • 浮点型:float(System.Single),double(System.Double)。
    • decimal型:decimal(System.Decimal)。
    • bool型:bool(System.Boolean)。
  • 用户定义的结构体(派生于System.ValueType)。
  • 枚举:enum(派生于System.Enum)。
  • 可空类型(派生于System.Nullable<T>泛型结构体,T?实际上是System.Nullable<T>的别名)。

示例1:

public void Method_1(){    int value1=10;      // 1    int value2=20;      // 2    int value3=Add(value,value);     // 3}public int Add(int n1,int n2)    //4{   int sum=n1+n2;     //5   return sum;   //6}
运行过程中数据的存储过程见下图示:


      我们已经知道值类型的数据都存储在线程栈中,且是由高地址位向低地址位分配,所以先分别把Value1、Value2压入栈顶,接着调用Add方法,把Add方法的2个形参再压入栈顶,并压入方法的返回地址。然后程序就运行到Add方法的内部,首先是给sum赋值并压入栈顶,然后return返回,直接返回到保存返回地址的栈位,该栈位下的变量因已离开作用域都自动被删除,最后是Value3赋值,将返回地址推出栈,并将Value3压入栈顶。

再来看个示例:

int i;Console.WriteLine(i);
    上述示例编译会出错:使用了未赋值的局部变量。因为在.Net中所有的元素使用前都必须初始化。

     所以我们可以这样进行初始化:int i = 0 ; 或 int i = new int(); ,实际上,当我们调用new创建实例时,编译器会隐式地调用无参构造函数进行初始化,所有的值类型变量被赋值为0(char类型被赋值为'0'),所有的引用类型被赋值为null。

我们可以用Type.IsValueType属性来判断一个类型是否为值类型:

public struct TypeTester{ }TypeTester testType = new TypeTester ();if (testTypetype.GetType().IsValueType){    Console.WriteLine("{0} is value type.", testType.ToString());}

2、引用类型:

  • 数组(派生于System.Array)
  • 用户用定义的以下类型:
    • 类:class(派生于System.Object)。
    • 接口:interface
    • 委托:delegate(派生于System.Delegate)。
    • object(System.Object的别名)。
  • 字符串:string(System.String的别名)。
示例2:
public class Sport{}Sport s = new Sport();

示例2中对象实例化过程:


我们可以将示例2中对象实例化过程分成两步来看:

1、Sport s; 

2、s = new Sport();

其中第1步是声明一个Sport类的引用s,对于32位的Windows系统,用4字节来存储内存地址,所以在栈中分配了一个4字节的空间来存储引用s。

第2步用new创建一个Sport类实例,即在堆中分配一个连续的存储空间,并将该连续空间首地址返回给栈中的s。

所以我们不能简单地认为引用类型变量都存储在堆中,因为对象的引用存储在栈中,而真正的对象存储才存储在堆中。

Note:需要说明的是程序中的类也是存储在托管堆中的,每个类又分为四个部分:类指针(用来关联对象)、同步索引(用来完成同步(比如线程的同步)需建立的)、静态方法及方法列表。

public class Sport{    public virtual string getName()    {        return "Sport";    }}public class Football:Sport{    public override string getName()    {        return "Football";     }}Sport s = new Football();  //栈中Sport引用指向堆中Football对象Console.WriteLine(s.getName());  //运行结果:Football -- 调用Football类的方法Sport s2 = s;Console.WriteLine(s2 == s);  //运行结果:True  -- s2与s指向同一对象空间
上面的例子Sport s = new Football(); 可以分成3步骤:

1、Sport s;  //在栈中分配一个空间存储引用地址。

2、Football temp = new Football();  //堆中分配对象空间

3、s = temp;   //将对象空间地址返回给栈中s;

所以s.getName()调用的Football类中的方法。

(1)String类型(引用类型特例)

对于引用类型,我们容易混淆的是String类型,先来看个实例:

string s1 = "Hello, ";string s2 = "world!";string s3 = s1 + s2;Console.WriteLine("s3="+s3);   //运行结果:s3 = Hello, World!string s1 = "a";string s2 = s1;s1 = "b";Console.WriteLine("s2 =" + s2);  //运行结果:s2 = a;
     这两个示例的运行结果都似乎表明String类型是值类型而非引用类型,实际上,String有个最为显著的特点,就是它具有恒定不变性。string的这个特性使它在重新赋值时并非直接修改string指向的内存空间的数据而是在heap堆上重新分配一块新的内存空间存放新数据,这也导致了string效率低下。

Note:string的恒定不变性:我们一旦在Heap堆上为string分配一块内存空间后,我们将不能对这块内存空间数据进行修改(除非被GC回收)。

再来看个示例:

string s1 = "abc";string s2 = "abc";Console.WriteLine(Object.ReferenceEquals(s1,s2));  //运行结果:True
     示例运行结果表明s1和s2指向同一块内存地址,那为什么两个互不相关的变量会指向同一块地址呢?原来string在创建一个对象之前会先在字符串池中中查找字符串值是否已存在,如果存在就直接指向该内存地址,不存在则在heap堆中重新分配一块内存。

(2)数组

     TestType[] testTypes = new TestType[100];
          对于数组来说,它本身属于引用类型,而如果TestType是值类型的话,那么testTypes中的数据应该存在栈中还是堆中呢?

  • 如果TestType是值类型的话(例如int类型),首先会在堆中分配一块连续的100单元的内存空间,并将这块内存空间的首地址返回给栈中分配的4字节的变量。

       int[] testTypes = new int[100];       foreach (int i in testTypes)       {           Console.Write(i);  //运行结果:输出100个0       }
             前面我们讲到了使用new创建对象时会自动初始化赋值,但要注意的是,值类型元素存储在堆中,并没有被装箱。

  • 如果TestType是引用类型的话,首先在堆中分配了一块空间,且初始化赋值为null,真正分配数据空间的是当我们实例化对象时。

      A [] test = new A[100];  //假设A是个类      A[0] = new A();  //真正分配数据空间      A[1] = new A();

三、类型判等

       类型比较通常有三种方法:“==”、Equals()及ReferenceEquals()方法,其中Equals()方法是System.Object提供的虚方法,用于比较两个对象是否指向相同的引用地址。但是需要注意的是,值类型与引用类型都对Equals()方法进行了重写或重载,而且重写后的方法的意思不一样。

     (1)值类型判等

  •   Equals()方法在System.ValueType中被重写,用于判断变量值是否相等。
  • "==" 也用于判断变量值是否相等。
  • 在值类型中使用ReferenceEquals()方法比较两个对象是否指向相同的引用地址,结果永远返回的是false。

     (2)引用类型判等

  •  Equals()方法在引用类型中也被重写,用于判断变量值是否相等。
  • "=="则用于比较两个对象是否指向相同的引用地址。
  • ReferenceEquals()方法也用于比较两个对象是否指向相同的引用地址。

         值得注意的是string类型的判等,这是引用类型的特例,我们举个例子:

     string a = new string(new char[] { 'h', 'e', 'l', 'l', 'o' });     string b = new string(new char[] { 'h', 'e', 'l', 'l', 'o' });     Console.WriteLine(a == b);  //运行结果:True     Console.WriteLine(a.Equals(b));  //运行结果:True        Console.WriteLine(Object.ReferenceEquals(a,b));  //运行结果:False

         从上面的例子中我们可以清楚看到,string类型的“==”和Equals()方法都是比较变量值是否相等,而ReferenceEquals()方法才是比较两个对象是否指向相同的引用地址。这是因为string类重写"=="及Equals()方法,使其用起来更像值类型。为什么要这样做呢?我只能说最大的原因是为了在堆中存储字符串数据,因为字符串数据可能非常大。

四、类型嵌套

        从上述的讲解,我们已经可以很清晰地了解了值类型和引用类型的存储位置,如果我们单纯地给出一个值类型或引用类型变量,我们一下子就能想到它分配的内存空间,但是,如果我们给出的是一个引用类型中包含值类型的变量,或者是一个值类型中包含引用类型的变量呢?它该存储在堆中还是栈中呢?

       首先先来看个例子:

    //引用类型中包含有值类型    public class ReferenceTypeClass    {        private int valueTypeField;        public ReferenceTypeClass()        {            valueTypeField = 0;        }        public void Method()        {            int valueTypeLocalVariable = 0;        }    }    ReferenceTypeClass referenceTypeClassInstance = new ReferenceTypeClass();//Where is valueTypeField? --堆中    referenceTypeClassInstance.Method(); //Where is valueTypeLocalVariable? --栈中    //值类型中包含有引用类型    public struct ValueTypeStruct    {        private object referenceTypeField;        public ValueTypeStruct()        {            referenceTypeField = new object();        }        public void Method()        {            object referenceTypeLocalVariable = new object();        }    }    ValueTypeStruct valueTypeStructInstance = new ValueTypeStruct();//Where is referenceTypeField? -- 堆中    valueTypeStructInstance.Method();//Where is referenceTypeLocalVariable?  --堆中
       Note:值类型总是分配在它声明的地方:作为字段时,跟随其所属的变量(实例)存储;作为局部变量时,存储在栈上。

       从上面的例题的ReferenceTypeClass类中,valueTypeField是值类型,但是它是作为ReferenceTypeClass类的一个字段存在,所以跟随ReferenceTypeClass类分配在堆中;而valueTypeLocalVariable也是值类型,但它是个局部变量,所以存储在栈中。

       在ValueTypeStruct结构体中,referenceTypeField是引用类型,虽然它是ValueTypeStruct结构体的一个字段,但是它没有跟随性原则,所以它还是分配在堆中。

原创粉丝点击