3.4 C#数据类型
应用程序总是需要处理数据,而现实世界中的数据类型多种多样,我们必须让计算机了解需要处理什么样的数据。
在C#语言中,包含两种数据类型,分别为值类型与引用类型。值类型变量存储的是数据类型所代表的实际数据,值类型变量的值(或实例)存储在栈(Stack)中,赋值语句相当于在传递变量的值。引用类型(如类就是引用类型)的实例,也叫对象,不存在栈中,而存储在可管理堆(Managed Heap)中,堆实际上是计算机系统中的空闲内存。引用类型变量的值存储在栈(Stack)中,但存储的不是引用类型对象,而是存储引用类型对象的引用,即地址。与指针所代表的地址不同,引用所代表的地址不能被修改,也不能转换为其他类型地址。它是引用型变量,只能引用指定类对象。引用类型变量赋值语句相当于是在传递对象的地址。它们的特点如表3-1所示。
表3-1 值类型和引用类型的特点
【例3-3】 下面给出了一个程序来指出值类型与引用类型的区别。
新建控制台程序项目“ex_variable”,具体代码如下。
using System; namespace ex_variable { class Program { public int s = 0; static void Main(string[] args) { test(); } static public void test() { int t1 = 1;//值类型变量t1,其值1存储在栈(Stack)中 int t2 = t1;//将t1的值(为1)传递给t2,t2=1,t1值不变 t2 = 2;//t2=2,t1值不变 Program p1 = new Program();//引用变量r1存储TestClass类对象的地址 Program p2 = p1;//p1和p2都代表是同一个TestClass类对象 p2.s = 2;//和语句p1.s=2等价 Console.WriteLine(p1.s); Console.WriteLine(p2.s); Console.ReadLine(); } } }
程序运行效果如图3-17所示。
图3-17 程序运行效果
3.4.1 值类型
值类型也称为数值类型,可分为简单类型、结构类型和枚举类型。其中,简单类型又包括整数类型、浮点类型、字符类型和布尔类型等。
1. 简单类型
1)整型
整数类型是C#数据类型的一种,整数类型变量的值为整数。C#语言提供了8种整数类型,如表3-2所示。
表3-2 整数类型
【例3-4】 下面给出一个简单的整数类型变量的定义及输出的实例。
新建控制台程序项目“ex_int”,具体代码如下。
using System; namespace ex_int { static void Main(string[] args) { int x=300; Console.WriteLine(x); x++; Console.WriteLine(x); Console.ReadLine(); } }
在Visual Studio 2008中选择“调试”→“开始执行”命令,程序的运行结果如图3-18所示。
图3-18 运行效果
该程序定义了一个int类型的变量x,并赋初值为300,然后输出了x的值,接着通过“++”操作符(将在后面运算符的章节中介绍)来使x的值增加1,接着再输出x的值。
2)浮点型
数学中的实数不仅包括整数,而且包括小数。小数在C#中采用两种数据类型来表示,即单精度型(float)和双精度型(double)。它们的差别在于取值范围和精度不同。计算机对浮点数的运算速度大大低于对整数的运算速度。在对精度要求不是很高的浮点计算中,我们可以采用float型,而采用double型获得的结果将更为准确。当然,如果在程序中大量使用双精度型浮点数,将会占用更多的内存单元,而且计算机的处理任务也将更加繁重。
● 单精度:取值范围在±1.5×10-45到±3.4×1038之间,精度为7位数。
● 双精度:取值范围在±5.0×10-324到±1.7×10308之间,精度为15~16位数。
定义浮点型变量的方式如下:
float x=12.3F;
3)十进制型
C#还专门为用户定义了一种十进制类型(decimal),主要用于方便用户在金融和货币方面的计算。在现代的企业应用程序中,不可避免地要进行大量的这方面的计算和处理,而目前采用的大部分程序设计语言都需要程序员自己定义货币类型。C#通过提供这种专门的数据类型,使用户能够更为快捷地设计这方面的应用程序。
十进制类型是一种高精度的浮点数,如表3-3所示。
表3-3 十进制类型
十进制类型的取值范围比double类型的范围要小得多,但它更精确。
当定义一个decimal变量并赋值给它时,可使用m下标表明它是一个十进制类型,如:
decimal d_value=10.m;
如果省略了m,在变量被赋值之前,它将被编译器当做双精度(double)类型来处理。
4)字符型
为了保存单个字符的值,C#支持char数据类型,如表3-4所示。
表3-4 char数据类型
char类型数据在计算机中占2个字节,其取值范围为0~65535。虽然这个数据类型在表面上类似于C语言的char类型,但它们有重大的区别。因为C#的char类型包含16位,并且不允许在char类型与byte类型之间隐式转换。如果字符要用整数表示,必须使用显式的类型转换,例如:
char a=(char)200;
char类型的变量是用单引号括起来的,如'a'。如果把字符放在双引号中,编译器会把它看做是字符串,从而产生错误。
C#的字符类型同样存在转义字符,如表3-5所示。
表3-5 转义字符
5)字符串类型
字符串是string类型的对象,它的值是文本。在内部,这些文本存储为char对象的只读集合,其中每个对象都表示一个以UTF-16编码的Unicode字符。C#字符串末尾没有以null结尾的字符,因此C#字符串可以包含任意数目的嵌入式null字符。
(1)字符串的声明。
可以通过各种方式来声明和初始化字符串,如下面的示例所示:
// 无初始化的声明 string str1; // 将字符串声明为null string str2 = null; //将字符串初始化为一个具体值 string oldPath = "c:\\Program Files\\Microsoft Visual Studio 8.0"; //将字符串初始化为空字符串 string newPath =""; // 使用系统字符串. System.String greeting = "Hello World!";
(2)字符串常用操作。
① 字符串连接。
string s1 = "A string is more "; string s2 = "than the sum of its chars."; s1 += s2; System.Console.WriteLine(s1); // 输出:A string is more than the sum of its chars.
② 正则字符串。
如果必须嵌入C#提供的转义符,则应使用正则字符串,如表3-6所示。
string columns = "Column 1\tColumn 2\tColumn 3"; //输出:Column 1 Column 2 Column 3 string rows = "Row 1\r\nRow 2\r\nRow 3"; /* 输出 Row 1 Row 2 Row 3 */
表3-6 字符串的转义序列
(3)格式化字符串。
格式字符串是内容可以在运行时动态确定的一种字符串。可采用以下方式创建格式字符串:使用静态Format方法并在大括号中嵌入占位符,这些占位符将在运行时替换为其他值。
【例3-5】 下面的示例使用格式字符串输出循环中每个迭代的结果。
新建控制台程序项目“ex_string”,具体代码如下。
class ex_string { static void Main() { nt j; string s; System.Console.WriteLine("Enter a number"); string input = System.Console.ReadLine(); System.Int32.TryParse(input, out j); or (int i = 0; i < 10; i++) { System.String.Format("{0} times {1} = {2}", i, j, (i * j)); System.Console.WriteLine(s); } System.Console.ReadKey(); } }
运行效果如图3-19所示。
图3-19 运行效果
2. 结构类型
利用上面介绍过的简单类型,进行一些常用的数据运算、文字处理似乎已经足够了。但是我们会经常碰到一些更为复杂的数据类型。比如,通信录的记录中可以包含他人的姓名、电话和地址。如果按照简单类型来管理,每一条记录都要存放到3个不同的变量当中,这样工作量很大,也不够直观。
在实际生活中,我们经常把一组相关的信息放在一起。把一系列相关的变量组织成为一个单一实体的过程,称为生成结构的过程。这个单一实体的类型就叫做结构类型,每一个变量称为结构的成员。结构类型的变量采用struct来进行声明,如我们可以定义通信录记录结构的定义如下:
struct PhoneBook{ public string name; public string phone; public string address; }
“PhoneBook p1;”中p1就是一个PhoneBook结构类型的变量。上面声明中的public表示对结构类型成员的访问权限,有关访问的细节问题我们将在第三部分详细讨论。对结构成员的访问可通过结构变量名加上访问符“.”号,再跟成员的名称,如下代码所示:
p1.name="Mike";
结构类型包含的成员类型没有限制,可以相同,也可以不同。比如,我们可以在通信录的记录中加上年龄这个成员:
struct PhoneBook{ public string name; public string uint age; public string phone; public string address; }
我们甚至可以把结构类型作为另一个结构的成员的类型:
struct PhoneBook{ public string name; public string uint age; public string phone; public struct address{ public string city; public string street; public uint no; } }
这里,“通信录”这个结构中又包括了“地址”这个结构,结构“地址”类型包括城市、街道、门牌号码3个成员。
注意:struct结构的定义要在class Program语句块之外,PhoneBook p1语句则在class Program语句块之内。
3. 枚举类型
枚举(enum)实际上是为一组在逻辑上密不可分的整数值提供便于记忆的符号。比如,我们声明一个代表星期的枚举类型的变量:
enum WeekDay { Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday }
定义一个WeekDay的语句为:
WeekDay day;
默认方式下,枚举元素序列从0开始。
注意:结构是由不同类型的数据组成的一组新的数据类型,结构类型的变量的值是由各个成员的值组合而成的。而枚举则不同,枚举类型的变量在某一时刻只能取枚举中某一个元素的值。比如,day这个表示“星期”的枚举类型的变量,它的值要么是Sunday,要么是Monday或其他的星期元素,但它在一个时刻只能代表具体的某一天,不能既是星期二,又是星期三。
day=Tuseday;
枚举类型的元素所赋的值的类型仅限于long、int、short和byte等整数类型。
3.4.2 引用类型
定义为类、委托、数组或接口的类型是引用类型。在运行时,当声明引用类型的变量时,该变量会一直包含值null。与值类型相比,引用类型不存储它们所代表的实际数据,但它们存储实际数据的引用。在C#中提供了以下几种引用类型供使用。
● 对象类型。
● 类类型。
● 接口。
● 代表元。
● 字符串类型。
● 数组。
例如:
MyClass mc = new MyClass(); MyClass mc2 = mc;
接口必须与实现它的类对象一起初始化。如果MyClass实现IMyInterface,则创建了IMyInterface的实例,如下面的示例所示:
IMyInterface iface = new MyClass();
1. 数组类型
所有数组都是引用类型,即使其元素是值类型也不例外。数组是从Array类隐式派生的,但可以通过C#提供的简化语法来声明和使用它们。数组是一种数据结构,它包含若干相同类型的变量。数组类型的声明方法如下:
type[] arrayName;
type可以是string、int等任意类型,甚至是数组类型。
数组具有以下属性。
● 数组可以是一维、多维或交错的。
● 数值数组元素的默认值设置为零,而引用元素的默认值设置为null。
● 交错数组是数组的数组,因此其元素是引用类型并初始化为null。
● 数组的索引从零开始,具有n个元素的数组的索引为从0到n-1。
● 数组元素可以是任何类型的,包括数组类型。
● 数组类型是从抽象基类型array派生的引用类型。由于此类型实现了IEnumerable和IEnumerable<(Of <(T>)>),因此可以对C#中的所有数组使用foreach迭代。
1)一维数组
关于数组的主要操作如下。
(1)声明。
可以用相同的方式声明存储字符串元素的数组,例如:
string[] stringArray = new string[6];
此数组包含从stringArray [0] 到stringArray [5] 的元素。new运算符用于创建数组并将数组元素初始化为它们的默认值。在此例中,所有数组元素都初始化为零。
(2)数组初始化。
可以在声明数组时将其初始化,在这种情况下不需要级别说明符,因为级别说明符已经由初始化列表中的元素数提供了,例如:
int[] array1 = new int[] { 1, 3, 5, 7, 9 };
另外,可以用相同的方式初始化字符串数组。下面声明一个字符串数组,其中每个数组元素用每天的名称初始化:
string[] weekDays = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
2)多维数组
数组可以具有多个维度。需要使用以阵列形式排列的数据时可以使用多维数组对变量予以声明。
(1)声明。
下列声明可创建一个四行两列的二维数组:
int[,] array = new int[4, 2];
下列声明可创建一个三维(4、2和3)数组:
int[, ,] array1 = new int[4, 2, 3];
(2)数组初始化。
可以在声明数组时将其初始化,如下例所示:
int[,] array2D = new int[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } }; int[, ,] array3D = new int[,,] { { { 1, 2, 3 } }, { { 4, 5, 6 } } };
如果选择声明一个数组变量但不将其初始化,必须使用new运算符将一个数组分配给此变量。例如:
int[,] array5; array5 = new int[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
也可以给数组元素赋值,例如:
array5[2, 1] = 25;
(3)将数组作为参数传递。
数组可作为参数传递给方法。因为数组是引用类型,所以方法可以更改元素的值。
可以将初始化的一维数组传递给方法。例如:
PrintArray(theArray); static void PrintArray(int[] arr) { // method code }
也可以在一个步骤中初始化并传递新数组。例如:
PrintArray(new int[] { 1, 3, 5, 7, 9 });
(4)使用ref和out传递数组。
与所有的out参数一样,在使用数组类型的out参数前必须先为其赋值,即必须由被调用方为其赋值。例如:
static void TestMethod1(out int[] arr) { arr = new int[10]; }
与所有的ref参数一样,数组类型的ref参数必须由调用方明确赋值。因此不需要由接收方明确赋值。可以将数组类型的ref参数更改为调用的结果。例如,可以为数组赋以null值,或将其初始化为另一个数组。例如:
static void TestMethod2(ref int[] arr) { arr = new int[10]; }
下面的示例说明了out和ref在将数组传递给方法时的用法差异。
【例3-6】 使用ref和out传递数组。
新建控制台程序项目“ex_array”,具体代码如下。
class ex_array { //在方法中重新定义数组 static void FillArray1(ref int[] arr) { arr = new int[10]{ 1, 2, 3, 4, 5,6,7,8,9,10 }; } //初始化数值,但不能重新定义数组 static void FillArray(out int[] arr) { // 初始化数组 arr = new int[5] { 1, 2, 3, 4, 5 }; } static void Main(string[] args) {int[]theArray;// 定义数组但不初始化 FillArray(out theArray); System.Console.WriteLine("数组依次是:"); for (int i = 0; i < theArray.Length; i++) { System.Console.Write(theArray[i] + " "); } System.Console.WriteLine(); FillArray1(ref theArray); // 输出数组元素 System.Console.WriteLine("数组依次是:"); for (int i = 0; i < theArray.Length; i++) { System.Console.Write(theArray[i] + " "); } System.Console.WriteLine(); System.Console.WriteLine("Press any key to exit."); System.Console.ReadKey(); } } }
运行效果如图3-20所示。
图3-20 运行效果
2. 对象类型
对象类型(object)是所有类型之母——它是其他类型最根本的基类。因为它是所有对象的基类,所以可把任何类型的值赋给它。因此,object类型可以赋以任何类型的值,这是C#的一个重要特性。将一个整型赋值给对象类型的表达式如下:
object theObj = 123;
下面的实例演示了如何定义object类型的变量,以及如何将它转换为其他类型的值。
【例3-7】 定义object类型的变量。
(1)在“文件”菜单上,指向“新建”,然后单击“项目”。
(2)确保“Windows窗体应用程序”模板处于选中状态,在“名称”字段中输入“object_test”,然后单击“确定”按钮。
使用代码编辑器在Main()方法中输入以下代码:
class Program { static void Main(string[] args) { object a; a = 100; Console.WriteLine(a); Console.WriteLine(a.GetType()); Console.WriteLine(a.ToString()); Console.ReadLine(); } }
选择“调试”→“开始执行”命令,程序的运行结果如图3-21所示。
图3-21 程序的运行结果
3. 类类型
一个类类型可以包含函数成员、字段和事件。函数成员包括方法、属性、索引、操作符、构造函数和析构函数。类和结构的功能是非常相似的,但正如前面所述,结构是值类型,而类是引用类型,它仅允许单继承(不能拥有派生一个新对象的多重基类)。但是,C#中的一个类可以派生自多重接口。关于类类型的知识,将在后面的章节中进行深入的介绍。
4. 接口类型
一个接口定义一个协定。实现接口的类或结构必须遵守其协定。一个接口可以从多个基接口继承,而一个类或结构可以实现多个接口。
接口可以包含方法、属性、事件和索引器。接口本身不提供它所定义的成员的实现。接口只指定实现该接口的类或结构必须提供的成员。
以下代码段定义了接口IFace,它只有一个方法:
interface IFace { Void ShowMyFace(); }
正如前面所提到的,不能从这个定义实例化一个对象,但可以从它派生一个类。因此,该类必须实现ShowMyFace抽象方法,如下所示:
class CFace:IFace { public void ShowMyFace() { Console.WriteLine(“implemention”); } }
C#中接口类型与类类型的区别如下。
(1)接口类似于类,但接口的成员都没有执行方式,它只是方法、属性、事件和索引符的组合而已,并且也只能包含这4种成员;类除了这4种成员之外还可以是别的成员,如字段。
(2)不能实例化一个接口,接口只包括成员的签名,而类可以实例化。
(3)接口没有构造函数,类有构造函数。
(4)接口不能进行运算符的重载,类可以进行运算符重载。
(5)接口的成员没有任何修饰符,其成员总是公共的,而类的成员则可以有修饰符,如虚拟或者静态。
(6)派生于接口的类必须实现接口中所有成员的执行方式,而从类派生则不然。
5. 代表元
代表元(delegate)类型用来封装一个静态方法或者一个对象实例。代表元是C#中比较复杂的概念,C#中的代表元和C/C++中的函数指针非常相似,使用代表元可以把代表元内部方法的引用封装起来,然后通过它使用代表元引用的方法。
它有一个特性就是不需要知道被引用的方法属于哪一个类对象,只要函数的参数个数与返回类型与代表元元素对象一致即可。
6. 类型转换
由于C# 是在编译时静态类型化的,因此变量在声明后就无法再次声明了,或者无法用于存储其他类型的值了,除非该类型可以转换为新的值的类型。例如,不能用整型变量来存储字符串。但有时可能需要将值复制到其他类型的变量或方法参数中。例如,可能有一个整数变量,需要将该变量传递给参数类型为double的方法,或者可能需要将类变量赋给接口类型的变量。这些操作需要进行类型转换。在C#中,可以执行以下几种类型的转换。
1)隐式转换
由于该转换是一种安全类型的转换,不会导致数据丢失,因此不需要任何特殊的语法。例如,从较小整数类型到较大整数类型的转换,以及从派生类到基类的转换都是这样的转换。对于内置数值类型,如果要存储的值无须截断或四舍五入即可适应变量,则可以进行隐式转换。
例如:
int num = 2147483647; long bigNum = num;
2)显式转换(强制转换)
显式转换需要强制转换运算符。源变量和目标变量兼容,但由于目标变量的类型大小比源变量小(或者目标变量是源变量的一个基类),因此存在数据丢失的风险。当进行转换可能会导致信息丢失时,编译器会要求执行显式转换,显式转换也称为“强制转换”。强制转换是显式通知编译器进行转换且知道可能会发生数据丢失的一种方式。若要执行强制转换,请在要转换的值或变量前面的圆括号中指定要强制转换到的类型。
【例3-8】 强制类型转换。
新建控制台程序项目“ex_change”,具体代码如下。
static void Main(string[] args) { double temp = 1234.56; int a; // Cast double to int. a = (int)temp; Console.WriteLine(temp); Console.ReadLine(); }
运行效果如图3-22所示。
图3-22 运行效果
【实用技巧】将字符串转换为整型。
如表3-7所示给出了将字符串转换为其他数值类型数据的常见方法。
表3-7 字符串转换为其他数值类型数据的常见方法
在Windows应用程序中,用户往往都在文本框中输入内容,而文本框只能存储字符串型数据,因此用户所有输入的数值型数据都需要进行转换,否则会出现异常,这对程序开发是非常重要的。以字符串转换为整型为例,在实际应用中,通常使用以下两种方法进行强制类型转换。
方法一,调用ToInt32(String)方法进行转换,如下面代码所示:
int numTemp= Convert.ToInt32("29"); numTemp++; Console.WriteLine(numTemp);
将numTemp转换为整型并加1,屏幕将输出30的结果。
方法二,使用Parse方法:
int numTemp= Int32.Parse ("29"); numTemp++; Console.WriteLine(numTemp);
同样会输出30的结果。
注意:如果字符串的格式无效,则Parse方法会引发一个异常;这时若使用的是TryParse方法则可避免引发异常,而是返回false。具体实现方法如下:
string inputString = "abc"; int numValue; bool parsed = Int32.TryParse(inputString, out numValue); if (!parsed) Console.WriteLine("Int32.TryParse could not parse '{0}' to an int.\n", inputString);
这样就可以避免异常情况的发生了。