第二篇 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语句、循环语句及循环中断语句。