重点:
? 类型系统
? 装箱和开箱
? 常用表达式和操作符
? 程序流程控制语句
目的:
通过学习,我们掌握了.NET平台下的数据类型系统,以及C#编成所需要的基本工具:操作符和表达式的使用。控制程序结构语句的使用。可以编写简单的C#程序。
1 类型系统
大多数编程语言都有两种数据类型:一种是语言本身具有的类型(基本数据类型);一种是可以由用户来创建的类型(类)。基本类型就是通常的简单类型,如字符、数字和布尔等;而类则是倾向于更精巧的类型。
使用两种完全不同的类型系统会导致很多问题产生。如兼容性问题,还有就是当您希望指定一个方法使用该语言支持的“任何”类型作为参数值时,由于这些基本类型互不兼容,您就不能指定这样的参数,除非为每一个基本类型编写一个包装类。
值得高兴的是在.NET的世界中就不会有这样的问题。因为.NET的核心是一个公共类型系统。在公共类型系统中一切都是对象,而且所有的对象都是从一个基类中隐性派生出来的。这个基类是公共类型系统的一部分,就是“System.Object”。相关内容在装箱与开箱中详细介绍。
1.1 数值类型
当您拥有一个数值类型的变量时,实际上就拥有了一个包含实际数据的变量。因此,数值类型的首要规则就是不能为空(NULL)。如下实例:
int i = 32;
该实例通过C#的公共类型系统在堆栈上分配了一个32位的空间,并把变量名为”i”的值存入了该空间。
在C#中定义了多种数值类型,包括枚举、结构和基本类型等。不管何时声明了一个以上类型的变量,都会在堆栈上分配与该类型相关的字节空间。并且直接与这些已分配的位打交道。此外传递一个数值类型的变量时,您传递的改变量的值,而不是对包含对象的引用。
1.2 引用类型
引用类型与C++中的引用类似,即它们都是类型安全的指针。类型安全的引用所引用的并不是一个准确无误的地址,而且它(非空时)总能保证指向指定类型的对象。并且该对象已经被分配在堆上。这里应注意的是引用也可以为空这个事实。如下实例:
string s =”Hello World!”;
该实例分配了一个引用类型(string),事实上是在堆上分配了一个值,并且返回了一个指向该值的引用。与基本数值类型一样,C#也定义了多种引用类型,如类、数组、代理和接口等。不管你何时声明了一个以上类型的变量,都会在堆上分配与该类型相关的字节空间,并且直接与该对象的引用而不是那些已分配的位打交道。
2 装箱与开箱
难道C#也是两种不同的数据类型吗?不是。那它是怎么实现类型兼容的呢?那就是“装箱”(boxing)来实现的。最简单的理解就是:装箱就是将数值类型转换为引用类型。相对应的就是引用类型通过“开箱”转换为数值类型。
这项技术之所以如此“伟大”,是因为一个对象在它需要是一个对象的时候,它就仅仅是一个对象。比如:如果您声明了一个System.int32类型的数值类型变量。您可以把它作为参数传递给任何方法,如果该方法的参数类型定义为System.Object,系统会自动执行装箱操作,将它转变成一个Object。对于程序员来说,它和普通的数据类型一样,但可以当作对象来操作。但事实上它只是堆栈上的4个字节而已。如:
int temp =58; //数值类型
System.Object bar =temp; //temp 被执行装箱操作转变成 对象类型 bar这是编译器就生成该值装箱所需的MSIL代码。
现在,要把bar转换为数值类型,就可以执行一个显示转换。
int temp = 58;
System.Object bar = temp;
int temp2 = (int)bar;
注意:装箱就是将数值类型转换成引用类型。
开箱就是将引用类型转换成数值类型。(需指明被转换的类型,因为它可以被转换成任何类型)
2.1所有类型的根 System.Object
我们已经说过所有类型最终都是从System.Object类型派生出来的,因此保证了系统中的每一种类型都至少有一套公共功能。
方法名 描述
boolEqual() 该方法在运行的时候比较两个对象引用以确定它们是否是完全相同的对象,如果两个变量指的是一个对象,则返回true。如果两个变量类型相同并且值也相等,也返回true。反之返回false。
intGetHashCode() 获取指定对象的散列代码。当类的实现程序出于性能考虑需要将对象的散列代码放入散列代码表中时,就需要用到散列函数。
TypeGetType() 在反射方法中用来获得对象的类型信息。
stringToString() 默认情况下,这个方法是用来获得对象名字的。它可以由派生类覆盖,以便返回代表对象的对用户更友好的字符串。
方法名 描述
voidFinalize() 该方法由运行环境来调用,以便在垃圾收集之前进行清除。注意这个方法也许会被调用,也许不会被调用。因此,不要把必须要执行的代码放在这个方法中。这条规则涉及到所谓的确定性结束。
ObjectMenberwiseclone() 该方法表示对对象的“浅复制”(shallowcopy)。浅复制的意思是复制一个包含引用的对象到另一个对象时,不会复制其中被引用的对象。如果需要让您的类支持“深复制”(deepcopy)也就是在复制时包含被引用对象的话,您就必须实现(ICloncable)接口,并且自己手动进行复制。
2.3类型转换
强制转换:即显示转换。如前面的开箱操作。
int temp2 = (int)bar;
将System.Object类型的对象bar转换成int型。使用该方法好处是通用性强,任意类型都可以进行转换,缺点是当转换出错时会抛出一个异常。
as转换操作符:使用as操作符好处是:当执行非法转换时,您不用担心会产生异常。而会返回一个null值。
int temp2 = bar as int;
将System.Object类型的bar对象转换为int类型,如果出现错误则temp2=null;
2.4 表达式和操作符
表2-1 C#操作符优先级(从上至下,由高至低)
操作符类别 操作符
初级操作符 (x), x.y, f(x), a[x], x++, x--, new, typeof, sizeof, checked,unchecked
一元操作符 +, -, !, ~, ++x, --x, (T)x
乘除操作符 *, /, %
加减操作符 +, -
位移操作符 <<, >>
关系操作符 <, >, <=, >=, is
等于操作符 ==
逻辑与 &
逻辑异或 ^
逻辑或 |
条件与 &&
条件或 ||
条件操作符 ?:
赋值操作符 =, *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=
2.4.1初级表达式操作符
? (x)这种形式的圆括号操作符被用来控制优先级,要么是在数学表达式中使用,或者在方法调用中使用。
? X.y点操作符被用来指定类或结构的一个成员。其中x代表含有y的实体,y代表x的成员。
? f(x) 这种形式的圆括号被用来列出一个方法的参数。
? a[x]方括号用在数组的索引中。方括号也与索引器一起使用,其中可以把对象当作数组来对待。索引器在后边的章节叙述。
? x++ x-- 在后边将叙述增量操作符和减量操作符。
? new 用来根据类的定义实例化一个对象
? typeof 在运行时查找关于类型或对象的信息
? sizeof可以用来得到给定类型的大小,以字节表示。注意:使用时有两点要求。第一,只可以对值类型是用sizeof操作符。因此对于类来说只能使用在类的成员上,不能使用在类自身。第二,该操作符只能使用于标记为unsafe的方法或代码段中。
? checked和unchecked 可以控制数学操作的溢出检查。
1. 数学操作符
C#语言支持几乎所有语言都支持的基本数学操作符:乘(*)、除(/)、加(+)、减(-)和取模(%)。前四个操作符意义很明显;取模操作符产生两个数整除后的余数。
2. 一元操作符
C#语言中支持一元操作符加和减。一元减操作符用来告诉编译器操作数应该变为负数。
3. 复合赋值操作符
复合赋值操作符指的是一个二元操作符和一个赋值操作符(=)的组合。语法格式如下:
X op = Y
其中op 表示操作符。表达式等效于下面的表达式:
X = X op Y
4. 增量操作符和减量操作符
增量操作符和减量操作符一开始是从C语言中引入的。然后被C++和java沿用,这两个操作符提供了这样一种简明的操作,即要把一个代表数字值的变量增加1或者是减小1。因此i++就等于把i的当前值加1。
增量操作符和减量操作符现在都有两个版本,通常初学者会混淆。这两种类型通常被称为前缀(prefix)和后缀(postfix),这指出了在何时修改变量。当使用增量操作符和减量操作符的前缀版本时——分别是++i和--i——程序就先执行操作(加1或减1),然后才产生值。当使用增量操作符和减量操作符的后缀版本时——分别是i++和i--——程序就先产生值,然后才执行操作(加1或减1)。如下例:
int i=1;
int j=0;
j=++i; // j=2;i=2
j=i++; // j=2;i=3
可以看出二者区别在于何时产生值以及何时修改操作数。
2.4.2 关系操作符
绝大多数操作符都返回一个数字结果,但是关系操作符不同,它返回的是布尔型结果。它执行的是比较操作书之间的关系,当关系条件为真时返回true,否则返回false。
1.比较操作符
关系操作符中一组被称为比较操作符的操作符包括:小于(<)、小于或等于(<=)、大于(>)、大于或等于(>=)、等于(==)和不等于(!=)。在处理数字时,这些比较操作符的每一个意思都比较明显,但是比较操作符如何处理对象就不是很明显了。当实例化一个对象时,请记住您所得到的只是指向由堆分配的一个对象的引用。因此,当您使用关系操作符来比较两个对象时,C#编译器并不比较两个对象的内容。相反,编译器比较的是这两个对象的地址。那么怎样才能使两个对象按照成员一个个比较呢?答案就在所有的.NET框架对象的隐式基础类库中。类System.Object有一个名为Equals的方法,这个方法专门为这个目的而设计的。
2.4.3 简单赋值操作符
赋值操作符左边的值被称为lvalue;赋值操作符右边的值被称为rvalue。Rvalue可以是任何常数、变量、数字或者是返回值与lvalue相兼容的方法。但是,lvalue必须是一个已定义类型的变量。原因是这个值要从右边复制到左边。因此在内存中必须分配相应的物理空间,这个物理空间就是新值所存放的地方。如:您可以使用语句i=4;因为i代表内存中的一个物理地址,要么是在堆栈中,要么是在堆中,这取决于i变量的数据类型。但是,您不能执行语句4=i;因为4是一个值而不是内存中的一个内容可以被改变的变量。从技术上来说,C#的规则就是lvalue可以是变量、属性或者索引器。属性和索引器的内容在后续章节叙述。
数字的赋值操作很简单,但是涉及到对象的复制就复杂了。记住,在您处理对象时,您并不是在处理简单的、在堆栈中分配的易于复制和移动的元素。当处理对象时,您实际上得到的是一个堆分配上的实体的引用。因而,当您试图把对象(或者任何引用类型)赋值给一个变量时,您并不是像对值类型那样直接复制数据。您只是简单地把一个引用从一个地方复制到另一个地方。
如下例:/Example/Chapter2/2-3-1.cs
using System;
namespace ConsoleApplication1
{
class Foo
{
public int i;
}
class TestApp
{
[STAThread]
static void Main(string[] args)
{
Foo test1 = new Foo();
test1.i = 1;
Foo test2 = new Foo();
test2.i = 2;
Console.WriteLine("Before Object Assignment:");
Console.WriteLine("test1.i={0}",test1.i);
Console.WriteLine("test2.i={0}",test2.i);
test1 = test2;
Console.WriteLine("After Object Assignment:");
Console.WriteLine("test1.i={0}",test1.i);
Console.WriteLine("test2.i={0}",test2.i);
test1.i = 20;
Console.WriteLine("After Change To Only test1 Menber:");
Console.WriteLine("test1.i={0}",test1.i);
Console.WriteLine("test2.i={0}",test2.i);
Console.Read();
}
}
}
执行结果如下:
Before Object Assignment:
test1.i=1
test2.i=2
After Object Assignment:
test1.i=2
test2.i=2
After Change To Only test1 Menber:
test1.i=20
test2.i=20
2.5 程序流程控制语句
在C#程序中,使您能够控制程序流程的语句有三类:选择语句、迭代语句和跳转语句。在每种语句中都要执行一个检测,这个检测产生一个布尔值,这个布尔值被用来控制程序的执行流程。
2.5.1 选择语句
选择语句可以用来判断执行什么代码以及何时执行这些代码。C#提供了两条选择语句:switch语句根据值来选择运行的代码,而if语句根据一个布尔条件来选择运行的代码。这两个选择语句中最常用的是if语句。
1. if语句
如果表达式的值是true,那么if语句就执行一条或多条语句。If语句的语法如下所示—方括号表示else语句的使用是可选的。
if(expression)
statement1
[else
statement2]
这里的expression是任何可以产生布尔值得表达式或变量。如果expression的值为true,则执行statement1,statement2将不被执行,反之亦然。statement1和statement2即可以是一条语句也可以是多条语句组合。如果是多条语句则需要使用大括号{}括起来。如下所示:
if(expression)
{
statement1;
statement2;
……
statementn;
}
2. 多个else子句
if语句的else子句使得在if的判断语句结果为false时,可以选择替代的一个动作过程。如下所示:
if(expression)
{
statement1;
}
else if(expression)
{
}
else if(expression)
{
}
4. switch语句
利用switch语句您可以指定一个表达式,这个表达式返回一个整数值,程序根据表达式的结果来决定执行代码块儿。当然使用多个if/else语句可以实现与switch同样的功能,区别在于switch语句中只有一个条件语句。而if/else语句中有多个条件测试语句。下面是switch语句的语法:
switch(switch_expression)
{
case constant_expression1:
statement1;
break;
case constant_expression2:
statement2;
break;
……
case constant_expression N:
statement N;
break;
[default]
}
switch语句的执行方式和if/else差不多,也是先求出expression的值,然后根据值来判断执行哪一段代码。