第二篇 C#语法及面向对象基础
第2章 C#语法基础
每种计算机语言都有其基本的语法规则,用来管理内存和处理数据。任何计算机程序都必须遵循其编写语言的语法规则。C#语言继承了许多C++的语法特性,简化了C++语法的诸多复杂性。很多关键字和C++中的是一样的。这对使用过C、C++、Java等语言的读者来说是个好消息,但是C#语法并不是C++语法的简单复制,如它对数据类型的管理更为严格。
本章主要内容:
● 变量的定义和使用
● 数据类型及其转换方法
● 常用表达式
● C#基本流程控制语句
● 命名空间介绍
2.1 C#的基本语法
C#在保持C系列语言的优美表示形式的同时,实现了应用程序的快速开发。各语句之间采用分号相隔,各代码块使用“{”作为开始,使用“}”作为结束。代码注释分两种情况,一种是行注释,使用“//”可以实现;另一种是多行注释,使用“/*”作为注释行的开始,使用“*/”作为注释行的结束。不管是行注释,还是多行注释,“C#”编译器都会忽略被注释的行。
在C#中没有单独的头文件,声明方法和类型不要求按照特定的顺序。在C#中引入了namespace(叫做命名空间,详细说明请参考后面章节中的介绍)代替C++中的包含文件,可以使用using namespace包含命名空间,这样程序中便可以使用空间中定义的类、方法等。
下面是对第1章中的程序实例稍加改进之后的代码。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace FormsTest { public partial class Form1 : Form { public Form1() { InitializeComponent(); //初始化控件 } private void button1_Click(object sender, EventArgs e) // button1的 //单击事件 { //MessageBox.Show("你单击了左边的button1按钮。"); } private void button2_Click(object sender, EventArgs e) // button2的 //单击事件 { MessageBox.Show("你单击了右边的button2按钮。"); //弹出消息框 } } }
可以看到语句MessageBox.Show("你单击了左边的button1按钮。")被“//”注释了。但是这并不会产生语法错误,因为C#中的方法可以没有命令语句,该段代码展示了C#的大部分基本语法。为方便开发和维护,代码的编写应该遵循缩进原则,具体格式如上面代码所示,当然VS2008工具具备自动缩进功能。
2.2 变量
变量,对应着内存中的一块存储位置。简单地说,变量是计算机存储于内存中其值可改变的量。这里的“可改变”是指程序运行期间的可改变。
2.2.1 变量的声明
在C#中变量也是对象,变量也有其方法和属性等特征。在程序中需要首先声明变量,然后才可以使用。可以按照如下方式命名变量。
<变量类型> <变量名>;
其中,<变量类型>有int、float、string、byte、long等。变量名的命名需要遵循以下原则。
● 第一个字符只能是字母(包括汉字)或下画线。其余字符可以为字母(包括汉字)、数字和下画线的组合。
● 变量名不能和C#的关键字或库函数名相同。如Main、public、int、class等。
下面的变量命名是合法的。
int x; string p2p; bool _start; float p_p;
而下面的代码则不合法。
int Main; //与库函数同名 string 2pp; //以数字开始 bool ! start; //有标点符号 float _; //只有下画线
另外,声明变量还需要有一定的意义,最好名字能表示其用途。如需要定义一个bool型变量,表示是否启动了线程,则_start是一个很好的变量名。
2.2.2 变量的赋值
在C#中对变量的赋值是非常容易的事。只需要在要赋值的变量名后边加上“=”和所赋的值即可。如下所示代码是给本节中定义的几个变量赋值。
X=20; p2p=”Hello, C#! ”; _start=true; p_p=3.14159;
当然,也可以在定义变量时赋值。同时也可以利用已赋值变量给变量赋值,具体方法如下所示。
int x=20; string p2p=”Hello, C#! ”; bool _start=true; float p_p=3.14159; float r_r=p_p //将已赋值变量p_p的值赋给r_r string p3p= p_p.ToString() //将已赋值变量的p_p.ToString()方法返回值赋给p3p double Sum = 0, a, b, c;
上面代码的最后一行同时声明了4个double型变量。其中在声明的同时Sum被赋初始值0,这在C#中是允许的。
程序中有时需要将变量的值输出。其方法与在C++中略有不同,下面是有关变量定义、赋值和输出的程序VarTest,代码如下所示。
using System; using System.Collections.Generic; using System.Text; namespace VarTest { class Program { static void Main(string[] args) { string str; double Sum = 0, a, b, c; a = 20; b = 30; c = 40; Sum = a + b + c; //求和 str="a+b+c="; //设置字符串值 Console.Write(str); //输出字符串 Console.WriteLine("{0}", Sum); //输出a、b、c的和 Console.ReadKey(); } } }
运行程序,得到如图2-1所示的结果。
图2-1 程序运行结果
其中定义了4个double型变量Sum、a、b、c,并分别赋值,求和后再赋值给Sum变量。注意,str为字符串变量,只能存储字符串。所有字符串都要用双引号括起来。上述“a+b+c”实际上是一串字符,后面将详细介绍。
Console.Write(str); Console.WriteLine("{0}", Sum);
这两行代码实现了写入标准输出流,简单地说就是在控制台窗口中显示。Console.WriteLine用到了两个参数的方法,第一个参数“{0}”表示输出其后边变量列表的第0个变量值。之所以叫变量列表,是因为该方法还可以同时输出多个变量。如将上面两行代码用下面一行代码代替,形式如下所示。
Console.WriteLine("{0}+{1}+{2}={3}", a, b, c, Sum);
其输出结果如图2-2所示。
图2-2 程序运行结果
2.2.3 简单数据类型
细心的读者可能会发现,上面所介绍的变量有多种类型。其实在计算机中,为了便于管理和有效利用内存,把变量分为了多种类型,如整数型、浮点数类型、字符类型等。在C#中,所有数据类型都是从System.Object继承而来的,也即是任何数据类型的值都可以赋值给Object类型变量。在C#中可以将数据类型分为值类型和引用类型。其中值类型又可以分为简单类型和struct类型。前面介绍的数据类型如int、double等数据类型就叫简单数据类型。本节只介绍简单类型,表2-1列出了C#中的简单数据类型。
表2-1 C#中的简单数据类型
从表2-1中可以看出,简单数据类型又可以分为整数类型、布尔类型、浮点类型、字符类型、字符串类型和decimal类型。
表2-1中前8种数据类型就是就是整数类型,即byte、sbyte、ushort、short、uint、int、ulong、long。
表2-1中的bool类型用来表示逻辑“真”和“假”,这在程序的逻辑判断中经常使用。在程序中用“true”和“false”表示逻辑“真”和“假”。
2.2.4 使用简单数据类型
程序BoolTest演示了布尔类型的用法,代码如下所示(鉴于篇幅原因,只给出了部分代码)。
static void Main(string[] args) { bool flagT, flagF; flagT = true; //初始化变量为true flagF = false; //初始化变量为false Console.WriteLine("The value of flagT is {0}", flagT); //输出变量值 Console.WriteLine("The value of flagF is {0}", flagF); //输出变量值 Console.ReadKey(); }
运行程序,效果如图2-3所示。
图2-3 程序运行结果
代码第一行是变量声明,声明了两个bool型变量。第二行、第三行分别对其赋值。随后的代码输出两个变量的值,bool型变量只能取“true”或“false”,若将整型数据或字符串等数据赋给它则会出错。
说明:定义了一种数据类型变量后,该变量的表示范围就已经确定了。如果出现表示范围以外的数据则会产生所谓的“溢出”,这样会导致程序发生错误。实际编程中应该谨慎选取数据类型。
表2-1中的double和float称为浮点类型。浮点类型和整数类型不同,它可以表示小数位。其中double型数据可以有15到16位小数,而float类型数据有7位小数。它们都有一个缺点,就是无法精确表示很多数(一般情况的精度要求足够了)。decimal可以弥补这个缺点,它可以表示到小数点后28位以后。decimal(十进制)类型数据常用于对数据精度要求很高的场合,如银行、复杂的科学计算等。
程序DecimalTest对比了decimal和double类型的精度,代码如下所示。
static void Main(string[] args) { //定义变量 decimal x; double y; x = 3.14159265358979323m; x = x / 3; y = (double)x; //输出变量 Console.WriteLine("x={0}", x); Console.WriteLine("y={0}", y); Console.ReadKey(); }
运行程序,输出结果如图2-4所示。
图2-4 程序运行结果
程序第一、二行分别定义了decimal型变量x和double型变量y。第三行给x赋值圆周率小数点后18位的数值。x对自身除以3之后,第5行代码将decimal强制转换后赋值给double型变量y,最后输出两变量的值。输出结果中,decimal类型变量x的精度达到27位。而double类型变量y的精度只达到了12位,便将后面的数据舍弃了。
表2-1中的char表示字符类型,用于存储字符,长度为一个字节;string是字符串类型,用于存储字符串。字符串的长度可以为0,也可以无限长。
下面程序代码演示了它们的部分属性,程序名为StrChTest。
static void Main(string[] args) { char c; string str, s; c=' I' ; str="you and "; s = " are friends"; str = str + c; //字符串相加 Console.WriteLine("str={0}", str); //输出字符串 str = str + s; //字符串相加 Console.WriteLine("str={0}", str); //输出字符串 str = Convert.ToString(c); //将字符转换为字符串 Console.WriteLine("str={0}", str); //输出字符串 Console.ReadKey(); }
运行程序,结果如图2-5所示。
图2-5 程序运行结果
第一、二行分别定义了一个字符变量c和两个字符串变量str和s。字符需用单引号括起来,而字符串用双引号括起来。对它们赋值完毕后是如下语句。
str = str + c;
该语句将字符和字符串相加后的结果赋值给原来字符串。这在C#这是允许的。从图2-5中可以看出输出第一行str的值为“you and I”。接下来是将两字符串相加,代码如下所示。
str = str + s;
从输出结果知道,后一字符串“are friends”接到了前一个字符串的末尾。如图2-5中第二行输出。
最后是类型转换,代码如下所示。
str = Convert.ToString(c);
字符不可以隐式转换为字符串,所以有必要使用类型转换语句。如图2-5中第三行输出。变量str只有一个字符,但是它仍然被当做字符串。最后,字符和字符串类型还支持包括汉字在内的所有Unicode编码字符。
2.2.5 使用struct创建结构类型
前面讲到,struct(结构)类型是一种复杂数据类型,是因为它可以包含简单数据类型。不但如此,它还可以包含包括自身在内的其他结构类型,以及方法、属性、索引器等。struct类型也是一种值类型,它通过值而不是通过引用方式传递参数。这是因为struct是值类型,而类是引用类型。这也是它与类最重要的区别。struct类型常用来封装小型变量组。如矩形的坐标、大地方位信息等。
2.2.6 结构类型例程
下面的代码显示了一个结构类型的一般定义方法。
<保护级别> struct <结构名> { <保护级别> <类型> <结构成员名称> }
此处<保护级别>有private和public。结构可以实现接口,却无法继承另外一个结构。正因为如此,结构成员不能被声明为protected。
下面是一段声明结构类型的示例代码。
public struct point { public int x; public int y; }
上面代码定义了一个点的坐标,其中结构名为point。成员变量是两个整形变量x和y。其<保护级别>设置为public,读者只需知道这是为了让其他代码可以访问。
定义了结构类型就可以在程序中使用该类型了,这些类型就像简单数据类型如int、double等,可以用来声明具体的变量。可以用如下方法声明。
<结构名> <结构变量名>;
如“point P; ”,声明了一个point类型的结构变量P。该结构变量即包含了一个整型变量x和一个整型变量y。可以通过P.x和P.y访问。在C#中用“.”表示对象的层次关系。
同样结构类型还可以包含结构类型,代码如下所示。
public struct Rectangle { //包含point结构 public point topleft; public point topright; public point buttomleft; public point buttomright; //包含变量 public bool used; }
该结构定义包含了4个point结构类型,和一个bool类型。其结构变量的声明和上述声明方法一致。如“Rectangle rec; ”,同样可以使用如rec.tpoleft.x、rec.tpoleft.y访问所包含结构中的对象。
需要说明的是,在结构类型中不能包含其自身。下面的代码将会导致循环,从而产生错误。
public struct Rectangle { public Rectangle rec; public bool used; }
下面的代码演示了使用struct定义结构类型的具体方法。
//程序StructTest using System; using System.Collections.Generic; using System.Text; namespace StructTest { //定义point结构 public struct point { public int x; public int y; } //定义Rectangle结构 public struct Rectangle { //包含point结构 public point topleft; public point topright; public point buttomleft; public point buttomright; public bool used; } class Program { static void Main(string[] args) { //初始化Rectangle结构变量 Rectangle rec, rc; rec.topleft.x= 200; rec.topleft.y = 200; rec.topright.x = 300; rec.topright.y = 200; rec.buttomleft.x = 200; rec.buttomleft.y = 300; rec.buttomright.x = 300; rec.buttomright.y = 300; rec.used = true; rc = rec; //输出结构实例中的变量值 Console.WriteLine("rc.topleft.x={0}", rc.topleft.x); Console.WriteLine("rc.topleft.y={0}", rc.topleft.y); Console.WriteLine("rc.topright.x={0}", rc.topright.x); Console.WriteLine("rc.topright.y={0}", rc.topright.y); Console.WriteLine("rc.topright.x={0}", rc.buttomleft.x); Console.WriteLine("rc.topright.y={0}", rc.buttomleft.y); Console.WriteLine("rc.buttomright.x={0}", rc.buttomright.x); Console.WriteLine("rc.buttomright.y={0}", rc.buttomright.y); Console.WriteLine("rc.used={0}", rc.used); Console.ReadKey(); } } }
程序首先定义了两个struct结构类型,其中一个是point类型,另一个是Rectangle类型。Point中定义了两个整型成员变量x和y。Rectangle中定义了矩形的4个point结构类型的顶点,以及一个bool类型。完成类型定义之后就可以在程序中使用所定义的类型了。代码首先定义两个Rectangle结构类型rec和rc,接下来对其中的rec中的结构成员进行初始化,代码如下。
rc = rec;
表示将结构变量rec的值赋给rc。因为它们具有相同的类型结构,在内存中相当于将rec的成员值复制给rc的对应成员。从图2-6的显示结构也可以看出,它们的成员值完全相同。
图2-6 程序运行结果
复杂变量的类型中,还有数组类型,详细介绍请参考第5章的数组处理。接下来将介绍有关类型转换问题。
2.2.7 定义结构的构造函数
构造函数是一种特殊的成员函数,主要用来初始化对象,在对象生成时该函数就自动执行。构造函数要求与类型名称相同且没有返回值。
下面定义了一个结构的构造函数实例,程序名为ConstructTest。
using System; using System.Collections.Generic; using System.Text; namespace ConstructTest { class Program { public struct point { public int x; public int y; public point(int x, int y) //定义构造函数 { this.x = x; //对成员赋值 this.y = y; } } static void Main(string[] args) { point P1 = new point(); //使用默认值初始化 point P2 = new point(32, 45); //使用自定义构造函数初始化 Console.WriteLine("P1.x={0}", P1.x); Console.WriteLine("P1.y={0}", P1.y); Console.WriteLine("P2.x={0}", P2.x); Console.WriteLine("P2.y={0}", P2.y); Console.ReadKey(); } } }
程序中的如下代码行是用来定义结构point,包含两个整型成员变量和一个构造函数,该函数带有两个参数,用来初始化前面定义的两个成员变量。
public struct point { public int x; public int y; public point(int x, int y) //定义构造函数 { this.x = x; //对成员赋值 this.y = y; } }
其中如下的代码,
this.x = x; this.y = y;
使用了this指针,此处读者只需知道它表示当前类。上述两行语句中的this.x和this.y表示了获取当前结构中的成员变量x和y。并将参数列表中的x和y值分别赋给它们。
接下来在Main函数中的开始两行语句,代码如下。
point P1 = new point(); //使用默认值初始化 point P2 = new point(32, 45); //使用自定义构造函数初始化
用来实例化类,得到两个相应对象P1和P2。此处的“new”就是用来进行类的实例化。其中第一个对象采用了默认值初始化,第二个对象采用了自定义的构造函数初始化。运行程序,得到如图2-7所示运行结果。
图2-7 程序运行结果
从结果可以看出,第一个对象P1的成员全部为0。第二给对象P2的成员变量值正是所预设的值,分别是32和45。
2.2.8 类型转换
程序中经常会遇到需要将某个类变量的值转换为另外一个类型。如需要显示某个整型变量,则需要将其转换为字符串。类型转换分为以下几种情况。
● 根据转换方式的不同,可以分为隐式(Implicit)转换和显式(Explicit)转换。
● 根据源类型和目标类型之间的关系进行划分,又可分为变换(Conversion)、投射(Cast)和装箱/拆箱(Boxing/Unboxing)。
2.2.9 隐式转换
隐式转换,通常将低类型向高类型转换。如将int型变量转换为float型变量。这类转换可以保证转换前后的值不发生变化。一般情况下,系统可以自动实现隐式转换。如下面的程序IntToDoubleTest所示。
static void Main(string[] args) { double k; int i = 5; k = i; Console.WriteLine("k={0}", k); //实现隐式转换,并输出转换结果 Console.ReadKey(); }
上述代码中定义了一个double类型(高类型)变量k和一个int类型(低类型)变量i。如下代码行将赋值给k,由于类型不同,系统自动实现了隐式转换。
k = i;
代码运行结果如图2-8所示。表2-2列出了可以实现隐式转换的类型。
图2-8 程序运行结果
表2-2 可以实现隐式转换的类型列表
从表2-2中可以看出,不存在向char类型的隐式转换。如整型不可以自动转换为char类型。但是在显示转换中是存在的,这将在后面章节中作介绍。另外,也不支持浮点类型向decimal类型转换。将上述程序改为如下形式。
static void Main(string[] args) { double k=5; int i; i = k; Console.WriteLine("i={0}", i); //不能实现隐式转换 Console.ReadKey(); }
运行程序,错误提示如图2-9所示。
图2-9 类型转换错误提示
错误提示需要进行强制转换或显式转换。将上述代码修改如下。
static void Main(string[] args) { double k=5; int i; i =(int)k; //强制转换类型(可能丢失数据) Console.WriteLine("i={0}", i); Console.ReadKey(); }
则可编译通过,该方法强迫数据从一种类型转换到另一种类型,这即是显示转换。
2.2.10 显式转换
两种类型之间没有任何关系的数据类型,是不能相互转换的。表2-3列出了可以进行显式转换数值类型。
表2-3 可以实现显式转换的数值类型
上述转换也可使用System.Convert方法进行显式转换,方法如下。
static void Main(string[] args) { double k=5; int i; i =Convert. ToInt32(k); //使用函数进行类型转换 Console.WriteLine("i={0}", i); Console.ReadKey(); }
Convert.ToInt32(k)的作用是将变量k的值转换为等效的32位有符号整数。System.Convert类为类型转换提供了一整套方法,前提是这些转换能够被编译器所支持。它提供一种与语言无关的方法进行类型转换,并且可用于公共语言运行库支持的所有语言。Convert用于执行收缩转换和不相关数据类型之间的转换。例如,支持从DateTime类型转换为string类型、从string类型转换为数字类型,以及从string类型转换为bool类型等。
显式转换需要明确制定需要转换的类型。转换前后可能有误差,所以有的地方又叫强制转换。表2-4列出了有关Convert的一些类型转换方法说明。
表2-4 Convert的一些转换方法
由于显式转换是从高类型向低类型转换,转换前后将不可避免存在误差,甚至出现不能成功转换(编译器会提示)。将上述代码修改如下。
static void Main(string[] args) { double k = -3.1415926535; uint ui; ui = Convert.ToUInt32(k); //转换为Uint32类型 Console.WriteLine("ui={0}", ui); }
系统将出现如图2-10所示OverflowException异常。然而,将上述代码改为如下形式。
图2-10 类型转换异常
double k = 3.1415926535; uint ui; ui = Convert.ToUInt32(k); Console.WriteLine("ui={0}", ui);
则系统的编译通过,同时得到如图2-11所示的运行结果。
图2.11 程序运行结果
转换的结果是double变量k的所有小数位被四舍五入掉了。上述两段代码说明,虽然有的类型之间可以实现转换,却是需要一定的条件的。如其中第一段发生异常的代码,就是因为Convert.ToUInt32(k)方法只能将变量k转换为无符号整型;而k是有符号的(负数),所以发生了异常。
2.2.11 根据参与类型转换的划分
根据参与类型转换的源类型和目标类型的关系不同进行划分,又可以将类型转换分为变换、投射和装箱/拆箱。
变换是最常见的一种类型转换,通常用于简单的值类型之间的转换。前面介绍的隐式转换和显式转换都是属于这种类型的转换。
当源类型和目标类型之间具有直接或间接的继承关系时,进行的类型转换属于投射。它分为两种情况。第一种是将子孙类型的对象转换为祖先类型,叫做向上投射(Upcast);第二种是将祖先类型的对象转换为子孙类型,叫做向下投射(Downcast)。
其中,向上投射可以使用隐式转换,而向下投射须使用显式转换。因为具有继承关系的两个类中,子类将拥有父类中的所有成员。因此当子类对象向父类转换时,可以确保不会产生成员丢失的情况。而当父类对象向子类转换时,不一定能确保父类对象拥有子类中定义的成员。
进行类型转换时,在源类型和目标类型中,如果一个是值类型而另一个是引用类型时,则会发生装箱/拆箱转换。其中,从值类型到引用类型的转换称为装箱,而从引用类型到值类型转换则称为拆箱。执行装箱操作时,程序会在堆中新创建一个对象(视为“箱子”)。然后将值类型的值复制到这个新创建的对象中。拆箱时,则是将对象中的值复制到栈上。
2.3 常量
有时候需要在程序中将某个量固定下来,使之保持不变。程序中的圆周率π,它是不需要改变的,同时也不希望程序运行期间被误改。这是就用到了常量这个概念。常量在程序运行期间,其值是不能改变的。在C#语言中,常量可以分为两种类型。一种是静态常量(Compile-time constant),另一种是动态常量(Runtime constant)。其中,前者使用“const”关键字来定义,后者使用“readonly”关键字来定义。
2.3.1 静态常量
可以使用如下方式定义静态常量。
<访问权限 > const <变量类型>
其中“访问权限”有public、private、protected等(具体含有将在第3章有关章节介绍)。使用“const”修饰的“变量类型”通常是值类型。如果是引用类型,则只能在初始化时为其赋予null。下面是一个名为ConstTest的用于定义常量的示例。
public const double pi = 3.14159265;
上面代码定义了一个公有整型常量pi。应该注意的是,静态常量在定义时需要对其初始化。
public const double piL = 3.14; //定义静态常量 static void Main(string[] args) { const double piH = 3.14159265; //定义静态常量 piH = 3.15; Console.WriteLine("piL={0}", piL); //输出静态常量 Console.WriteLine("piH={0}", piH); //输出静态常量 Console.ReadKey(); }
程序中定义了一个const成员变量piL和一个const变量piH。运行结果如图2-12所示。
图2-12 程序运行结果
将上述代码改为如下所示的代码。
const double piH = 3.14159265; piH = 3.15; Console.WriteLine("piL={0}", piL); Console.WriteLine("piH={0}", piH); Console.ReadKey();
则系统出现错误提示:“赋值号左边必须是变量、属性或索引器。”
这说明const常量在程序运行过程中,其值是不允许改变的。其次,const常量还有一个优点,就是在定义const常量时是不占用内存的。因为在编译程序时产生的中间预言代码中,将所有用到这种常量的地方,都会使用其实际值给予替换。
2.3.2 动态常量
动态常量(也叫只读常量)使用“readonly”关键字来定义。相对于静态常量,动态常量要灵活得多,它的定义方式如下。
<访问权限 > readonly <变量类型>
“访问权限”同样有public、private、和protected。只读常量的“变量类型”可以是值类型,也可以是引用类型。
下面是两个只读常量的声明示例。
public readonly System.Text.StringBuilder Sb = new StringBuilder(); private readonly double pi = 3.14159265;
之所以称为动态变量,是因为系统要为“readonly”所定义的动态常量分配内存空间。它和类中其他成员一样拥有独立的内存空间,这和静态常量是不同的。此外,动态常量除了可以在定义的时候设定常量值外,也可以在类的构造函数中设定。由于动态常量相当于类中的成员,因此使用静态常量所受到的类型限制,在动态常量的情况下就不存在了。也可以使用“readonly”去定义任何类型的常量。
2.3.3 使用动态常量
下面代码程序名为ReadOnlyTest,演示动态常量的一些设置。
using System; using System.Collections.Generic; using System.Text; namespace ReadOnlyTest { //定义结构point public struct point { public int x; public int y; //定义结构point的构造函数 public point(int x, int y) { this.x = x; this.y = y; } } class Program { //定义动态常量 private static readonly double pi = 3.14159265; //使用构造函数定义动态常量 public static readonly point P=new point (12,26); static void Main(string[] args) { //输出对比结果 Console.WriteLine("pi={0}", pi); Console.WriteLine("P.x={0}", P.x); Console.WriteLine("P.y={0}", P.y); Console.ReadKey(); } } }
程序中首先定义了带有自定义构造函数的结构point(结构的构造函数的定义方法,请参考前面的章节)。
public struct point { public int x; public int y; public point(int x, int y) { this.x = x; this.y = y; } }
该构造函数带有两个整型变量x和y。用于将参数列表中传下来的参数,分别赋值给结构的成员变量x和y。后面实例化该对象时便是调用了该函数。代码如下所示。
private static readonly double pi = 3.14159265; public static readonly point P=new point (12,26);
这两行中用到了“static”关键字,此关键字是一个静态方法,用来定义的对象可以直接调用。需要注意的是,动态常量不能也没有必要使用“static”关键字修饰。
其中,第一行代码定义了一个静态双精度浮点型常量pi。第二行使用自定义的构造函数,实例化一个point类型的结构。整个程序的最后几行是常量值的输出。运行程序,结果如图2-13所示。
图2-13 程序运行结果
上述程序实现了在结构的构造函数种进行初始化,达到了代码重用的效果。
表2-5总结了静态常量和动态常量的异同点。
表2-5 静态常量和动态常量对比
2.4 表达式
C#语言中的表达式与数学中的表达式基本类似,都有严格的格式。在C#中表达式用于帮助数据处理,如加、减、取反和逻辑判断等。任何程序中基本都离不开表达式的使用。通常给一个变量赋值用的符号“=”是一个赋值运算符,它与变量和值构成一个表达式。表达式描述了对数据进行处理的顺序和操作。由运算符和运算量组成,其中运算量(又叫操作数)通常指的是变量。下面内容主要介绍运算符。
2.4.1 数学运算符
和数学中的运算符对应,在C#中有加、减、乘、除和求余5种基本的数学运算符。除此之外还有两个符号运算符,它们是正号“+”和负号“-”。表2-6列出了它们的基本用法和意义。
表2-6 数学运算符
2.4.2 普通数学运算符
下面程序MathOperatorTest演示了上述运算符的使用和意义。
static void Main(string[] args) { double a; int b, c; a = 0; b = 25; c = 2; a = b + c; Console.WriteLine("a={0}", a); a = b - c; Console.WriteLine("a={0}", a); a = b * c; Console.WriteLine("a={0}", a); a =(double)b / (double)c; Console.WriteLine("a={0}", a); a = b % c; //求余 Console.WriteLine("a={0}", a); a = +a; //取符号为“+” Console.WriteLine("a={0}", a); a = -a; //取符号为“-” Console.WriteLine("a={0}", a); Console.ReadKey(); }
上面代码运用了所有的数学运算符,需要说明的是下面的代码。
a =(double)b / (double)c;
这里用到了类型转换,是由于C#中的整数相除,仍然是整数。也就是说,如果将上述代码修改如下。
a =b / c;
则a的值为12,而不是12.5,所以在进行相除时一定要注意变量类型。运行程序,得到如图2-14所示的结果。
图2-14 程序运行结果
可以看出,一元运算符“-”实现了变量的符号取反运算;而运算符“+”对变量的数值并没有影响。但是,这并不表示“+”运算符没有用处,示例代码如下所示。
string str1 , str2 , str3; str1=”abcdefg”; str2=”higklmn”; str1=str2 + str3; //字符串相加 Console.WriteLine("str1={0}”, str1);
上面代码运行后将会输出“abcdefghigklmn”。
也就是“+”运算符实现了两个字符串的连接,而其他数学运算符则不能实现对字符串的操作。后面的章节还会介绍“+”运算符的其他应用,如运算符重载等。
2.4.3 自加和自减运算符
自加运算符用“++”表示,自减运算符用“--”表示。它们在C#中分别都有两种使用方式,同时这两种方式的意义也是不一样的。表2-7列出了它们的用法和意义。
表2-7 自加和自减运算符
自加和自减运算符采用“自右至左”的结合方向,它们只能对整型变量进行运算,而不能是表达式或常数,如下面语句是错误的。
6++; (a+b)--;
程序AutoTest演示了自加和自减运算符的使用和意义,代码如下。
static void Main(string[] args) { int a, b; a = 0; b = 5; a = b++; //自加运算,先赋值加后 Console.WriteLine("a={0}", a); Console.WriteLine("b={0}", b); a = b--; //自减运算,先赋值加减 Console.WriteLine("a={0}", a); Console.WriteLine("b={0}", b); a = ++b; //自加运算,先加后赋值 Console.WriteLine("a={0}", a); Console.WriteLine("b={0}", b); a = --b; //自减运算,先减后赋值 Console.WriteLine("a={0}", a); Console.WriteLine("b={0}", b); Console.ReadKey(); }
程序运行结果如图2-15所示。
图2-15 程序运行结果
2.4.4 赋值运算符
前面已经多次使用到了“=”赋值运算符。其实,在C#中还有很多类似的运算符,正确地使用它们可以提高程序开发效率。表2-8列出了C#中的赋值运算符及其解释。
表2-8 C#中的赋值运算符
下面程序AssignmentTest演示了上述赋值运算符的使用。
static void Main(string[] args) { byte a , b; a = 9; b = 3; a += b; //相加 Console.WriteLine("a={0}", a); a -= b; //相减 Console.WriteLine("a={0}", a); a *= b; //相乘 Console.WriteLine("a={0}", a); a /= b; //相除 Console.WriteLine("a={0}", a); a <<= b; //左移 Console.WriteLine("a={0}", a); a >>= b; //右移 Console.WriteLine("a={0}", a); a &= b; //求位与运算 Console.WriteLine("a={0}", a); a ^= b; //求位异或运算 Console.WriteLine("a={0}", a); a —= b; //求位或运算 Console.WriteLine("a={0}", a); Console.ReadKey(); }
代码首先定义了两个字节型(无符号8位整数)变量数a和b,其中a=9,二进制表示为a=00001001。b=3,其二进制表示为b=00000011。运行程序,结果如图2-16所示。
图2-16 程序运行结果
前4行分别是加、减、乘、除的结果,第5行中a=72。语法如下所示。
a <<= b;
将a左移了3位(b=3),此时的a=9,即a=00001001。将其向左移后变成a=01001000也即a=72。在移动时将右边空出的位填0。
同理,下面的代码。
a >>= b;
将a的值右移3位,等于将a的值还原。
接下来的代码。
a &= b;
用于将a和b按位求与运算。此时a=00001001, b=00000011。所以得到最后的a=00000001即a=1。其余两个运算符道理相同,在此不再赘述。
2.4.5 比较运算符
比较运算符通常用于确定变量之间的关系,如大于、小于等。它是程序作出逻辑判断的基础。表2-9列出了C#中的一些常用的比较运算符及其解释。
表2-9 C#中的比较运算符
static void Main(string[] args) { int a, b; bool bt, bf; bt = true; bf = false; a = 5; b = 8; if (a == b) //判断是否相等 { Console.WriteLine("a等于b");
下面的程序CompareTest说明了上述比较运算符的使用和意义。
} else { Console.WriteLine("a不等于b"); } //输出类型判断结果 Console.WriteLine(a is int); //输出逻辑与运算结果 Console.WriteLine(bt && bf); Console.ReadKey(); }
程序中用到了“if else”分支语句,该语句的意义正如其英文意义。当“if”的括号中的表达式值为“true”时,程序进入该代码块,否则进入“else”代码块运行。程序运行结果如图2-17所示。
图2-17 程序运行结果
2.4.6 运算符的优先级
上面介绍的很多运算符都可以相互组合为表达式,如(a=b)+c*d、x%y+z等。这为复杂问题的解决提供了方便,但同时带来了需要考虑的运算符的优先级问题。表2-10列出了C#中各种运算符的优先级顺序。
表2-10 C#中的比较运算符
其中的“? :”为条件运算符,该运算符使用形式如下。
c? e1:e2
当“c”为真时程序执行e1语句,反之则执行e2语句。该语句也是C#中唯一的三元运算符。
注意:编程应该以“可靠性高于效率”的原则进行。如将费解的地方分解处理,或者使用“()”将没有把握的地方括起来,或者对代码加注释等。这样使程序容易让人读懂,便于后期的检查和维护。
2.4.7 命名空间
在.NET Framework中包含了一个由4000多个类组成的基础类库,这就是.NET Framewok基础类库。基础类库中的这些类被组织成为命名空间。为从字节流的输入和输出到数据库操作、到文件操作、到Windows窗体控件和ASP.NET控件,所涉及的所有内容提供多种有用的功能。通常的C#应用程序都离不开.NET Framework类库支持。
其实,在前面的每个程序中都用到了命名空间。有System.、System.IO、System.Text等。以第2.3.2节的程序为例,它们的使用方式如下。
using System; using System.Collections.Generic; using System.Text;
包含命名空间用到了“using”关键字,告知系统“本程序需要用到这个命名空间,请提供该命名空间中的所有需要的服务”。这样便可以在自己的程序(命名空间)中使用上述命名空间中的类。
当然,也可以自定义命名空间。实际上C#中的几乎所有类都放在特定的命名空间中,包括用户自己编写的程序。
下面的示例程序,整个命名空间以“namespace ReadOnlyTest”开始。“ReadOnlyTest”便是自定义的程序名,也是自定义的命名空间名。整个代码块包含在“namespace ReadOnlyTest”所在的大括号内,其中是系统和用户定义的类、结构及其成员,包括point结构和program类。
namespace ReadOnlyTest { //定义结构 public struct point { public int x; public int y; //定义结构的构造函数 public point(int x, int y) { this.x = x; this.y = y; } } class Program { //定义动态常量 private static readonly double pi = 3.14159265; //使用构造函数定义动态常量 public static readonly point P=new point (12,26); static void Main(string[] args) { //输出结果 Console.WriteLine("pi={0}", pi); Console.WriteLine("P.x={0}", P.x); Console.WriteLine("P.y={0}", P.y); Console.ReadKey(); } } }
之所以在程序中引入命名空间,是由于该方式可以有效解决类的重名问题。比如在开发大型软件时A开发组定义了classA类,然而B开发小组也定义了一个classA类。这样在最后组织到一起进行编译时编译器便会提示两个类重名。而引入命名空间之后,可以将A开发组的命名空间命名为namespaceA,将B开发组的命名空间命名为namespaceB。这样由于命名空间的不同便可以使用相同的类名了,这在C#中是完全可行的。只是如果需要访问classA,需要使用完全的路径名,如namespaceA.classA、namespaceB.classA,这样编译器才知道到底访问哪个空间中的类。当然也可以使用“using”关键字将它们包含进程序,但是仍然需要限定命名空间。
细心的读者可能会发现有的命名空间会非常长,如System.Security.Cryptography.X509 Certificates。如果每次都要限定命名空间,将会是一件非常烦琐的事情。让人欣慰的是,C#提供了一种“别名”机制,可以使用该机制定义一个需要引用的命名空间别名,定义方法如下。
using <别名> =<命名空间>
定义了别名之后就可以利用该别名访问对于命名空间中的类了。这与访问实际的命名空间是一样的,代码如下。
using SSCX= System.Security.Cryptography.X509Certificates; ...... SSCX.X509Store.Equals=true; ......
2.4.8 嵌套命名空间
从命名空间的引用可以看出,命名空间中可以继续定义命名空间。这就是命名空间的嵌套,定义方式如下。
namespace NameA { public class classA { ...... funA() { ...... } ...... } namespace NameB { public class classB { ...... funB() { ...... } ...... } } ...... }
上面代码定义了一个命名空间NameA,其中又定义了一个类classA和一个命名空间NameB。其中类classA中又定义了一个方法funA()。命名空间NameB中又定义了一个方法funB()。如果该名称空间的外部空间程序要访问funA(),则可以通过全名NameA.classA.funA()访问;相应的访问funB()可以通过全名NameA.NameB.classB.funB()访问。
上面介绍的是在一个文件中定义命名空间,C#中还有可以分开定义同一个命名空间,并不要求一定在同一个文件中。
2.5 流程控制
从功能上划分,语句可以分为两类。一类是用于描述计算的操作运算语句,如数学运算、赋值运算、方法调用语句等;另一类是用于控制这些语句执行顺序的语句,如选择语句、循环控制语句等。这类语句叫做流程控制语句。
可以将流程控制语句分为分支语句、循环语句和跳转语句等几个大类。本节主要介绍这几类语句。
2.5.1 分支语句
程序中经常会出现很多的运算结果,这些结果又对应着多种可能的执行分支语句。分支语句便是用于选择和执行程序中的这些分支语句。C#分支语句主要包括三元运算符、if语句和switch语句。
2.5.2 三元运算符
三元运算符在前面已经有所介绍,其格式如下。
c? e1:e2
其分支方法是,当“c”为真时程序执行e1语句,反之则执行e2语句。下面的程序TriTest演示了三元运算符的具体应用。
static void Main(string[] args) { const double noon=12; string resultStr; //获取系统当前时间的小时部分 double now = System.DateTime.Now.Hour; Console.WriteLine("time={0}", now); //判断变量大小,用以确定是上午或下午 resultStr=(now< noon)? "现在是上午":"现在是下午"; Console.WriteLine("{0}", resultStr); Console.ReadKey(); }
程序首先定义了三个变量,其中两个double型变量用于存储时间,另一个string型变量用于显示结果。
double now = System.DateTime.Now.Hour;
用来获取系统当前时间的小时部分,并把结果赋值给变量now。
resultStr=(now<time)? "现在是上午":"现在是下午";
该语句是关键,判断现在时间和设定的时间的大小,用以确定是上午还是下午。结果返回给变量resultStr,最后是显示结果。运行程序得到如图2-18所示结果。
图2-18 程序运行结果
三元运算符,也可以实现嵌套使用。嵌套格式如下所示。
expr0? expr1:expr2;
其中表达式expr1和expr2都可以嵌套。如下面的代码。
expr0? (expr00? expr11: expr22):expr2;
为了便于区分使用了括号,但实际中可以不使用。
上面程序中只能判断是上午还是下午,如果还需要判断是下午还是晚上,则可以将上述代码改为如下形式。
static void Main(string[] args) { const double noon=12; const double eveningTime = 19; string resultStr; //获取系统当前时间的小时部分 double now = System.DateTime.Now.Hour; Console.WriteLine("time={0}", now); //判断变量大小,用以确定是上午或下午或晚上 resultStr=(now<noon)? "现在是上午":(now<eveningTime)? "现在是下午":"现在是晚上"; Console.WriteLine("{0}", resultStr); Console.ReadKey(); }
将系统设置为12点以后,19点以前的时间。然后运行程序,结果如图2-19所示。
图2-19 程序运行结果
2.5.3 if语句
与三元运算符具有同样功能的还有if语句。所不同的是if语句没有返回值。if语句有多种形式,前面用到的有if…else是其中比较简单的一种。除此之外还有if…else if…else,或者仅仅一个单独的if语句,但是没有单独的else语句。
对if…else语句,其表达形式如下。
if(expr) { ...... } else { ...... }
expr可以是任何表达式或方法的返回结果,结果的类型必须是bool型。这和三元运算符是一致的。当返回为“true”时,执行if代码块中的语句。否则,执行else代码块中的语句。
if…else属于二选一执行。也有多选一执行的用法,这就是if…else if…else。其表达的形式如下。
if(expr1) { ...... } else if(expr2) { ...... } else { }
expr1和expr2的意义和上面是一样的。当expr1返回“true”时,执行if代码块;否则判断expr2的返回,如果是“true”则执行expr2代码块中语句;否则执行else代码块中的语句。当然这种格式并不限于三选一,还可以有更多的选择分支。只需要多加“else if”语句便可。
2.5.4 使用if语句
下面的程序IfElseTest演示了如何使用if语句。
using System; using System.Collections.Generic; using System.Text; namespace IfElseTest { class Program { static void Main(string[] args) { Console.WriteLine("请选择你目前的开发工作:"); Console.WriteLine("1.Windows桌面应用程序"); Console.WriteLine("2.Web应用程序"); Console.WriteLine("3.Web服务"); Console.Write("请输入你的选择:"); //读取用户的输入字符 string choice=Console.ReadLine(); //判断用户的输入 if (choice=="1") { Console.WriteLine("你目前的开发工作是:1.Windows桌面应用程序"); } else if (choice == "2") { Console.WriteLine("你目前的开发工作是:2.Web应用程序"); } else if (choice == "3") { Console.WriteLine("你目前的开发工作是:3.Web服务"); } else { Console.WriteLine("对不起你选择错误,下次请输入1-3之间的整数"); } Console.ReadKey(); } } }
整个程序主要是完成对用户输入的判断。其中下面的代码在控制台上列出用户选项,并提示用户输入。
Console.WriteLine("请选择你目前的开发工作:"); Console.WriteLine("1.Windows桌面应用程序"); Console.WriteLine("2.Web应用程序"); Console.WriteLine("3.Web服务"); Console.Write("请输入你的选择:");
下面定义了一个字符串变量,用于接收用户输入的字符。
string choice=Console.ReadLine();
接下来判断用户的输入是1~3中的哪一个数字字符,并输出对应项。如果输入不在1~3之间,则输出错误提示。运行代码,输入一个数字,如“1”,运行结果如图2-20所示。
图2-20 程序运行结果
2.5.5 程序流程
如图2-21所示为上节示例整个程序的流程图。
图2-21 程序流程图
相比而言,三元运算符的开发效率更高,if语句更为直观,其代码易于理解和维护。但是从开发效率的角度来看,if语句稍逊一筹。
2.5.6 switch语句
if语句在解决程序分支问题上提供了一个很好的解决方案。但是,如果选项很多,则需要很多的else if语句,这样会造成代码的不易理解和维护。在C#中还提供了一种专门解决多分支问题的语句,这就是switch语句。
“switch”在英语中的意思是“开关”,所以又叫开关语句。该语句可以一次将测试变量与多个可能值进行比较,而不是一次只能测试一个可能值。该语句的基本结构如下所示。
switch(expr) { case value1: statement1; break; case value2: statement2; break; ...... case valueN: statementN; break; default: defaultStatement; break; }
expr可以是任何整数类型或字符串,或者返回整数类型、字符串类型的表达式、方法等。value1~valueN是expr的可能取值。如果expr的值为其中之一则执行其对应的statement。如果所有value都不满足,则执行default语句对应的defaultStatement语句。break语句表示跳出整个switch语句。
2.5.7 使用switch语句
将第2.5.4节中的例子改为使用switch语句控制分支,程序名为SwitchTest,代码如下所示。
using System; using System.Collections.Generic; using System.Text; namespace SwitchTest { class Program { static void Main(string[] args) { Console.WriteLine("请选择你目前的开发工作:"); Console.WriteLine("1.Windows桌面应用程序"); Console.WriteLine("2.Web应用程序"); Console.WriteLine("3.Web服务"); Console.Write("请输入你的选择:"); //读取用户的输入字符 string choice=Console.ReadLine(); //判断用户的输入 switch(choice) { case "1": Console.WriteLine("你目前的开发工作是:1.Windows桌面应用程序"); break; //跳出循环 case "2": Console.WriteLine("你目前的开发工作是:2.Web应用程序"); break; //跳出循环 case "3": Console.WriteLine("你目前的开发工作是:3.Web服务"); break; //跳出循环 default: Console.WriteLine("对不起你选择错误,下次请输入1-3之间的整数"); break; //跳出循环 } Console.ReadKey(); } } }
程序使用switch语句将if语句部分替换了。需要注意的是每个case部分的可能变量值类型应使用双引号括起来。因为choice是一个字符串变量。运行程序并输入一个数字,如“2”,运行结果如图2-22所示。
图2-22 程序运行结果
2.5.8 goto语句
和C、C++一样,C#也支持goto语句。该语句是一种用于流程无条件转移的语句。使用该语句的前提是需要在程序中加入标签。下面是goto语句的使用格式。
...... goto <LabelName>; ...... <LabelName>: ......
其中<LabelName>是标签名。程序执行到goto语句处便会忽略goto和<LabelName>之间的语句,而转向<LabelName>后面执行。
前面的程序SwitchTest中有一个需要改进的地方,就是当用户输入的不是1~3之间的数字时,程序便自动退出了。使用goto语句可以做以下改进,改进后的程序名为GotoTest,代码如下所示。
static void Main(string[] args) { Console.WriteLine("请选择你目前的开发工作:"); Console.WriteLine("1.Windows桌面应用程序"); Console.WriteLine("2.Web应用程序"); Console.WriteLine("3.Web服务"); //设置标签 Label: Console.Write("请输入你的选择:"); //读取用户的输入字符 string choice=Console.ReadLine(); //判断用户的输入 switch(choice) { case "1": Console.WriteLine("你目前的开发工作是:1.Windows桌面应用程序"); break; //跳出循环 case "2": Console.WriteLine("你目前的开发工作是:2.Web应用程序"); break; //跳出循环 case "3": Console.WriteLine("你目前的开发工作是:3.Web服务"); break; //跳出循环 default: Console.WriteLine("对不起,你选择错误,下次请输入1-3之间的整数"); goto Label; //转向Label处执行 break; //跳出循环 } Console.ReadKey(); }
在程序中设置了标签和goto语句,当用户输入非法字符时,程序将执行default语句。输出错误信息提示后,便遇到goto语句转向Label后边的语句,提示用户重新输入。
运行程序,先输入一个字符,如“y”,程序提示错误信息;再输入一个数字,如“3”,运行结果如图2-23所示。
图2-23 程序运行结果
说明:虽然goto语句能够使程序员很方便地控制程序的转向,但是过多地使用goto语句会造成程序结构的混乱。这会让人难以读懂代码,容易导致后期的软件维护相当困难,所以建议C#语言的初学者不使用goto语句。
2.5.9 循环语句
循环语句用于解决多次重复性的计算问题,如穷举问题和迭代问题。该语句充分发挥了计算机的快速计算能力。
在C#中循环语句有do-while循环、while循环、for循环和foreach循环。下面将分别作详细介绍。
2.5.10 do-while语句
do-while是两个关键字的组合循环,其结构如下。
do { statement; }while(expr);
其中do代码块中的statement是要循环处理的语句。这些语句按照顺序执行。当执行完毕程序跳出代码块执行while语句。该语句首先判断expr返回值是true还是false,如果是true,则继续执行do代码块中的代码。一般情况下statement都和expr有一定的关联。如图2-24所示为do-while语句执行的流程图。
图2-24 do-while语句执行流程
2.5.11 使用do-while语句
下面的程序DoWhileTest中用到了do-while语句,该代码是对前面例程的改进。
using System; using System.Collections.Generic; using System.Text; namespace DoWhileTest { class Program { static void Main(string[] args) { bool flag; Console.WriteLine("请选择你目前的开发工作:"); Console.WriteLine("1.Windows桌面应用程序"); Console.WriteLine("2.Web应用程序"); Console.WriteLine("3.Web服务"); do { flag = false; Console.Write("请输入你的选择:"); //读取用户的输入字符 string choice = Console.ReadLine(); //判断用户的输入 switch (choice) { case "1": Console.WriteLine("你目前的开发工作是:1.Windows桌面应用程序"); break; case "2": Console.WriteLine("你目前的开发工作是:2.Web应用程序"); break; case "3": Console.WriteLine("你目前的开发工作是:3.Web服务"); break; default: Console.WriteLine("对不起,你选择错误,下次请输入1-3之间的整数"); flag = true; break; } }while (flag); Console.ReadKey(); } } }
上面的程序使用do-while语句替换了goto语句。实现了使用goto语句同样的功能。其中,
bool flag;
定义了一个布尔变量,用于测试循环是否结束。do代码块中的如下语句,
flag = false;
将flag设置为“false”。这样当代码没有执行到default代码块时,程序跳出switch语句便执行while判断,此时flag为“false”则跳出do-while循环。如果程序执行到default代码块,程序将会执行到下面的语句。
flag = true;
该语句将flag的值改变为“true”。这样在做while判断时就会继续返回do代码块继续执行下一轮循环。
运行程序并向控制台输入一个字符,如“a”,程序便会提示错误信息;再输入一个数字字符,如“1”,运行结果如图2-25所示。
图2-25 程序运行结果
2.5.12 while语句
while循环语句的结构如下。
while(expr) { statement; }
无论从关键字看,还是从功能上看,它和do-while语句都非常相似。唯一和do-while语句不同的是do-while语句先执行“statement”语句,然后判断是否继续循环;而while循环语句首先对expr的返回值进行判断,如果为“true”才执行代码块中的语句statement。反之,则一次也不执行。如图2-26所示为while语句执行的流程图。
图2-26 while语句执行流程图
2.5.13 使用while语句
下面的程序WhileTest使用while语句,替换了上面例程中的do-while语句。
using System; using System.Collections.Generic; using System.Text; namespace WhileTest { class Program { static void Main(string[] args) { bool flag; Console.WriteLine("请选择你目前的开发工作:"); Console.WriteLine("1.Windows桌面应用程序"); Console.WriteLine("2.Web应用程序"); Console.WriteLine("3.Web服务"); flag = true; while (flag) { flag = false; Console.Write("请输入你的选择:"); //读取用户的输入字符 string choice = Console.ReadLine(); //判断用户的输入 switch (choice) { case "1": Console.WriteLine("你目前的开发工作是:1.Windows桌面应用程序"); break; case "2": Console.WriteLine("你目前的开发工作是:2.Web应用程序"); break; case "3": Console.WriteLine("你目前的开发工作是:3.Web服务"); break; default: Console.WriteLine("对不起你选择错误,下次请输入1-3之间的整数"); flag = true; break; } } Console.ReadKey(); } } }
与程序DoWhileTest不同的是,上面代码首先需要将flag设为“true”。这样才能通过判断,执行while代码块中的语句。代码如下所示。
flag = false;
将flag设置为“false”,这样代码没有执行到“default”语句便会跳出循环。如果执行到“default”语句,代码如下所示。
flag = true;
将flag设置为“true”,从而继续循环。
运行程序,首先输入错误字符串,如“abc”,程序提示输入错误信息;然后输入一个数字字符,如“3”。得到如图2-27所示运行结果。
图2-27 程序运行结果
2.5.14 for语句
for循环语句与前面介绍的while等语句最大的不同就是,for语句可以指定循环的次数。并且主要是通过循环次数判断是否要继续循环。
下面是for语句的使用格式。
for(<初始化计数器>; <继续循环的条件>; <操作计数器>) { statement; }
<初始化计数器>在整个循环过程中只执行一次。如果<继续循环的条件>返回的是“true”,那么执行statement语句,并且执行完之后再执行<操作计数器>。否则,如果<继续循环的条件>返回的是false那么跳出循环。如图2-28所示为“for”语句流程图。
图2-28 for语句执行流程图
2.5.15 使用for语句
程序ForTest演示了for语句的使用,代码如下所示。
using System; using System.Collections.Generic; using System.Text; namespace ForTest { class Program { static void Main(string[] args) { int i, j, k; //前5行输出 for (i = 0; i <=4; i++) //控制行 { for (k = 0; k < i; k++) //输出空格 { Console.Write(" "); } for (j = 1; j <= 9-2 * i; j++) //控制列 { Console.Write("*"); } Console.WriteLine(); } //后5行输出 for (i = 4; i >= 0; i--) //控制行 { for (k = 0; k < i; k++) //输出空格 { Console.Write(" "); } for (j = 1; j <= 9-2 * i; j++) //控制列 { Console.Write("*"); } Console.WriteLine(); } Console.ReadKey(); } } }
运行程序,结果如图2-29所示。
图2-29 程序运行结果
运行结果得到一个“沙漏”图形。在程序中将该图形分为两部分输出,第一部分画“沙漏”的上半部,代码如下所示。
//前5行输出 for (i = 0; i <=4; i++) //控制行 { for (k = 0; k < i; k++) //输出空格 { Console.Write(" "); } for (j = 1; j <= 9-2 * i; j++) //控制列 { Console.Write("*"); } Console.WriteLine(); }
其中,for语句中又嵌套了两个for循环。这在C#中是允许的。上半部总共有5行,所以控制行的变量i从0取到4。嵌套的第一个for用于控制输出空格,第二个for语句用于控制第i行输出“*”的个数。有兴趣的读者可以自己研究一下,此处不再做细致分析。
接下来的语句是画“沙漏”下半部分。由于只是将上半部分倒过来,所以直接将第一部分的如下语句,
for (i = 0; i <=4; i++)
换成如下语句即可。
for (i = 4; i >= 0; i--)
2.5.16 foreach循环语句
foreach循环与for循环较为相似。主要用于循环访问各种存储数据对象的数据内容,如数组、集合等。下面的程序实例ForeachTest,演示了foreach语句循环访问数组的方法。
using System; using System.Collections.Generic; using System.Text; namespace ForeachTest { class Program { static void Main(string[] args) { int Val=1; //定义一个二维数组 int[, ] MyArry = new int[2,3] ; //初始化二维数组 for (int i = 0; i < MyArry.GetLength(0); i++) { for (int j = 0; j < MyArry.GetLength(1); j++) { //给数组元素赋值 MyArry[i, j] = Val; Val = Val + Val; } } //访问并输出数组内容 foreach (int ind in MyArry) { Console.WriteLine(ind); } Console.ReadKey(); } } }
程序使用到了数组,有关数组的具体介绍请参考第5章相关内容。程序首先定义一个二维数组MyArry,并对其进行初始化。初始化代码如下。
for (int i = 0; i < MyArry.GetLength(0); i++) { for (int j = 0; j < MyArry.GetLength(1); j++) { MyArry[i, j] = Val; Val = Val + Val; } }
接着使用foreach循环访问数组中的各元素,代码如下。
foreach (int ind in MyArry) { Console.WriteLine(ind); }
其中的ind是获取到的数组元素值。运行程序,结果如图2-30所示。
图2-30 程序运行结果
说明:foreach循环语句只能读取数据,而不能写入数据。同时不能以索引方式随机访问数据。但是foreach循环语句的执行效率比for语句略高。
在循环语句中有时并不需要按照特定的方式循环,而需要在某个情况发生时就跳出循环。这时就可以使用循环中断语句了。
2.5.17 循环中断语句
循环中断语句包括break、continue、return。
其中,break语句前面已经用到过。循环执行过程中遇到该语句,就会跳出循环,不再执行下面的语句。
2.5.18 使用break语句
下面的程序BreakTest说明了break语句的用法。
static void Main(string[] args) { for (int i = 0; i < 5; i++) { //如果i == 3 if (i == 3) { //输出i的值 Console.WriteLine("i={0}", i); //跳出整个循环 break; Console.WriteLine("Excuted! "); } if (i == 4) { //输出i的值 Console.WriteLine("i={0}", i); } } Console.ReadKey(); }
输出结果为i=3。这表明下面的语句,
if (i == 3) { Console.WriteLine("i={0}", i); break; Console.WriteLine("Excuted! "); }
中break后面的如下语句,
Console.WriteLine("Excuted! ");
和如下语句,
if (i == 4) { Console.WriteLine("i={0}", i); }
并没有被执行到。因为循环遇到break语句便跳出了循环。
2.5.19 使用continue语句
continue语句与break语句略有差异。它用于循环中的某次执行,而继续下次循环。将上述程序改为如下形式。
static void Main(string[] args) { for (int i = 0; i < 5; i++) { if (i == 3) { Console.WriteLine("i={0}", i); continue; //继续下一轮循环(如果满足循环条件) Console.WriteLine("Excuted! "); } if (i == 4) { Console.WriteLine("i={0}", i); } } Console.ReadKey(); }
运行程序,得到如图2-31所示运行结果。
图2-31 程序运行结果
结果说明下面的代码段,
if (i == 3) { Console.WriteLine("i={0}", i); continue; Console.WriteLine("Excuted! "); }
中的如下代码,
Console.WriteLine("i={0}", i);
和如下代码,
if (i == 4) { Console.WriteLine("i={0}", i); }
被执行了,而continue语句后的如下代码,
Console.WriteLine("Excuted! ");
并没有执行。也就是说,continue语句用来实现终止本次循环,而接着执行下次的循环。
2.5.20 使用return语句
最后来说明return语句,该语句主要用于结束函数的执行,并可以将需要的参量返回。可以用于包括循环语句在内的函数中的任何地方。
将一节的代码中的continue改为return,代码如下。
static void Main(string[] args) { for (int i = 0; i < 5; i++) { if (i == 3) { Console.WriteLine("i={0}", i); return; //函数返回,退出当前函数 Console.WriteLine("Excuted! "); } if (i == 4) { Console.WriteLine("i={0}", i); } } Console.ReadKey(); }
程序将在遇到return语句后立即退出整个Main函数,而返回一个void类型。表现在控制台是程序输出“i=3”便立即关闭控制台,这表明整个程序已退出。
2.6 小结
本章介绍了C#中经常用到的变量、常量、表达式,以及流程控制,其中包括它们的定义和使用,并配合了大量实例。
变量部分主要介绍了变量的声明和赋值,以及C#中简单数据类型。还对结构做了较详细的介绍,同时也介绍了程序中常遇到的类型转换。常量部分主要介绍了静态常量和动态常量及其区别。表达式部分介绍了常用的运算符,以及其使用方法。同时对命名空间作了大致的介绍。
本章的最后介绍的是流程控制语句,这也是重点介绍之一。包括了分支语句、goto语句、循环语句及循环中断语句。