首页 > 代码库 > 2.3 数据类型

2.3 数据类型

2.3.1  CTS类型(.NET中内置的基本类型)

CTS类型(.NET中内置的基本类型)指的是内置于.NET Framework中的数据类型,比如System.Int32、System.Double等。

C#认可的基本预定义类型并没有内置于C#语言中,而是内置于.NET Framework中。例如,在C#中声明一个int类型的数据是,声明的实际上是.NET中结构System.Int32的一个实例。

                     

通用类型的系统的功能:

  • 建立一个支持跨语言集成、类型安全和高性能代码执行的框架。
  • 提供一个支持完整实现多种编程语言的面向对象的模型。
  • 定义各语言必须遵守的规则,有助于确保用不同语言编写的对象能够交互作用。

例如,要把int i转化成string,可以编写下面的代码:

1 string s = i.ToString();

 

这样做其意义深远:这表示在语法上,可以把所有的基本数据类型看成支持某些方法的类。

  • 确保IL上的强制类型安全;
  • 实现了不同.NET语言的互操作性;
  • 所有的数据类型都是对象。它们可以有方法,属性,等。

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

2.3.2    C#中预定义的类型

  • 整型

表格 2?2 整型

类型

名称

CTS类型

说明

范围

整型

sbyte

System.SByte

8位有符号的整数

-128~127(-27~27-1)

byte

System.Byte

8位无符号的整数

0~255(0~28-1)

short

System.Int16

16位有符号的整数

-32 768~32 767(-214~214-1)

ushort

System.Uint16

16位无符号的整数

0~65 535(216-1)

int

System.Int32

32位有符号的整数

-2 147 483 648~2 147 483 647(-231~231-1)

uint

System.Uint32

32位无符号的整数

0~4 294 967 295(0~232-1)

long

System.Int64

64位有符号的整数

-9 223 372 036 854 775 808~

9 223 372 036 854 775 807(-263~263-1)

ulong

System.Uint64

64位无符号的整数

0~18 446 744 073 709 551 615(0~264-1)

如果对一个整数的类型没有进行显式的声明,则该变量默认是int类型。

如果想要把整数声明为其他类型,可以在数字后面加上指定的字符:

1 uint ui = 20U;
2 long l = 30L;
3 ulong ul = 40UL;

 

也可以使用小写字母u和l,但是小写字母‘l’容易与数字‘1’混淆。

  • 浮点类型

表格 2?3 浮点类型

类型

名称

CTS类型

说明

位数

范围(大致)

浮点类型

float

System.Single

32位单精度浮点数

7

±1.5×10245~±3.4×1038

double

System.Double

64位双精度浮点数

15/16

±5.0×10-324~±1.7×10308

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

如果对一个非整数(如2.43)硬编码,则编译器会默认该变量是double类型。如果想指定该值为float类型,可以在其后方加上字符F(或f):

1 float variable1 = 2.43F;
2 float variable2 = 2.0F;
3 float variable3 = 2.50F;

 

 

  • decimal类型

decimal类型表示精度更高的浮点数。

表格 2?4 decimal类型

名称

CTS类型

说明

位数

范围(大致)

decimal

System.Decimal

128位高精度十进制数表示法

28

±1.0×10-28~±7.9×1028

CTS和C#一个重要的优点是提供了一种专用类型进行财务计算,这就是decimal类型。但应注意,decimal类型不是基本类型,所以计算时使用该类型会有性能损失。

要把数字定义为decimal类型,而不是double、float或整型,可以在数字后边加上字符M(或m):

1 decimal variable1 = 23.20M;
2 decimal variable2 = 2.00M;

 

 

  • bool类型

表格 2?5 bool类型

名称

CTS类型

说明

位数

bool

System.Boolen

表示true或false

NA

true或false

bool值和整数值不能相互转换。如果变量声明为bool类型,那么变量的值只能为true或false,不可以用0表示false,非0的值表示true。

  • 字符类型

为了保存单个字符,C#支持char类型数据。

表格 2?6 字符类型

名称

CTS类型

char

System.Char

表示一个16位的(Unicode)字符

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

技术分享 

图 2?7 错误的表示方法

除了字面量可以表示char类型的值以外,4位16进制的Unicode值(如‘\u0041’)、带有数据类型转换的整数值(如(char)65)或16进制数(‘\0041’)表示它们,同时还可以用转义序列表示,转义字符如下表所示:

表格 2?7 转义字符

转义序列

字符

\’

单引号

\”

双引号

\\

反斜杠

\0

\a

警告

\b

退格

\f

换页

\n

换行

\r

回车

\t

水平制表符

\v

垂直制表符

那么我们就可以通过以下几种方式表示一个字符变量:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //1.使用字面量表示字符变量
 6             char variable1 = C;
 7 
 8             //2.使用4位16进制的Unicode值表示字符变量
 9             char variable2 = \u0041;
10 
11             //3.使用类型转换的整数值表示字符变量
12             char variable3 = (char)65;
13 
14             //4.使用16进制数表示字符变量
15             char variable4 = \x0041;
16 
17             //5.使用转义字符表示字符变量
18             char variable5 = \\;
19 
20             Console.WriteLine("variable1的值:" + variable1);
21             Console.WriteLine("variable2的值:" + variable2);
22             Console.WriteLine("variable3的值:" + variable3);
23             Console.WriteLine("variable4的值:" + variable4);
24             Console.WriteLine("variable5的值:" + variable5);
25 
26             Console.ReadKey();
27         }
28     }

技术分享

 

图 2?8 运行结果

  • 字符串类型

表格 2?8 字符串类型

名称

CTS类型

说明

string

System.String

Unicode字符串

字符串字面量需要放在双引号中,如果试图将字符串放到单引号中,编译器会把塔当作char类型,从而引发错误。

1 //字符串字面量应放在双引号中
2 string str1 = "This is a string !";
3 string str2 = This is a string !;

 

 

技术分享 

图 2?9 错误的表示方法

  • object类型

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

    • 可以使用object引用绑定任何子类型的对象。object引用也可以用于反射,此时必须有代码来处理类型未知的对象。
1 //可以使用object引用绑定任何子类型对象
2 //1.为object对象绑定Person类型的引用
3 object obj1 = new Person();
4 //2.为object对象绑定FileInfo类型的引用
5 object obj2 = new System.IO.FileInfo(@"D:\file.txt");
6 
7 //装箱,将一个值类型转换成引用类型
8 object obj3 = 2;

 

    • object类型实现了许多一般用途的基本方法,包括Equals()、GetHashCode()、GetType()和ToString()。用户自定义的类如果没有提供这些方法的实现,默认使用object类中对应方法中的实现,用户也可以重写这些方法,如重写ToString()方法。

我们首先定义一个Person类,代码如下:

1     public class Person
2     {
3         public string name = "";
4 
5         public void ShowPersonalInfo()
6         {
7             Console.WriteLine("My name is " + name);
8         }
9     }

 

Person类中只第一个类一个字段name,和一个方法ShowPersonalInfo()。

接下来我们创建一个Person类的对象,看看对象里边有哪些东西:

技术分享 

图 2?10 Person对象的字段及方法

从上图我们可以看出,Person对象中多出了Equals()、GetHashCode()、GetType()、ToString()4个方法,这个4个方法我们在Person类中并没有定义,而是从ojbect类中继承过来的,接下来我们调用Person类中的ToString()方法,看一下结果:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Person p = new Person();
 6             string str = p.ToString();
 7             Console.WriteLine(str);
 8 
 9             Console.ReadKey();
10         }
11     }

技术分享

图 2?11 运行结果

从运行结果可以看出,因为我们在Person类中没有提供ToString()方法的实现代码,所以我们此处使用的object类中ToString()方法的实现代码,默认实现返回 Person类型的完全限定名。

现在我们在Person类中对ToString()方法进行重写,在Person类中为ToString()方法提供我们自己想要的实现代码,Person类的代码如下:

 

 1     public class Person
 2     {
 3         public string name = "";
 4 
 5         public void ShowPersonalInfo()
 6         {
 7             Console.WriteLine("My name is " + name);
 8         }
 9 
10         //重写ToString()方法,重写通过关键字override实现
11         public override string ToString()
12         {
13             return "This is my ToString() !";
14         }
15     }

 

最后我们再次调用Person类中的ToString()方法,运行结果如下:

 技术分享

图 2?12 运行结果

从运行结果可以看出,当前的ToString()方法使用的是Person类中的我们自己定义的实现代码,而不是父类object中默认返回Object对象完全限定名的实现代码。

2.3.3   值类型和引用类型

C# 中的类型一共分为两类,一类是值类型(Value Type),一类是引用类型(Reference Type)。值类型和引用类型是以它们在计算机内存中是如何被分配的来划分的。

  • 值类型

值类型实例通常分配在线程的堆栈(stack)上,并且不包含任何指向实例数据的指针,因为变量本身就包含了其实例数据。

值类型包括:

    • 结构:struct(直接派生于System.ValueType);
    • 数值类型:sbyte(System.Sbyte)、byte(System.Byte)、short(System.Int16)、ushort(System.Int16)、int(System.Int32)、uint(System.Int32)、long(System.Int64)、ulong(System.Int64)、float(System.Singele)、double(System.Double)、decimal(System.Decimal)、bool(System.Boolean);
    • 枚举:enum(派生于System.Enum);
    • 可空类型(派生于System.Nullable<T>泛型结构体,T实际上是System.Nullable<T>的别名)。

所有的值类型都隐式地继承自 System.ValueType类型(注意System.ValueType本身是一个类类型),System.ValueType和所有的引用类型都继承自 System.Object基类。你不能显示地让结构继承一个类,因为C#不支持多重继承,而结构已经隐式继承自ValueType。

下面我们通过代码,深入理解一下值类型在内存中是如何存放的:

    首先我们定义一个数据结构,代码如下:

 1     /// <summary>
 2     /// 数据结构,表示一个点的X,Y坐标
 3     /// </summary>
 4     struct StructPoint
 5     {
 6         public int X, Y;
 7         public StructPoint(int X, int Y)
 8         {
 9             this.X = X;
10             this.Y = Y;
11         }
12     }

 

接下来我们声明变量,代码如下:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             int variable1;
 6             variable1 = 20;
 7             bool variable2 = false;
 8 
 9             StructPoint point = new StructPoint(20, 30);
10             Console.WriteLine(variable1);
11             Console.WriteLine(variable2);
12             Console.WriteLine("X:" + point.X + "\tY:" + point.Y);
13 
14             Console.ReadKey();
15         }
16     }

 

现在,我们分析每一行代码执行时,内存中发生的变化:

1 int variable1;

 

当这行代码执行后,内存中就为变量variable1开辟了一块儿空间,如果观察MSIL代码,会发现此时变量还没有被压到栈上,因为.maxstack(最高栈数) 为0。并且没有看到入栈的指令,这说明只有对变量进行操作,才会进行入栈。

1 variable1 = 20;

 

这行代码执行时,变量variable1就被压入栈中对应的内存空间,现在变量已经包含了值类型的所有字段,所以,此时你已经可以对它进行操作了。我们可以用下图表示当前栈中的情况:

技术分享

1 bool variable2 = false;

 

这一行代码时,在内存中为变量variable2分配了一块儿空间,然后将变量压入栈中,此时内存中情况如下图:

技术分享

1 StructPoint point = new StructPoint(20, 30);

 

这一行代码时,在内存中为变量point分配了一块儿空间,然后将变量压入栈中,此时内存中情况如下图:

技术分享 

对变量进行操作,实际上是一系列的入栈、出栈操作。

  • 引用类型

引用类型分配在托管堆(heap)上,会在托管堆中创建一个对象,并把对象的地址传给堆栈上的变量(也可以反过来理解,堆栈上的变量指向(引用)托管堆中的对象)。

引用类型包括:string、类、接口、委托等

当我们声明一个引用类型的变量时,该变量会在堆栈上分配内存空间,用来保存引用对象实例在托管堆中的内存地址,当需要访问这个实例时,首先从堆栈中找到该变量,然后通过堆栈中变量保存的内存地址从托管堆中找到引用对象实例的值。当然,如果只声明了一个引用类型的变量,没有对变量进行初始化,那么托管堆中就不会开辟空间来保存引用类型实例,那样的话堆栈中变量的值就是Null,也就是没办法保存实例在托管堆栈中的内存地址,因为根本就不存在这个实例。这个时候,如果要访问这个实例,肯定就出错了。

我们通过下面的代码对上面的话进行解释,首先我们来看正常情况下,堆栈和托管堆发生了什么:

我们先定义一个Person类,该类只有一个字段name,代码如下:

1     public class Person
2     {
3         public string name;
4     }

 

然后我们声明一个Person类型的变量,并对其实例化,代码如下:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //声明一个Person类型的变量
 6             Person p;
 7             //对变量进行实例化
 8             p = new Person();
 9             //为实例对象的字段赋值
10             p.name = "CS";
11             //引用实例对象的name字段
12             Console.WriteLine(p.name);
13 
14             Console.ReadKey();
15         }
16     }

 

接下来我们分析一下每一行代码执行时,堆栈和托管堆中都发生了什么:

1 Person p;

 

这句代码执行时,在堆栈中分配空间,并且为变量p设置了默认值Null,所以这句代码执行完,堆栈中的情况应该是这样的:

技术分享 

1 p = new Person();

 

当这行代码通过new关键字,在托管堆中为引用类型的实例分配了内存空间,而且由于我们调用的是Person类默认的构造函数,没有对实例对象的字段name赋值,所以字段name在托管堆中值将被自动设置为默认值Null,最后将分配的内存地址保存在了堆栈中对应的变量中,代码执行完后,堆栈和托管堆中的情况去下图所示(0x04A831C2表示的是托管堆中为实例对象分配的内存空间的地址):

技术分享 

从图中我们可以看出,当前堆栈的变量中的值已经变成了托管堆中为实例对象分配的内存空间的地址,也就是说,变量p指向了托管堆中的实例对象。

1 p.name = "CS";

 

这句话是为实例对象的name字段设置值,那么它是怎么在内存中找到这个字段,并修改它的值的呢,其实也很简单,首先堆栈中保存着实例对象在托管堆中对应的内存地址,也就是上张图片中的0x04A831C2,有个这个内存地址,就可以在到托管堆中找到实例对象,最终找到实例对象中的name字段,修改他的值,最后结果如下图所示:

技术分享 

1 Console.WriteLine(p.name);

 

这句代码和上句代码很相似,只不过一个是设置字段name的值,一个是要获取到字段name的值,他们首先都需要在内存中找到字段name。

2.3 数据类型