C#高级编程(第10版) C# 6 & .NET Core 1.0 (.NET开发经典名著)
上QQ阅读APP看书,第一时间看更新

2.4 预定义数据类型

前面介绍了如何声明变量和常量,下面要详细讨论C#中可用的数据类型。与其他语言相比,C#对其可用的类型及其定义有更严格的描述。

2.4.1 值类型和引用类型

在开始介绍C#中的数据类型之前,理解C#把数据类型分为两种非常重要:

● 值类型

● 引用类型

下面几节将详细介绍值类型和引用类型的语法。从概念上看,其区别是值类型直接存储其值,而引用类型存储对值的引用。

这两种类型存储在内存的不同地方:值类型存储在堆栈(stack)中,而引用类型存储在托管堆(managed heap)上。注意区分某个类型是值类型还是引用类型,因为这会有不同的影响。例如,int是值类型,这表示下面的语句会在内存的两个地方存储值20:

        // i and j are both of type int
        i = 20;
        j = i;

但考虑下面的代码。这段代码假定已经定义了类Vector, Vector是一个引用类型,它有一个int类型的成员变量Value:

        Vector x, y;
        x = new Vector();
        x.Value = 30; // Value is a field defined in Vector class
        y = x;
        WriteLine(y.Value);
        y.Value = 50;
        WriteLine(x.Value);

要理解的重要一点是在执行这段代码后,只有一个Vector对象。x和y都指向包含该对象的内存位置。因为x和y是引用类型的变量,声明这两个变量只保留了一个引用——而不会实例化给定类型的对象。两种情况下都不会真正创建对象。要创建对象,就必须使用new关键字,如上所示。因为x和y引用同一个对象,所以对x的修改会影响y,反之亦然。因此上面的代码会显示30和50。

如果变量是一个引用,就可以把其值设置为null,表示它不引用任何对象:

        y = null;

如果将引用设置为null,显然就不可能对它调用任何非静态的成员函数或字段,这么做会在运行期间抛出一个异常。

在C#中,基本数据类型(如bool和long)都是值类型。如果声明一个bool变量,并给它赋予另一个bool变量的值,在内存中就会有两个bool值。如果以后修改第一个bool变量的值,第二个bool变量的值也不会改变。这些类型是通过值来复制的。

相反,大多数更复杂的C#数据类型,包括我们自己声明的类,都是引用类型。它们分配在堆中,其生存期可以跨多个函数调用,可以通过一个或几个别名来访问。CLR实现一种精细的算法,来跟踪哪些引用变量仍是可以访问的,哪些引用变量已经不能访问了。CLR会定期删除不能访问的对象,把它们占用的内存返回给操作系统。这是通过垃圾回收器实现的。

把基本类型(如int和bool)规定为值类型,而把包含许多字段的较大类型(通常在有类的情况下)规定为引用类型,C#设计这种方式是为了得到最佳性能。如果要把自己的类型定义为值类型,就应把它声明为一个结构。

2.4.2 .NET类型

数据类型的C#关键字(如int、short和string)从编译器映射到.NET数据类型。例如,在C#中声明一个int类型的数据时,声明的实际上是.NET结构System.Int32的一个实例。这听起来似乎很深奥,但其意义深远:这表示在语法上,可以把所有的基本数据类型看成支持某些方法的类。例如,要把int i转换为string类型,可以编写下面的代码:

        string s = i.ToString();

应强调的是,在这种便利语法的背后,类型实际上仍存储为基本类型。基本类型在概念上用.NET结构表示,所以肯定没有性能损失。

下面看看C#中定义的内置类型。我们将列出每个类型,以及它们的定义和对应.NET类型的名称。C#有15个预定义类型,其中13个是值类型,两个是引用类型(string和object)。

2.4.3 预定义的值类型

内置的.NET值类型表示基本类型,如整型和浮点类型、字符类型和布尔类型。

1.整型

C#支持8个预定义的整数类型,如表2-1所示。

表2-1

有些C#类型的名称与C++和Java类型一致,但定义不同。例如,在C#中,int总是32位有符号的整数。而在C++中,int是有符号的整数,但其位数取决于平台(在Windows上是32位)。在C#中,所有的数据类型都以与平台无关的方式定义,以备将来从C#和.NET迁移到其他平台上。

byte是0~255(包括255)的标准8位类型。注意,在强调类型的安全性时,C#认为byte类型和char类型完全不同,它们之间的编程转换必须显式请求。还要注意,与整数中的其他类型不同,byte类型在默认状态下是无符号的,其有符号的版本有一个特殊的名称sbyte。

在.NET中,short不再很短,现在它有16位长。int类型更长,有32位。long类型最长,其值有64位。所有整数类型的变量都能被赋予十进制或十六进制的值,后者需要0x前缀:

        long x = 0x12ab;

如果对一个int、uint、long还是ulong类型的整数没有任何显式的声明,则该变量默认为int类型。为了把输入的值指定为其他整数类型,可以在数字后面加上如下字符:

        uint ui = 1234U;
        long l = 1234L;
        ulong ul = 1234UL;

也可以使用小写字母u和l,但后者会与整数1混淆。

2.浮点类型

C#提供了许多整数数据类型,也支持浮点类型,如表2-2所示。

表2-2

float数据类型用于较小的浮点值,因为它要求的精度较低。double数据类型比float数据类型大,提供的精度也大一倍(15位)。

如果在代码中对某个非整数值(如12.3)硬编码,则编译器一般假定该变量是double。如果想指定该值为float,可以在其后加上字符F(或f):

        float f = 12.3F;

3. decimal类型

decimal类型表示精度更高的浮点数,如表2-3所示。

表2-3

.NET和C#数据类型一个重要的优点是提供了一种专用类型进行财务计算,这就是decimal类型。使用decimal类型提供的28位的方式取决于用户。换言之,可以用较大的精确度(带有美分)来表示较小的美元值,也可以在小数部分用更多的舍入来表示较大的美元值。但应注意,decimal类型不是基本类型,所以在计算时使用该类型会有性能损失。

要把数字指定为decimal类型而不是double、float或整数类型,可以在数字的后面加上字符M(或m),如下所示:

        decimal d = 12.30M;

4. bool类型

C#的bool类型用于包含布尔值true或false,如表2-4所示。

表2-4

bool值和整数值不能相互隐式转换。如果变量(或函数的返回类型)声明为bool类型,就只能使用值true或false。如果试图使用0表示false,非0值表示true,就会出错。

5.字符类型

为了保存单个字符的值,C#支持char数据类型,如表2-5所示。

表2-5

char类型的字面量是用单引号括起来的,如’A'。如果把字符放在双引号中,编译器会把它看成字符串,从而产生错误。

除了把char表示为字符字面量之外,还可以用4位十六进制的Unicode值(如’\u0041')、带有强制类型转换的整数值(如(char)65)或十六进制数('\x0041')表示它们。它们还可以用转义序列表示,如表2-6所示。

表2-6

2.4.4 预定义的引用类型

C#支持两种预定义的引用类型:object和string,如表2-7所示。

表2-7

1. object类型

许多编程语言和类层次结构都提供了根类型,层次结构中的其他对象都从它派生而来。C#和.NET也不例外。在C#中,object类型就是最终的父类型,所有内置类型和用户定义的类型都从它派生而来。这样,object类型就可以用于两个目的:

● 可以使用object引用来绑定任何特定子类型的对象。例如,第8章将说明如何使用object类型把堆栈中的值对象装箱,再移动到堆中。object引用也可以用于反射,此时必须有代码来处理类型未知的对象。

● object类型实现了许多一般用途的基本方法,包括Equals()、GetHashCode()、GetType()和ToString()。用户定义的类需要使用一种面向对象技术——重写(见第4章),来提供其中一些方法的替代实现代码。例如,重写ToString()时,要给类提供一个方法,给出类本身的字符串表示。如果类中没有提供这些方法的实现代码,编译器就会使用object类型中的实现代码,它们在类上下文中的执行不一定正确。

后面将详细讨论object类型。

2. string类型

C#有string关键字,在遮罩下转换为.NET类System.String。有了它,像字符串连接和字符串复制这样的操作就很简单了:

        string str1 = "Hello ";
        string str2 = "World";
        string str3 = str1 + str2; // string concatenation

尽管这是一个值类型的赋值,但string是一个引用类型。string对象被分配在堆上,而不是栈上。因此,当把一个字符串变量赋予另一个字符串时,会得到对内存中同一个字符串的两个引用。但是,string与引用类型的常见行为有一些区别。例如,字符串是不可改变的。修改其中一个字符串,就会创建一个全新的string对象,而另一个字符串不发生任何变化。考虑下面的代码(代码文件StringSample/Program.cs):

        using static System.Console;
        class Program
        {
          static void Main()
          {
          string s1 = "a string";
          string s2 = s1;
          WriteLine("s1 is " + s1);
          WriteLine("s2 is " + s2);
          s1 = "another string";
          WriteLine("s1 is now " + s1);
          WriteLine("s2 is now " + s2);
        }
      }

其输出结果为:

        s1 is a string
        s2 is a string
        s1 is now another string
        s2 is now a string

改变s1的值对s2没有影响,这与我们期待的引用类型正好相反。当用值a string初始化s1时,就在堆上分配了一个新的string对象。在初始化s2时,引用也指向这个对象,所以s2的值也是a string。但是当现在要改变s1的值时,并不会替换原来的值,在堆上会为新值分配一个新对象。s2变量仍指向原来的对象,所以它的值没有改变。这实际上是运算符重载的结果,运算符重载详见第8章。基本上,string类已实现,其语义遵循一般的、直观的字符串规则。

字符串字面量放在双引号中("...");如果试图把字符串放在单引号中,编译器就会把它当成char类型,从而抛出错误。C#字符串和char一样,可以包含Unicode和十六进制数转义序列。因为这些转义序列以一个反斜杠开头,所以不能在字符串中使用没有经过转义的反斜杠字符,而需要用两个反斜杠字符(\\)来表示它:

        string filepath = "C:\\ProCSharp\\First.cs";

即使用户相信自己可以在任何情况下都记住要这么做,但输入两个反斜杠字符会令人迷惑。幸好,C#提供了替代方式。可以在字符串字面量的前面加上字符@,在这个字符后的所有字符都看成其原来的含义——它们不会解释为转义字符:

        string filepath = @"C:\ProCSharp\First.cs";

甚至允许在字符串字面量中包含换行符:

        string jabberwocky = @"'Twas brillig and the slithy toves
        Did gyre and gimble in the wabe.";

那么jabberwocky的值就是:

        'Twas brillig and the slithy toves
        Did gyre and gimble in the wabe.

C# 6定义了一种新的字符串插值格式,用$前缀来标记。这个前缀在2.3节中使用过。可以使用字符串插值格式,改变前面演示字符串连接的代码片段。对字符串加上$前缀,就允许把花括号放在包含一个变量甚或代码表达式的字符串中。变量或代码表达式的结果放在字符串中花括号所在的位置:

        public static void Main()
        {
          string s1 = "a string";
          string s2 = s1;
          WriteLine($"s1 is {s1}");
          WriteLine($"s2 is {s2}");
          s1 = "another string";
          WriteLine($"s1 is now {s1}");
          WriteLine($"s2 is now {s2}");
        }

注意:字符串和字符串插值功能参见第10章。