第3章C#中的引用类型和值类型

数据类型是.NET最基本的构成元素,数据类型是该平台下的C#开发语言的基本组成。每一个变量都要求定义为一个特定的类型,并且要求存储在变量中的值只能是这种类型的值。变量既能保存值类型,也可以保存引用类型,还可以是指针。其中引用类型和值类型是.NET中最基本的数据类型。

3.1 引用类型和值类型简介

CLR(公共语言运行时)为.NET平台提供了一个运行环境,CLR支持两种类型:引用类型和值类型。在.NET中的变量的类型通常是引用类型或者值类型。其中值类型包括:基元类型、枚举和结构。引用类型包括:类、字符串、标准模块、接口、数组和委托。CLR中的数据类型示意图如图3-1所示。

图3-1 CLR类型结构

3.2 引用类型

C#预定义的引用类型包括object和string两种类型。而用户定义的引用类型可以是接口类型、类类型和委托类型。引用类型实例总是从托管堆上分配,引用类型实例内存的回收通过垃圾收集器。

3.2.1 引用类型内存分配

引用类型实例分配在托管堆上,变量保存了实例数据的内存引用。引用类型可以是自描述类型、指针类型或接口类型。而自描述类型进一步细分成数组和类类型。类类型则可以是用户定义的类、装箱的值类型和委托。通常声明为以下类型:class、interface、delegate、object、string及其他的自定义引用类型时,该变量即为引用类型。

在C#中有以下一些常用引用类型。

❑ 数组:(派生于System.Array);

❑ 指针:Indicator(指针);

❑ 接口:interface(接口)。

用户使用定义的以下类型。

❑ 类:class(派生于System.Object);

❑ 委托:delegate(派生于System.Delegate)。

❑ object(System.Object的别名);

❑ 字符串:string(System.String的别名)。

【实例3-1】本例将创建一个控制台应用程序,当用户输入自己的名字后,显示“欢迎**使用C# 4.0开发平台!”。

(1)创建控制台应用程序启动Visual Studio 2010,选择“文件”→“新建”命令,在“项目”选项卡中选择“控制台应用程序”,创建一个名称为“3.1”的控制台应用程序,如图3-2所示。

图3-2 创建3.1的项目

(2)编写字符串引用类型的代码,如下所示:

    01          static void Main(string[] args)
    02          {
    03             string yourname;                          //字符串引用类型变量
    04             Console.WriteLine("请输入您的昵称!");
    05             string name = Console.ReadLine();
    06             yourname = NewStr(name);
    07             Console.WriteLine(yourname);
    08             Console.ReadLine();
    09          }
    10
    11          public static string NewStr(string yourname) //公共静态字符函数
    12          {
    13             string newstr;                             //字符串引用类型变量
    14             newstr = "欢迎" + yourname + "使用C# 4.0开发平台!";
    15             return newstr;
    16          }

【代码解析】第11~15行创建公共静态字符函数NewStr()。

【运行效果】代码编写完成之后,按“F5”键或者单击工具栏中的“启动调试”按钮,当用户输入“FREEMEN”后,按“Enter”键,显示结构如图3-3所示。

图3-3 运行结果

引用类型事实上保存一个指向它引用的对象的内存地址。上面的代码段中有两个字符串变量(yourname和newstr)引用了同一个string对象。改变某一个引用指向的对象的属性同时也会影响所有其他指向这个对象的引用。当字符串newstr发生变化时,字符串yourname也跟着发生变化。产生这种行为的原因是由于string对象是恒定的,也就是说,一旦一个string对象被创建,它的值就不能再修改,所以当改变一个字符串变量的值的时候,仅仅是新创建了一个包含修改内容的新的string对象。

3.2.2 引用类型赋值

引用类型赋值时,将会产生一个对该堆上同一个对象的新引用。下面将以引用类型中的class类型为例,实现引用类型的赋值及原始值的覆盖。

【实例3-2】本节将创建一个控制台应用程序,实现引用类型的赋值及原始值的覆盖。

(1)创建控制台应用程序启动Visual Studio 2010,选择“文件”→“新建”菜单命令,在“项目”选项卡中选择“控制台应用程序”,创建一个名称为“3.2”的控制台应用程序。

(2)编写Person类代码,并在Program类中实现引用类型的赋值,代码如下:

    01      class Person
    02      {
    03          public string fullName;                                     //声明引用类型的字符变量
    04          public int age;                                              //声明int变量
    05          public Person(string n, int a)                              //定义Person
    06          {
    07             fullName = n;
    08             age = a;
    09          }
    10          public void PrintInfo()                                     //输出
    11          {
    12             Console.WriteLine("{0} 的年龄是 {1} 岁", fullName, age);
    13          }
    14      }
    15      class Program
    16      {
    17          public static void ArrayOfObjects(params object[] list)    //数组对象操作方法
    18          {
    19             for (int i = 0; i < list.Length; i++)                   //使用循环操作
    20             {
    21                 if (list[i] is Person)
    22                 {
    23                    ((Person)list[i]).PrintInfo();
    24                 }
    25                 else
    26                    Console.WriteLine(list[i]);
    27             }
    28             Console.WriteLine();
    29          }
    30          public static void SendAPersonByValue(Person p)
    31          {
    32             //为age赋值
    33             p.age = 99;
    34             p = new Person("张三", 999);                             //实例化Person
    35          }
    36          public static void SendAPersonByReference(ref Person p)
    37          {
    38             //修改age值
    39             p.age = 555;
    40             p = new Person("张三", 999) ;                            //实例化Person
    41          }
    42          public static void Main()
    43          {
    44             Console.WriteLine("***** 为Person类设置一个新的引用类型值*****");
    45             Person fred = new Person("李四", 12);
    46             Console.WriteLine("值被覆盖前的person对象是:");
    47             fred.PrintInfo();
    48             SendAPersonByValue(fred);
    49             Console.WriteLine("值被覆盖前的person对象是:");
    50             fred.PrintInfo(); //年龄的更改会有效果,但是重新赋值不会起效
    51             Console.WriteLine("\n***** 通过person对象参考 *****");
    52             Person mel = new Person("王五", 23);
    53             Console.WriteLine("值被覆盖前的person对象是:");
    54             mel.PrintInfo();
    55             SendAPersonByReference(ref mel);
    56             Console.WriteLine("值被覆盖前的person对象是:");
    57             mel.PrintInfo();                                         //被重新赋予另外一个对象
    58             Console.ReadLine();
    59          }
    60     }

【代码解析】第1~14行创建Person类,在该类中完成年龄的定义、输出。第17~29行使用数组对象操作方法操作Person类。第45行实例化Person类。

上面代码完全是对同一Person对象的操作,被调用者可以改变对象的状态数据的值和所引用的对象,可重新赋值。

【运行效果】代码编写完成之后,按“F5”键或者单击工具栏中的“启动调试”按钮,显示结果如图3-4所示。

图3-4 运行结果

3.3 值类型

C#中值类型实例通常分配在线程的堆栈上,并且不包含任何指向实例数据的指针,因为变量本身就包含了其实例数据。在托管代码中,类型决定了类型实例的分配位置,而使用类型的开发人员对此没有控制权。值类型实例不受垃圾收集器的控制。

3.3.1 值类型内存分配

值类型主要包括简单类型、结构体类型和枚举类型等。通常声明为以下类型:int、char、float、long、bool、double、struct、enum、short、byte、decimal、sbyte、uint、ulong、ushort等时,该变量即为值类型。C#中的主要值类型如表3-1所示。

表3-1 值类型表

需要说明,C#的所有值类型均隐式派生自System.ValueType,每种值类型均有一个隐式的默认构造函数来初始化该类型的默认值。例如:

    int i = new int();
    //等价于:
    Int32 i = new Int32();
    //等价于:
    int i = 0;
    //等价于:
    Int32 i = 0;

【实例3-3】本例将创建一个控制台应用程序,实现值类型的操作。

(1)创建控制台应用程序。

启动Visual Studio 2010,选择“文件”→“新建”菜单命令,在“项目”选项卡中选择“控制台应用程序”,创建一个名称为“3.3”的控制台应用程序。

(2)编写值类型操作其本身的代码,代码如下:

    01          static void Main(string[] args)
    02          {
    03             Console.WriteLine("所设置的Int类型的值是:");
    04             Console.WriteLine(ReturnValue());
    05             Console.ReadLine();
    06          }
    07          public class MyInt
    08          {
    09             public int MyValue;              //声明值类型变量MyValue
    10          }
    11
    12          public static int ReturnValue()     //静态函数返回int值
    13          {
    14             MyInt x = new MyInt();
    15             x.MyValue = 3;
    16             MyInt y = new MyInt();
    17             y = x;
    18             y.MyValue = 4;
    19             return x.MyValue;                //返回int值
    20       }

【代码解析】第9行声明值类型变量MyValue。第12~20行静态函数ReturnValue()返回int值。第14行实例化MyInt类,并为该类的MyValue赋值。

【运行效果】代码编写完成之后,按“F5”键或者单击工具栏中的“启动调试”按钮,显示结果如图3-5所示。

图3-5 运行结果

3.3.2 值类型赋值

值类型包括数值类型、枚举和结构,它们都分配在栈上,一旦离开定义的作用域,立即就会被从内存中删除。当一个值类型赋值给另一个值类型的时候,默认情况下完成的是一个成员到另一个成员的复制。就数值和布尔型而言,唯一要复制的就是变量本身的值。

值类型赋值的时候,是复制各个值到赋值目标,实际上各自在栈中都有存在,对一个值的操作不会影响另一个。

【实例3-4】本例将创建一个控制台应用程序,来说明值类型赋值的时候,是复制各个值到赋值目标,实际上各自在栈中都有存在,对一个值的操作不会影响另一个。

(1)创建控制台应用程序。

启动Visual Studio 2010,选择“文件”→“新建”菜单命令,在“项目”选项卡中选择“控制台应用程序”,创建一个名称为“3.4”的控制台应用程序。

(2)代码如下所示:

    01      //定义一个结构
    02      struct MyPoint
    03      {
    04          public int x, y;
    05      }
    06      // 类将作为结构使用
    07      class ShapeInfo
    08      {
    09          public string infoString;
    10          public ShapeInfo(string info)
    11          { infoString = info; }
    12      }
    13
    14      struct MyRectangle
    15      {
    16          public ShapeInfo rectInfo;
    17          public int top, left, bottom, right;
    18          public MyRectangle(string info)
    19          {
    20             rectInfo = new ShapeInfo(info);
    21             top = left = 10;
    22             bottom = right = 100;
    23          }
    24      }
    25      class ValRefClass
    26      {
    27          static void Main(string[] args)
    28          {
    29             Console.WriteLine("***** 值类型/ 引用类型 *****");
    30             //在栈中
    31             MyPoint p = new MyPoint();
    32             Console.WriteLine("-> Creating p1");
    33             MyPoint p1 = new MyPoint();
    34             p1.x = 100;
    35             p1.y = 100;
    36             Console.WriteLine("-> Assigning p2 to p1");
    37             MyPoint p2 = p1;
    38             // P1
    39             Console.WriteLine("p1.x = {0}", p1.x);//100
    40             Console.WriteLine("p1.y = {0}", p1.y);//100
    41             // P2.
    42             Console.WriteLine("p2.x = {0}", p2.x);//100
    43             Console.WriteLine("p2.y = {0}", p2.y);//100
    44             // 修改p2.x.不改变p1.x.
    45             Console.WriteLine("-> Changing p2.x to 900");
    46             p2.x = 900;
    47             //显示
    48             Console.WriteLine("-> Here are the X values again...");
    49             ;//100,如果将MyPoint改成类类型,此处会显示900。
    50             Console.WriteLine("p1.x = {0}", p1.x)
    51             Console.WriteLine("p2.x = {0}", p2.x);//900
    52             Console.WriteLine();
    53             // 创建第一个MyRectangle.
    54             Console.WriteLine("-> Creating r1");
    55             MyRectangle r1 = new MyRectangle("This is my first rect");
    56             // 将一个新的MyRectangle赋值给r1
    57             Console.WriteLine("-> Assigning r2 to r1");
    58             MyRectangle r2;
    59             r2 = r1;
    60             //修改r2的值
    61             Console.WriteLine("-> Changing all values of r2");
    62             r2.rectInfo.infoString = "This is new info!";
    63             r2.bottom = 4444;
    64             //显示s
    65             Console.WriteLine("-> Values after change:");
    66             Console.WriteLine("-> r1.rectInfo.infoString:{0}", r1.rectInfo.infoString);
    67             Console.WriteLine("-> r2.rectInfo.infoString:{0}", r2.rectInfo.infoString);
    68             Console.WriteLine("-> r1.bottom: {0}", r1.bottom);//100
    69             Console.WriteLine("-> r2.bottom: {0}", r2.bottom);//4444
    70             Console.ReadLine();
    71          }
    72       }

【代码解析】第4行为结构MyPoint定义两个变量,供后面调用。

【运行效果】代码编写完成之后,按“F5”键或者单击工具栏中的“启动调试”按钮,显示结果如图3-6所示。

图3-6 运行效果

3.4 引用类型和值类型的区别

引用类型和值类型的区别如下。

❑ 值类型对象有两种表示:未装箱形式和装箱形式,而引用类型总是装箱形式。

❑ 当定义自己的值类型时,应该重写Equals方法和GetHashCode方法。

❑ 值类型中不可以有任何的抽象方法,不可以引入任何新的虚方法,所有的方法都隐含为sealed方法。

❑ 当一个引用类型变量被创建时,它被初始化为null。值类型变量总是包含一个符合它的类型的值。

❑ 当将一个值类型变量赋值给另一个值类型变量,会进行一个字段对字段的复制,而将一个引用类型变量赋值给另一个引用类型变量时,只会复制内存地址。

❑ 两个或多个引用类型变量可以指向托管堆中的同一个对象,而每个值类型变量都有一份自己的对象数据副本。

❑ 值类型实例在内存回收时不可能收到任何通知。

System.Runtime.InteropServices.StructLayout特性用于指示CLR是按指定的顺序来存储类型实例的字段,还是以任何CLR认为合适的顺序排列字段。C#编译器为引用类型选择的是LayoutKind.Auto方式,而为值类型选择的是LayoutKind.Sequential方式。

值类型和引用类型的区别如表3-2所示。

表3-2 值类型和引用类型区别表

3.5 C# 4.0中的新特性:查看调用层(View Call Hierarchy)

该特性主要用于查看函数和属性,在编程过程中遇到不明用途的函数,可在该函数上面单击鼠标右键,该特性会告诉用户的函数使用分层列表,如图3-7所示。

图3-7 调用层次

为了查看某个函数的详细信息可以在图3-7中单击查看调用层次,它会显示一个窗体,详细显示对应函数的信息,如图3-8所示。

图3-8 函数信息图

在层次结构中选择窗口函数,调用它会显示参数和函数调用的位置的详细信息,如图3-9所示。

图3-9 函数详细信息

3.6 小结

本章讲解了C#中的数据类型,C#中每一种类型要么是值类型,要么是引用类型。所以每个对象要么是值类型的实例,要么是引用类型的实例。值类型的实例通常是在线程栈上分配的(静态分配)。引用类型的对象总是在进程堆中分配(动态分配)的。

3.7 练习

一、填空题

1.CLR的全称是(  )。

2.常见的值类型包括(  )。

3.常见的引用类型包括(  )。

4.值类型存放的位置(  )。

5.引用类型存放的位置(  )。

二、简答题

1.简述C#中常见的数据类型。

2.说说值类型和引用类型的区别。

三、上机题

尝试使用C#创建一个控制台应用程序,输入一个自然数,显示结果在输入的自然数上加10,运行效果要求与如图2-3所示一致。