3.3 数据类型

C#中的变量类型根据其定义可以分为两种:一种是值类型,另一种是引用类型。这两种类型的差异在于数据的存储方式,值类型的变量本身直接存储数据。而引用类型则存储实际数据的引用,程序通过此引用找到真正的数据,在以下内容中将会对这些类型进行详细讲解。

3.3.1 值类型

视频讲解:光盘\TM\lx\3\03值类型.mp4

值类型变量直接存储其数据值,主要包含整数类型、浮点类型以及布尔类型等。值类型变量在栈中进行分配,因此效率很高,使用值类型主要目的是为了提高性能。值类型具有如下特性:

值类型变量都存储在栈中。

访问值类型变量时,一般都是直接访问其实例。

每个值类型变量都有自己的数据副本,因此对一个值类型变量的操作不会影响其他变量。

复制值类型变量时,复制的是变量的值,而不是变量的地址。

值类型变量不能为null,必须具有一个确定的值。

值类型是从System.ValueType类继承而来的类型,下面详细介绍值类型中包含的几种数据类型。

1.整数类型

整数类型用来存储整数数值,即没有小数部分的数值。可以是正数,也可以是负数。整型数据在C#程序中有3种表示形式,分别为十进制、八进制和十六进制。

十进制:十进制的表现形式大家都很熟悉,如120、0、-127。

注意 不能以0作为十进制数的开头(0除外)。

八进制:如0123(转换成十进制数为83)、-0123(转换成十进制数为-83)。

注意 八进制必须以0开头。

十六进制:如0x25(转换成十进制数为37)、0Xb01e(转换成十进制数为45086)。

注意 十六进制必须以0X或0x开头。

在C#中内置的整数类型如表3.1所示。

表3.1 C#内置的整数类型

byte类型以及short类型是范围比较小的整数,如果正整数的范围没有超过65535,声明为ushort类型即可,当然更小的数值直接以byte类型作处理即可。只是使用这种类型时必须特别注意数值的大小,否则可能会导致运算溢出的错误。

【例3.7】创建一个控制台应用程序,在其中声明一个int类型的变量ls并初始化为927、一个byte类型的变量shj并初始化为255,最后输出,代码如下。(实例位置:光盘\TM\sl\3\2)

        static void Main(string[] args)
        {
            int ls=927;                                //声明一个int类型的变量ls
            byte shj=255;                              //声明一个byte类型的变量shj
            Console.WriteLine("ls={0}", ls);           //输出int类型变量ls
            Console.WriteLine("shj={0}", shj);         //输出byte类型变量shj
            Console.ReadLine();
        }

程序的运行结果为:

        ls=927
        shj=255

此时,如果将byte类型的变量shj赋值为266,重新编译程序,就会出现错误提示。主要原因是byte类型的变量是8位无符号整数,它的范围在0~255之间,266已经超出了byte类型的范围,所以编译程序会出现错误提示。

注意 在定义局部变量时,要对其进行初始化。

2.浮点类型

浮点类型变量主要用于处理含有小数的数值数据,浮点类型主要包含float和double两种数值类型。表3.2列出了这两种数值类型的描述信息。

表3.2 浮点类型及描述

如果不做任何设置,包含小数点的数值都被认为是double类型,例如9.27,没有特别指定的情况下,这个数值是double类型。如果要将数值以float类型来处理,就应该通过强制使用f或F将其指定为float类型。

【例3.8】下面的代码就是将数值强制指定为float类型。

        float theMySum=9.27f;                              //使用f强制指定为float类型
        float theMuSums=1.12F;                             //使用F强制指定为float类型

如果要将数值强制指定为double类型,则应该使用d或D进行设置,但加不加“d”或“D”没有硬性规定,可以加也可以不加。

【例3.9】下面的代码就是将数值强制指定为double类型。

        double myDou=927d;                               //使用d强制指定为double类型
        double mudou=112D;                               //使用D强制指定为double类型

注意 如果需要使用float类型变量时,必须在数值的后面跟随f或F,否则编译器会直接将其作为double类型处理。也可以在double类型的值前面加上(float),对其进行强制转换。

3.布尔类型

布尔类型主要用来表示true/false值,一个布尔类型的变量,其值只能是true或者false,不能将其他的值指定给布尔类型变量,布尔类型变量不能与其他类型之间进行转换。布尔类型通常被用在流程控制中作为判断条件。

【例3.10】将927赋值给布尔类型变量x,代码如下。

        bool x = 927;

这样赋值显然是错误的,编译器会返回错误提示“常量值927无法转换为bool”。布尔类型变量大多数被应用到流程控制语句当中,例如,循环语句或者if语句等。

说明 在定义全局变量时,如果没有特定的要求不用对其进行初始化,整数类型和浮点类型的默认初始化为0,布尔类型的初始化为false。

闯关训练:开发财务系统时,通过值类型创建存储流动资金金额的临时性变量。

3.3.2 引用类型

视频讲解:光盘\TM\lx\3\04引用类型.mp4

引用类型是构建C#应用程序的主要对象类型数据。在应用程序执行的过程中,预先定义的对象类型以new创建对象实例,并且存储在堆中。堆是一种由系统弹性配置的内存空间,没有特定大小及存活时间,因此可以被弹性地运用于对象的访问。引用类型就类似于生活中的代理商,代理商没有自己的产品,而是代理厂家的产品,使其就好像是自己的产品一样。

引用类型具有如下特征。

必须在托管堆中为引用类型变量分配内存。

使用new关键字来创建引用类型变量。

在托管堆中分配的每个对象都有与之相关联的附加成员,这些成员必须被初始化。

引用类型变量是由垃圾回收机制来管理的。

多个引用类型变量可以引用同一对象,这种情形下,对一个变量的操作会影响另一个变量所引用的同一对象。

引用类型被赋值前的值都是null。

所有被称为“类”的都是引用类型,主要包括类、接口、数组和委托。下面通过一个实例来演示如何使用引用类型。

【例3.11】创建一个控制台应用程序,在其中创建一个类C,在此类中建立一个字段Value,并初始化为0,然后在程序的其他位置通过new创建对此类的引用类型变量,最后输出,代码如下。(实例位置:光盘\TM\sl\3\3)

        class Program
        {
            cIassC                                                    //创建一个类C
            {
              public int Value=0;                                     //声明一个公共int类型的变量Value
            }
            static void Main(string[] args)
            {
              int v1=0;                                               //声明一个int类型的变量v1,并初始化为0
              int v2=v1;                                              //声明一个int类型的变量v2,并将v1赋值给v2
              v2=927;                                                 //重新将变量v2赋值为927
              C r1=newC();                                            //使用new关键字创建引用对象
              C r2=r1;                                                //使r1等于r2
              r2.Value=112;                                           //设置变量r2的Value值
              Console.WriteLine("Values:{0}, {1}", v1, v2);           //输出变量v1和v2
              Console.WriteLine("Refs:{0}, {1}", r1.Value, r2.Value); //输出引用类型对象的Value值
              Console.ReadLine();
            }
        }

程序的运行结果为:

        Values:0,927
        Refs:112,112

3.3.3 值类型与引用类型的区别

视频讲解:光盘\TM\lx\3\05值类型与引用类型的区别.mp4

从概念上看,值类型直接存储其值,而引用类型存储对其值的引用。这两种类型存储在内存的不同地方。在C#中,必须在设计类型时就决定类型实例的行为。如果在编写代码时不能理解引用类型和值类型的区别,那么将会给代码带来不必要的异常。

从内存空间上看,值类型是在栈中操作,而引用类型则在堆中分配存储单元。栈在编译时就分配好内存空间,在代码中有栈的明确定义,而堆是程序运行中动态分配的内存空间,可以根据程序的运行情况动态地分配内存的大小。因此,值类型总是在内存中占用一个预定义的字节数。而引用类型的变量则在堆中分配一个内存空间,这个内存空间包含的是对另一个内存位置的引用,这个位置是托管堆中的一个地址,即存放此变量实际值的地方。

也就是说值类型相当于现金,要用就直接用,而引用类型相当于存折,要用得先去银行取。

说明 C#的所有值类型均隐式派生自System.ValueType,而System.ValueType直接派生于System.Object。即System.ValueType本身是一个类类型,而不是值类型。其关键在于ValueType重写了Equals()方法,从而对值类型按照实例的值来比较,而不是引用地址来比较。

下面以一段代码来详细讲解值类型与引用类型的区别,代码如下。

        namespace ConsoleApplication1
        {
            class Program
            {
              static void Main(string[] args)
              {
                  ReferenceAndValue.Demonstration(); //调用ReferenceAndValue类中的Demonstration方法
                  Console.ReadLine();
              }
            }
            public class stamp                                            //定义一个类
            {
              public string Name{get; set;}                               //定义引用类型
              public int Age{get; set;}                                   //定义值类型
            }
            public static class ReferenceAndValue                         //定义一个静态类
            {
              public static void Demonstration()                          //定义一个静态方法
              {
                  stamp Stamp_1=new stamp{Name="Premiere", Age=25};       //实例化
                  stamp Stamp_2=new stamp{Name="Again", Age=47};          //实例化
                  int age=Stamp_1.Age;                                    //获取值类型Age的值
                  Stamp_1.Age=22;                                         //修改值类型的值
                  stamp guru=Stamp_2;                                     //获取Stamp_2中的值
                  Stamp_2.Name="Again Amend";                             //修改引用的Name值
                  Console.WriteLine("Stamp_1's age:{0}", Stamp_1.Age);    //显示Stamp_1中的Age值
                  Console.WriteLine("age's value:{0}", age);              //显示age值
                  Console.WriteLine("Stamp_2's name:{0}", Stamp_2.Name);  //显示Stamp_2中的Name值
                  Console.WriteLine("guru's name:{0}", guru.Name);        //显示guru中的Name值
              }
            }
        }

运行结果如图3.2所示。

图3.2 值类型与引用类型

从图3.2中可以看出,当改变了Stamp_1.Age的值时,age没跟着变,而在改变了Stamp_2.Name的值后,guru.Name却跟着变了,这就是值类型和引用类型的区别。在声明age值类型变量时,将Stamp_1.Age的值赋给它,这时,编译器在栈上分配了一块空间,然后把Stamp_1.Age的值填进去,二者没有任何关联,就像在计算机中复制文件一样,只是把Stamp_1.Age的值复制给age了。而引用类型则不同,在声明guru时把Stamp_2赋给它。前面说过,引用类型包含的只是堆上数据区域地址的引用,其实就是把Stamp_2的引用也赋给guru,因此它们指向了同一块内存区域。既然是指向同一块区域,不管修改谁,另一个的值都会跟着改变。就像信用卡跟亲情卡一样,用亲情卡取了钱,与之关联的信用卡账上也会跟着发生变化。

3.3.4 枚举类型

视频讲解:光盘\TM\lx\3\06枚举类型.mp4

枚举类型是一种独特的值类型,它用于声明一组具有相同性质的常量,编写与日期相关的应用程序时,经常需要使用年、月、日、星期等日期数据,可以将这些数据组织成多个不同名称的枚举类型。使用枚举可以增加程序的可读性和可维护性。同时,枚举类型可以避免类型错误。

说明 在定义枚举类型时,如果不对其进行赋值,默认情况下,第一个枚举数的值为0,后面每个枚举数的值依次递增1。

在C#中使用关键字enum类声明枚举,其形式如下。

        enum枚举名
        {
          list1=value1,
          list2=value2,
          list3=value3,
          …
          listN=valueN,
        }

其中,大括号{}中的内容为枚举值列表,每个枚举值均对应一个枚举值名称,value1~valueN为整数数据类型,list1~listN则为枚举值的标识名称。下面通过一个实例来演示如何使用枚举类型。

【例3.12】创建一个控制台应用程序,通过使用枚举来判断当前系统日期是星期几,代码如下。(实例位置:光盘\TM\sl\3\4)

        class Program
        {
            enum MyDate                            //使用enum创建枚举
            {
              Sun=0,                               //设置枚举值名称Sun,枚举值为0
              Mon=1,                               //设置枚举值名称Mon,枚举值为1
              Tue=2,                               //设置枚举值名称Tue,枚举值为2
              Wed=3,                               //设置枚举值名称Wed,枚举值为3
              Thi=4,                               //设置枚举值名称Thi,枚举值为4
              Fri=5,                               //设置枚举值名称Fri,枚举值为5
              Sat=6                                //设置枚举值名称Sat,枚举值为6
          }
          static void Main(string[] args)
          {
              int k=(int)DateTime.Now.DayOfWeek;   //获取代表星期几的返回值
              switch (k)
              {
                    //如果k等于枚举变量MyDate中的Sun的枚举值,则输出今天是星期日
                  case (int)MyDate.Sun: Console.WriteLine("今天是星期日"); break;
                    //如果k等于枚举变量MyDate中的Mon的枚举值,则输出今天是星期一
                  case (int)MyDate.Mon: Console.WriteLine("今天是星期一"); break;
                    //如果k等于枚举变量MyDate中的Tue的枚举值,则输出今天是星期二
                  case (int)MyDate.Tue: Console.WriteLine("今天是星期二"); break;
                    //如果k等于枚举变量MyDate中的Wed的枚举值,则输出今天是星期三
                  case (int)MyDate.Wed: Console.WriteLine("今天是星期三"); break;
                    //如果k等于枚举变量MyDate中的Thi的枚举值,则输出今天是星期四
                  case (int)MyDate.Thi: Console.WriteLine("今天是星期四"); break;
                    //如果k等于枚举变量MyDate中的Fri的枚举值,则输出今天是星期五
                  case (int)MyDate.Fri: Console.WriteLine("今天是星期五"); break;
                    //如果k等于枚举变量MyDate中的Sat的枚举值,则输出今天是星期六
                  case (int)MyDate.Sat: Console.WriteLine("今天是星期六"); break;
              }
              Console.ReadLine();
          }
        }

程序运行的结果为“今天是星期三”。

查看程序运行的结果,因为当前日期是2014年4月16日星期三,所以输出的结果显示当天是星期三。程序首先通过enum关键字建立一个枚举,枚举值名称分别代表一周的七天,如果枚举值名称是Sun,说明其代表的是一周中的星期日,其枚举值为0,依此类推。然后,声明一个int类型的变量k,用于获取当前表示的日期是星期几。最后,调用swith语句,输出当天是星期几。

3.3.5 类型转换

视频讲解:光盘\TM\lx\3\07类型转换.mp4

类型转换就是将一种类型转换成另一种类型,转换可以是隐式转换或者显式转换,本节将详细介绍这两种转换方式,并讲解有关装箱和拆箱的内容。

说明 对于类型转换,读者可以这么想象,大脑前面是一片内存,源和目标分别是两个大小不同的内存块(由变量及数据的类型来决定),将源数据赋值给目标内存的过程,就是用目标内存块去套取源内存中的数据,能套多少算多少。

1.隐式转换

所谓隐式转换就是不需要声明就能进行的转换。进行隐式转换时,编译器不需要进行检查就能自动进行转换。表3.3列出了可以进行隐式转换的数据类型。

表3.3 隐式类型转换表

从int、uint、long或ulong到float,以及从long或ulong到double的转换可能导致精度损失,但是不会影响其数量级。其他的隐式转换不会丢失任何信息。

说明 当一种类型的值转换为大小相等或更大的另一类型时,则发生扩大转换。当一种类型的值转换为较小的另一种类型时,则发生收缩转换。

【例3.13】将int类型的值隐式转换成long类型,代码如下。

        int i=927;                                         //声明一个整型变量i并初始化为927
        long j=i;                                          //隐式转换成long类型
2.显式转换

显式转换也可以称为强制转换,需要在代码中明确地声明要转换的类型。如果要把高精度的变量的值赋给低精度的变量,就需要使用显式转换。表3.4列出了需要进行显式转换的数据类型。

表3.4 显式类型转换表

由于显式转换包括所有隐式转换和显式转换,因此总是可以使用强制转换表达式将任何数值类型转换为任何其他的数值类型。

【例3.14】创建一个控制台应用程序,将double类型的x进行显式类型转换,代码如下。(实例位置:光盘\TM\sl\3\5)

        static void Main(string[] args)
        {
            doubIe x=19810927.0112;                  //建立double类型变量x
            int y=(int)x;                            //显示转换成整型变量y
            Console.WriteLine(y);                    //输出整型变量y
            Console.ReadLine();
        }

程序的运行结果为19810927。

显示转换也可以通过Convert关键字进行显式类型转换,上述例子还可以通过下面的代码实现。

【例3.15】创建一个控制台应用程序,通过Convert关键字进行显式类型转换,代码如下。

        double x=19810927.0112;                        //建立double类型变量x
        int y=Convert.ToInt32(x);                      //通过Convert关键字转换
        Console.WriteLine(y);                          //输出整型变量y
        Console.ReadLine();
3.装箱和拆箱

将值类型转换为引用类型的过程叫做装箱,相反,将引用类型转换为值类型的过程叫做拆箱,下面将通过例子详细介绍装箱与拆箱的过程。

(1)装箱

装箱允许将值类型隐式转换成引用类型,下面通过一个实例演示如何进行装箱操作。

【例3.16】创建一个控制台应用程序,声明一个整型变量i并赋值为2008,然后将其复制到装箱对象obj中,最后再改变变量i的值,代码如下。(实例位置:光盘\TM\sl\3\6)

        static void Main(string[] args)
        {
            int i=2008;                                //声明一个int类型变量i,并初始化为2008
            object obj=i;                              //声明一个object类型obj,其初始化值为i
            Console.WriteLine("1、i的值为{0},装箱之后的对象为{1}", i, obj);
            i=927;                                     //重新将I赋值为927
            Console.WriteLine("2、i的值为{0},装箱之后的对象为{1}", i, obj);
            Console.ReadLine();
        }

程序的运行结果为:

        1.i的值为2008,装箱之后的对象为2008
        2.i的值为927,装箱之后的对象为2008

从程序运行结果可以看出,值类型变量的值复制到装箱得到的对象中,装箱后改变值类型变量的值,并不会影响装箱对象的值。

(2)拆箱

拆箱允许将引用类型显式转换为值类型,下面通过一个示例演示拆箱的过程。

【例3.17】创建一个控制台应用程序,声明一个整型变量i并赋值为112,然后将其复制到装箱对象obj中,最后,进行拆箱操作将装箱对象obj赋值给整型变量j,代码如下。(实例位置:光盘\TM\sl\3\7)

        static void Main(string[] args)
        {
            int i=112;                                 //声明一个int类型的变量i,并初始化为112
            object obj=i;                              //执行装箱操作
            Console.WriteLine("装箱操作:值为{0},装箱之后对象为{1}", i, obj);
            int j=(int)obj;                            //执行拆箱操作
            Console.WriteLine("拆箱操作:装箱对象为{0},值为{1}", obj, j);
            Console.ReadLine();
        }

程序的运行结果为:

        装箱操作:值为112,装箱之后对象为112
        拆箱操作:装箱对象为112,值为112

查看程序运行结果,不难看出,拆箱后得到的值类型数据的值与装箱对象相等。需要读者注意的是,在执行拆箱操作时,要符合类型一致的原则,否则会出现异常。

说明 装箱是将一个值类型转换为一个对象类型(object),而拆箱则是将一个对象类型显式转换为一个值类型。对于装箱而言,它是将被装箱的值类型复制一个副本来转换,而对于拆箱而言,需要注意类型的兼容性,例如,不能将一个值为“string”的object类型转换为int类型。

互动练习:尝试使用C#制作一个可以在窗体上画桃花的游戏,具体要求为在窗体的左侧显示桃花的3种状态,即花骨朵、花蕾、开花,然后用鼠标单击某一种状态,即可在右侧显示的桃枝上绘制桃花的相应状态。