虽然不是我写的书,但是我觉得这个很必要提供给网络爱好者,关于安装VS2005这个不用我说了
希望大家都支持我

理解了C#的用途后,就可以学习如何使用它。本章将介绍C#编程的基础知识,并假定您具备C#编程的基本知识,这是后续章节的基础。本章的主要内容如下:
●    声明变量
●    变量的初始化和作用域
●    C#的预定义数据类型
●    在C#程序中使用循环和条件语句指定执行流
●    枚举
●    命名空间
●    Main()方法
●    基本的命令行C#编译器选项
●    使用System.Console执行控制台I/O
●    在C#和Visual Studio .NET中使用文档编制功能
●    C#标识符和关键字
●    C#编程的推荐规则和约定
阅读完本章后,您就有足够的C#知识编写简单的程序了,但还不能使用继承或其他面向对象的特征。这些内容将在本书后面的几章中讨论。
2.1 引言
如前所述,C#是一种面向对象的语言。在快速浏览C#语句的基础时,我们假定您已经很好地掌握了面向对象(OO)编程的概念。换言之,我们希望您懂得类、对象、接口和继承的含义。如果以前使用过C++或Java,就应有很好的面向对象编程(OOP)的基础。但是,如果您不具备OOP的背景知识,这个主题有许多很好的信息资源。本书的附录A就详细介绍了OOP。附录A可以从
www.wrox.com上下载。
如果您对VB6、C++或 Java中的一种语言有丰富的编程经验,就应注意在介绍C#基础知识时,我们对C#、C++、Java和VB6进行了许多比较。但是,您也许愿意阅读一本有关C#和自己所选语言的比较的图书,来学习C#。如果是这样,可以从Wrox Press网站(
www.wrox.com)上下载不同的文档来学习C#。
2.2 第一个C#程序
下面采用传统的方式,看看一个最简单的C#程序——这是一个把信息写到屏幕上的控制台应用程序。
2.2.1 代码
在文本编辑器(例如Notepad)中键入下面的代码,把它保存为.cs文件(例如First.cs):
using System;

namespace Wrox.ProCSharp.Basics
{
  class MyFirstCSharpClass
  {
    static void Main()
    {
      Console.WriteLine("This isn't at all like Java!");
      Console.ReadLine();
      return;
    }
  }
}
注意:
在后面的几章中,介绍了许多代码示例。编写C#程序最常用的技巧是使用Visual Studio 2005生成一个基本项目,再把自己的代码添加进去。但是,前面几章的目的是讲授C#语言,并使过程尽可能简单,在第14章之前避免涉及Visual Studio 2005。我们使代码显示为简单的文件,这样您就可以使用任何文本编辑器键入它们,并在命令行上对其进行编译。
2.2.2 编译并运行程序
对源文件运行C#命令行编译器(csc.exe),编译这个程序:
csc First.cs
如果使用csc命令在命令行上编译代码,就应注意.NET命令行工具,包括csc,只有在设置了某些环境变量后才能使用。根据安装.NET(和Visual Studio 2005)的方式,这里显示的结果可能与您机器上的结果不同。
注意:
如果没有设置环境变量,有两种解决方法。第一种方法是在运行csc之前,在命令行上运行批处理文件%Microsoft Visual Studio 2005%\Common7\Tools\vcvars32.bat。其中%Microsoft Visual Studio 2005是安装Visual Studio 2005的文件夹。第二种方法(更简单)是使用Visual Studio 2005命令行代替通常的命令提示窗口。Visual Studio 2005命令提示在“开始”菜单—“程序”—Microsoft Visual Studio 2005-Microsoft Visual Studio Tools子菜单下。它只是一个命令提示窗口,打开时会自动运行vcvars32.bat。
编译代码,会生成一个可执行文件First.exe。在命令行或Windows Explorer上,象运行任何可执行文件那样运行该文件,得到如下结果:
csc First.cs
Microsoft (R) Visual C# .NET Compiler version 8.00.40607.16
for Microsoft (R) .NET Framework version 2.0.40607
Copyright (C) Microsoft Corporation 2001-2003. All rights reserved.
First.exe
This isn't at all like Java!
这些信息也许不那么真实!这与Java有一些非常相似的地方,但有一两个地方与Java或C++不同(如大写的Main函数)。下面通过这个程序快速介绍C#程序的基本结构。
2.2.3 详细介绍
首先对C#语法作几个解释。在C#中,与其他C风格的语言一样,每个语句都必须用一个分号(;)结尾,语句可以写在多个代码行上,不需要使用续行字符(例如VB中的下划线)。用花括号({ ... })把语句组合为块。单行注释以两个斜杠字符开头(//),多行注释以一个斜杠和一个星号(/*)开头,以一个星号和一个斜杠(*/)结尾。在这些方面,C#与C++和Java一样,但与VB不同。分号和花括号使C#代码与VB代码有完全不同的外观。如果您以前使用的是VB,就应特别注意每个语句结尾的分号。对于使用C风格语言的新用户,忽略分号常常是导致编译错误的一个最主要的原因。
在上面的代码示例中,前几行代码是处理命名空间(如本章后面所述),这是把相关类组合在一起的方式。Java和C++开发人员应很熟悉这个概念,但对于VB6开发人员来说是新概念。C#命名空间与C++命名空间或Java的包基本相同,但VB6中没有对应的概念。namespace关键字声明了应与类相关的命名空间。其后花括号中的所有代码都被认为是在这个命名空间中。编译器在using指令指定的命名空间中查找没有在当前命名空间中定义、但在代码中引用的类。这非常类似于Java中的import语句和C++中的using namespace语句。
using System;

namespace Wrox.ProCSharp.Basics
{
在First.cs文件中使用using指令的原因是下面要使用一个库类System.Console。using System指令允许把这个类简写为Console(类似于System命名空间中的其他类)。标准的System命名空间包含了最常用的.NET类型。我们用C#做的所有工作都依赖于.NET基类,认识到这一点是非常重要的;在本例中,我们使用了System命名空间中的Console类,以写入控制台窗口。
注意:
几乎所有的C#程序都使用System命名空间中的类,所以假定本章所有的代码文件都包含using System;语句。
C#没有用于输入和输出的内置关键字,而是完全依赖于.NET类。
接着,声明一个类,它表面上称为MyFirstClass。但是,因为该类位于Wrox.ProCSharp.Basics命名空间中,所以其完整的名称是Wrox.ProCSharp.Basics.MyFirstCSharpClass:
  class MyFirstCSharpClass
  {
与Java一样,所有的C#代码都必须包含在一个类中,C#中的类类似于Java和C++中的类,大致相当于VB6子句的类模块。类的声明包括class关键字,其后是类名和一对花括号。与类相关的所有代码都应放在这对花括号中。
下面声明方法Main()。每个C#可执行文件(例如控制台应用程序、Windows应用程序和Windows服务)都必须有一个入口点—— Main方法(注意M大写):
    static void Main()
    {
这个方法在程序启动时调用,类似于C++和Java中的main函数,或VB6模块中的Sub Main。该方法要么不能有返回值void,要么返回一个整数(int)。C#方法对应于C++ 和 Java中的方法(有时把C++中的方法称为成员函数),它还对应于VB的Function 或VB的Sub。这取决于方法是否有返回值(与VB不同,C#在函数和子例程之间没有概念上的区别)。
注意,C#中的方法定义如下所示。
[modifiers] return_type MethodName([parameters])
{
  // Method body. NB. This code block is pseudo-code
}
第一个方括号中的内容表示可选关键字。修饰符(modifiers)用于指定用户所定义的方法的某些特性,例如可以在什么地方调用该方法。在本例中,有两个修饰符public和static。修饰符public表示可以在任何地方访问该方法,所以可以在类的外部调用。这与C++和Java中的public相同,与VB中的Public相同。修饰符static表示方法不能在类的特定实例上执行,因此不必先实例化类再调用。这是非常重要的,因为我们创建的是一个可执行文件,而不是类库。这与C++和Java中的static关键字相同,但VB中没有对应的关键字(在VB中,Static关键字有不同的含义)。把返回类型设置为void,在本例中,不包含任何参数。
最后,看看代码语句。
      Console.WriteLine("This isn't at all like Java!");
      Console.ReadLine();
      return;
在本例中,我们只调用了System.Console类的WriteLine()方法,把一行文本写到控制台窗口上。WriteLine()是一个静态方法,在调用之前不需要实例化Console对象。
Console.ReadLine()读取用户的输入,添加这行代码会让应用程序等待用户按下回车键,之后退出应用程序。在Visual Studio 2005中,控制台窗口会消失。
然后调用return退出该方法(因为这是Main方法)。在方法的首部指定void,因此没有返回值。Return语句等价于C++和Java中的return,也等价于VB中的Exit Sub或Exit Function。
对C#基本语法有了大致的认识后,下面就要详细讨论C#的各个方面了。因为没有变量是不可能编写出任何重要的程序的,所以首先介绍C#中的变量。
2.3 变量
在C#中声明变量使用下述语法:
datatype identifier;
例如:
int i;
该语句声明int变量i。编译器不会让我们使用这个变量,除非我们用一个值初始化了该变量。但这个声明会在堆栈中给它分配4个字节,以保存其值。
声明i之后,就可以使用赋值运算符(=)给它分配一个值:
i = 10;
还可以在一行代码中声明变量,并初始化它的值:
int i = 10;
其语法与C++和Java语法相同,但与VB中声明变量的语法完全不同。如果用户以前使用的是VB6,应记住C#不区分对象和简单的类型,所以不需要类似Set的关键字,即使是要把变量指向一个对象,也不需要Set关键字。无论变量的数据类型是什么,声明变量的C#语法都是相同的。
如果在一个语句中声明和初始化了多个变量,那么所有的变量都具有相同的数据类型:
int x = 10, y =20;   // x and y are both ints
要声明类型不同的变量,需要使用单独的语句。在多个变量的声明中,不能指定不同的数据类型:
int x = 10;
bool y = true;         // Creates a variable that stores true or false
int x = 10, bool y = true;   // This won't compile!
注意上面例子中的//和其后的文本,它们是注释。//字符串告诉编译器,忽略其后的文本。本章后面会详细讨论代码中的注释。
2.3.1 变量的初始化
变量的初始化是C#强调安全性的另一个例子。简单地说,C#编译器需要用某个初始值对变量进行初始化,之后才能在操作中引用该变量。大多数现代编译器把没有初始化标记为警告,但C#编译器把它当作错误来看待。这就可以防止我们无意中从其他程序遗留下来的内存中获取垃圾值。
C#有两个方法可确保变量在使用前进行了初始化:
●    变量是类或结构中的字段,如果没有显式初始化,在默认状态下创建这些变量时,其值就是0。
●    方法的局部变量必须在代码中显式初始化,之后才能在语句中使用它们的值。此时,初始化不是在声明该变量时进行的,但编译器会通过方法检查所有可能的路径,如果检测到局部变量在初始化之前就使用了它的值,就会产生错误。
C#的方法与C++的方法相反,在C++中,编译器让程序员确保变量在使用之前进行了初始化,在VB中,所有的变量都会自动把其值设置为0。
例如,在C#中不能使用下面的语句:
public static int Main()
{
  int d;
  Console.WriteLine(d);   // Can't do this! Need to initialize d before use
  return 0;
}
注意在这段代码中,演示了如何定义Main(),使之返回一个int类型的数据,而不是void。
在编译这些代码时,会得到下面的错误消息:
Use of unassigned local variable 'd'
同样的规则也适用于引用类型。考虑下面的语句:
Something objSomething;
在C++中,上面的代码会在堆栈中创建Something类的一个实例。在C#中,这行代码仅会为Something对象创建一个引用,但这个引用还没有指向任何对象。对该变量调用方法或属性会导致错误。
在C#中实例化一个引用对象需要使用new关键字。如上所述,创建一个引用,使用new关键字把该引用指向存储在堆上的一个对象:
objSomething = new Something();   // This creates a Something on the heap
2.3.2 变量的作用域
变量的作用域是可以访问该变量的代码区域。一般情况下,确定作用域有以下规则:
●    只要字段所属的类在某个作用域内,其字段(也称为成员变量)也在该作用域内(在C++、Java和 VB中也是这样)。
●    局部变量存在于表示声明该变量的块语句或方法结束的封闭花括号之前的作用域内。
●    在for、while或类似语句中声明的局部变量存在于该循环体内(C++程序员注意,这与C++的ANSI标准相同。Microsoft C++编译器的早期版本不遵守该标准,但在循环停止后这种变量仍存在)。
1. 局部变量的作用域冲突
大型程序在不同部分为不同的变量使用相同的变量名是很常见的。只要变量的作用域是程序的不同部分,就不会有问题,也不会产生模糊性。但要注意,同名的局部变量不能在同一作用域内声明两次,所以不能使用下面的代码:
int x = 20;
// some more code
int x = 30;
考虑下面的代码示例:
using System;

namespace Wrox.ProCSharp.Basics
{
  public class ScopeTest
  {
    public static int Main()
    {
      for (int i = 0; i < 10; i++)
      {
        Console.WriteLine(i);
      }   // i goes out of scope here

      // We can declare a variable named i again, because
      // there's no other variable with that name in scope
      for (int i = 9; i >= 0; i--)
      {
        Console.WriteLine(i);
      }   // i goes out of scope here
      return 0;
    }
  }
}
这段代码使用一个for循环打印出从0~9的数字,再打印从9~0的数字。重要的是在同一个方法中,代码中的变量i声明了两次。可以这么做的原因是在两次声明中,i都是在循环内部声明的,所以变量i对于循环来说是局部变量。
下面看看另一个例子:
    public static int Main()
    {
      int j = 20;
      for (int i = 0; i < 10; i++)
      {
        int j = 30;   // Can't do this - j is still in scope
        Console.WriteLine(j + i);
      }
      return 0;
    }
如果试图编译它,就会产生如下错误:
ScopeTest.cs(12,14): error CS0136: A local variable named 'j' cannot be declared in this scope because it would give a different meaning to 'j', which is already used in a 'parent or current' scope to denote something else
其原因是:变量j是在for循环开始前定义的,在执行for循环时应处于其作用域内,在Main方法结束执行后,变量j才超出作用域,第二个j(不合法)则在循环的作用域内,该作用域嵌套在Main方法的作用域内。编译器无法区别这两个变量,所以不允许声明第二个变量。这也是与C++不同的地方,在C++中,允许隐藏变量。
2. 字段和局部变量的作用域冲突
在某些情况下,可以区分名称相同(尽管其完全限定的名称不同)、作用域相同的两个标识符。此时编译器允许声明第二个变量。原因是C#在变量之间有一个基本的区分,它把声明为类型级的变量看作是字段,而把在方法中声明的变量看作局部变量。
考虑下面的代码:
using System;

namespace Wrox.ProCSharp.Basics
{
  class ScopeTest2
  {
    static int j = 20;

    public static void Main()
    {
      int j = 30;
      Console.WriteLine(j);
      return;
    }
  }
}
即使在Main方法的作用域内声明了两个变量j,这段代码也会编译—— j被定义在类级上,在该类删除前是不会超出作用域的(在本例中,当Main方法中断,程序结束时,才会删除该类)。此时,在Main方法中声明的新变量j隐藏了同名的类级变量,所以在运行这段代码时,会显示数字30。


但是,如果要引用类级变量,该怎么办?可以使用语法object.fieldname,在对象的外部引用类的字段或结构。在上面的例子中,我们访问静态方法中的一个静态字段(静态字段详见下一节),所以不能使用类的实例,只能使用类本身的名称:

    public static void Main()
    {
      int j = 30;
      Console.WriteLine(ScopeTest2.j);
    }
    ...
如果要访问一个实例字段(该字段属于类的一个特定实例),就需要使用this关键字。this的作用与C++和Java中的this相同,与VB中的Me相同。
2.3.3 常量
在声明和初始化变量时,在变量的前面加上关键字const,就可以把该变量指定为一个常量。顾名思义,常量是其值在使用过程中不会发生变化的变量:
const int a = 100;   // This value cannot be changed
VB和C++开发人员会非常熟悉常量。但C++开发人员应注意,C#不支持C++常量的所有细微的特性。在C++中,变量不仅可以声明为常量,而且根据声明,还可以有常量指针,指向常量的变量指针、常量方法(不改变包含对象的内容),方法的常量参数等。这些细微的特性在C#中都删除了,只能把局部变量和字段声明为常量。
常量具有如下特征:
●    常量必须在声明时初始化。指定了其值后,就不能再修改了。
●    常量的值必须能在编译时用于计算。因此,不能用从一个变量中提取的值来初始化常量。如果需要这么做,应使用只读字段(详见第3章)。
●    常量总是静态的。但注意,不必(实际上,是不允许)在常量声明中包含修饰符static。
在程序中使用常量至少有3个好处:
●    常量用易于理解的清楚的名称替代了含义不明确的数字或字符串,使程序更易于阅读。
●    常量使程序更易于修改。例如,在C#程序中有一个SalesTax常量,该常量的值为6%。如果以后销售税率发生变化,把新值赋给这个常量,就可以修改所有的税款计算结果,而不必查找整个程序,修改税率为0.06的每个项。
●    常量更容易避免程序出现错误。如果要把另一个值赋给程序中的一个常量,而该常量已经有了一个值,编译器就会报告错误。
2.4 预定义数据类型
前面介绍了如何声明变量和常量,下面要详细讨论C#中可用的数据类型。与其他语言相比,C#对其可用的类型及其定义进行了过分的修饰。
2.4.1 值类型和引用类型
在开始介绍C#中的数据类型之前,理解C#把数据类型分为两种是非常重要的:
●    值类型
●    引用类型
下面几节将详细介绍值类型和引用类型的语法。从概念上看,其区别是值类型直接存储其值,而引用类型存储对值的引用。与其他语言相比,C#中的值类型基本上等价于VB或C++中的简单类型(整型、浮点型,但没有指针或引用)。引用类型与VB中的引用类型相同,与C++中通过指针访问的类型类似。
这两种类型存储在内存的不同地方:值类型存储在堆栈中,而引用类型存储在托管堆上。注意区分某个类型是值类型还是引用类型,因为这种存储位置的不同会有不同的影响。例如,int是值类型,这表示下面的语句会在内存的两个地方存储值20:
// i and j are both of type int
i = 20;
j = i;
但考虑下面的代码。这段代码假定已经定义了一个类Vector,Vector是一个引用类型,它有一个int类型的成员变量Value:
Vector x, y
x = new Vector ();
x.Value = 30;   // Value is a field defined in Vector class
y = x;
Console.WriteLine(y.Value);
y.Value = 50;
Console.WriteLine(x.Value);
要理解的重要一点是在执行这段代码后,只有一个Vector对象。x和y都指向包含该对象的内存位置。因为x和y是引用类型的变量,声明这两个变量只是保留了一个引用——而不会实例化给定类型的对象。这与在C++中声明指针和VB中的对象引用是相同的——在C++和VB中,都不会创建对象。要创建对象,就必须使用new关键字,如上所示。因为x和y引用同一个对象,所以对x的修改会影响y,反之亦然。因此上面的代码会显示30和50。
注意:
C++开发人员应注意,这个语法类似于引用,而不是指针。我们使用.(句点)符号,而不是->来访问对象成员。在语法上,C#引用看起来更类似于C++引用变量。但是,抛开表面的语法,实际上它类似于C++指针。
如果变量是一个引用,就可以把其值设置为null,表示它不引用任何对象:
y = null;
这类似于Java中把引用设置为null,C++中把指针设置为NULL,或VB中把对象引用设置为Nothing。如果将引用设置为null,显然就不可能对它调用任何非静态的成员函数或字段,这么做会在运行时抛出一个异常。
在像C++这样的语言中,开发人员可以选择是直接访问某个给定的值,还是通过指针来访问。VB的限制更多:COM对象是引用类型,简单类型总是值类型。C#在这方面类似于VB:变量是值还是引用仅取决于其数据类型,所以,int总是值类型。不能把int变量声明为引用(在第5章介绍装箱时,可以在类型为object的引用中封装值类型)。
在C#中,基本数据类型如bool和long都是值类型。如果声明一个bool变量,并给它赋予另一个bool变量的值,在内存中就会有两个bool值。如果以后修改第一个bool变量的值,第二个bool变量的值也不会改变。这些类型是通过值来复制的。
相反,大多数更复杂的C#数据类型,包括我们自己声明的类都是引用类型。它们分配在堆中,其生存期可以跨多个函数调用,可以通过一个或几个别名来访问。CLR执行一种精细的算法,来跟踪哪些引用变量仍是可以访问的,哪些引用变量已经不能访问了。CLR会定期进行清理,删除不能访问的对象,把它们占用的内存返回给操作系统。这是通过垃圾收集器实现的。
把基本类型(如int和bool)规定为值类型,而把包含许多字段的较大类型(通常在有类的情况下)规定为引用类型,C#设计这种方式的原因是可以得到最佳性能。如果要把自己的类型定义为值类型,就应把它声明为一个结构。
2.4.2 CTS类型
如第1章所述,C#认可的基本预定义类型并没有内置于语言中,而是内置于.NET Framework中。例如,在C#中声明一个int类型的数据时,声明的实际上是.NET结构System.Int32的一个实例。这听起来似乎很深奥,但其意义深远:这表示在语法上,可以把所有的基本数据类型看作是支持某些方法的类。例如,要把int i转换为string,可以编写下面的代码:
string s = i.ToString();
应强调的是,在这种便利语法的背后,类型实际上仍存储为基本类型。基本类型在概念上用.NET结构表示,所以肯定没有性能损失。
下面看看C#中定义的类型。我们将列出每个类型,以及它们的定义和对应.NET类型(CTS 类型)的名称。C#有15个预定义类型,其中13个是值类型,2个是引用类型(string和object)。
2.4.3 预定义的值类型
内置的值类型表示基本数据类型,例如整型和浮点类型、字符类型和bool类型。
1. 整型
C#支持8个预定义整数类型,如表2-1所示。
表 2-1
名   称    CTS 类 型    说   明    范   围
sbyte    System.SByte    8位有符号的整数
–128 到 127 (–27到27–1)
short    System.Int16    16位有符号的整数
–32 768 到 32 767 (–215到215–1)
int    System.Int32    32位有符号的整数
–2 147 483 648 到 2 147 483 647(–231到231–1)
long    System.Int64    64位有符号的整数
–9 223 372 036 854 775 808到9 223 372 036 854 775 807(–263到263–1)
byte    System.Byte    8位无符号的整数
0到255(0到28–1)
ushort    System.Uint16    16位无符号的整数
0到65535(0到216–1)
uint    System.Uint32    32位无符号的整数
0到4 294 967 295(0到232–1)
ulong    System.Uint64    64位无符号的整数
0到18 446 744 073 709 551 615(0到264–1)

Windows的将来版本将支持64位处理器,可以把更大的数据块移入移出内存,获得更快的处理速度。因此,C#支持8至64位的有符号和无符号的整数。
当然,VB开发人员会发现有许多类型名称是新的。C++和Java开发人员应注意:一些C#类型名称与C++和Java类型一致,但类型有不同的定义。例如,在C#中,int总是32位带符号的整数,而在C++中,int是带符号的整数,但其位数取决于平台(在Windows上是32位)。在C#中,所有的数据类型都以与平台无关的方式定义,以备将来C#和.NET迁移到其他平台上。
byte是0~255(包括255)的标准8位类型。注意,在强调类型的安全性时,C#认为byte类型和char类型完全不同,它们之间的编程转换必须显式写出。还要注意,与整数中的其他类型不同,byte类型在默认状态下是无符号的,其有符号的版本有一个特殊的名称sbyte。
在.NET中,short不再很短,现在它有16位,Int类型更长,有32位。 long类型最长,有64位。所有整数类型的变量都能赋予10进制或16进制的值,后者需要0x前缀:
long x = 0x12ab;
如果对一个整数是int、uint、long或是ulong没有任何显式的声明,则该变量默认为int类型。为了把键入的值指定为其他整数类型,可以在数字后面加上如下字符:
uint ui = 1234U;
long l = 1234L;
ulong ul = 1234UL;
也可以使用小写字母u和l,但后者会与整数1混淆。
2. 浮点类型
C#提供了许多整型数据类型,也支持浮点类型,如表2-2所示。C和C++程序员很熟悉   它们。


表 2-2
名称    CTS类型    说 明    位 数    范围 (大致)
float    System.Single    32位单精度浮点数
7    ±1.5 × 10-45 到 ±3.4 × 1038
double    System.Double    64位双精度浮点数
15/16    ±5.0 × 10-324到 ±1.7 × 10308

float数据类型用于较小的浮点数,因为它要求的精度较低。double数据类型比float数据类型大,提供的精度也大一倍(15位)。
如果在代码中没有对某个非整数值(如12.3)硬编码,则编译器一般假定该变量是double。如果想指定值为float,可以在其后加上字符F(或f):
float f = 12.3F;
3. decimal类型
另外,decimal类型表示精度更高的浮点数,如表2-3所示。
表 2-3
名   称    CTS类型    说   明    位   数    范围(大致)
decimal    System.
Decimal    128位高精度十进制数表示法    28    ±1.0×10-28到±7.9 × 1028

CTS和C#一个重要的优点是提供了一种专用类型表示财务计算,这就是decimal类型,使用decimal类型提供的28位的方式取决于用户。换言之,可以用较大的精确度(带有美分)来表示较小的美元值,也可以在小数部分用更多的舍入来表示较大的美元值。但应注意,decimal类型不是基本类型,所以在计算时使用该类型会有性能损失。
要把数字指定为decimal类型,而不是double、 float或整型,可以在数字的后面加上字符M(或m),如下所示。
decimal d = 12.30M;
4. bool类型
C#的 bool 类型用于包含bool值true或false,如表2-4所示。
表 2-4
名   称    CTS 类 型    值
bool    System.Boolean    true或false

bool值和整数值不能相互转换。如果变量(或函数的返回类型)声明为bool类型,就只能使用值true或false。如果试图使用0表示false,非0值表示true,就会出错。
5. 字符类型
为了保存单个字符的值,C#支持char数据类型,如表2-5所示。
表 2-5
名   称    CTS 类 型    值
char    System.Char    表示一个16位的(Unicode)字符

虽然这个数据类型在表面上类似于C和C++中的char类型,但它们有重大区别。C++的char表示一个8位字符,而C#的char包含16位。其部分原因是不允许在char类型与8位byte类型之间进行隐式转换。
尽管8位足够编码英语中的每个字符和数字0~9了,但它们不够编码更大的符号系统中的每个字符(例如中文)。为了面向全世界,计算机行业正在从8位字符集转向16位的Unicode模式,ASCII编码是Unicode的一个子集。
char类型的字面量是用单引号括起来的,例如'A'。如果把字符放在双引号中,编译器会把它看作是字符串,从而产生错误。
除了把char表示为字符字面量之外,还可以用4位16进制的Unicode值(例如'\u0041'),带有数据类型转换的整数值(例如(char)65),或16进制数('\x0041')表示它们。它们还可以用转义序列表示,如表2-6所示。
表 2-6
转 义 序 列    字   符
\'    单引号
\"    双引号
\\    反斜杠
\0    空
\a    警告
\b    退格
\f    换页
\n    换行
\r    回车
\t    水平制表符
\v    垂直制表符

C++开发人员应注意,因为C#本身有一个string类型,所以不需要把字符串表示为char类型的数组。
2.4.4 预定义的引用类型
C#支持两个预定义的引用类型,如表2-7所示。

表 2-7
名   称    CTS 类    说   明
object    System.Object    根类型,CTS中的其他类型都是从它派生而来的(包括值类型)
string    System.String    Unicode字符串
1. object类型
许多编程语言和类结构都提供了根类型,层次结构中的其他对象都从它派生而来。C#和.NET也不例外。在C#中,object类型就是最终的父类型,所有内在和用户定义的类型都从它派生而来。这是C#的一个重要特性,它把C#与VB和C++区分开来,但其行为与Java非常类似。所有的类型都隐含地最终派生于System.Object类,这样,object类型就可以用于两个目的:
●    可以使用object引用绑定任何子类型的对象。例如,第5章将说明如何使用object类型把堆栈中的一个值对象装箱,再移动到堆中。对象引用也可以用于反射,此时必须有代码来处理类型未知的对象。这类似于C++中的void指针或VB中的Variant数据类型。
●    object类型执行许多基本的一般用途的方法,包括Equals()、GetHashCode()、GetType()和ToString()。用户定义的类需要使用一种面向对象技术—— 重写(见第4章),提供其中一些方法的替代执行代码。例如,重写ToString()时,要给类提供一个方法,提供类本身的字符串表示。如果类中没有提供这些方法的实现代码,编译器就会使用object类型中的实现代码,它们在类中的执行不一定正确。
后面的章节将详细讨论object类型。
2. string类型
有C和C++开发经验的人员可能在使用C风格的字符串时不太顺利。C或C++字符串不过是一个字符数组,因此客户机程序员必须做许多工作,才能把一个字符串复制到另一个字符串上,或者连接两个字符串。实际上,对于一般的C++程序员来说,执行包装了这些操作细节的字符串类是一个非常头痛的耗时过程。VB程序员的工作就比较简单,只需使用string类型即可。而Java程序员就更幸运了,其String类在许多方面都类似于C#字符串。
C#有string关键字,在翻译为.NET类时,它就是System.String。有了它,像字符串连接和字符串复制这样的操作就很简单了:
string str1 = "Hello ";
string str2 = "World";
string str3 = str1 + str2; // string concatenation
尽管这是一个值类型的赋值,但string是一个引用类型。String对象保留在堆上,而不是堆栈上。因此,当把一个字符串变量赋给另一个字符串时,会得到对内存中同一个字符串的两个引用。但是,string与引用类型在常见的操作上有一些区别。例如,修改其中一个字符串,就会创建一个全新的string对象,而另一个字符串没有改变。考虑下面的代码:
using System;

class StringExample
{
  public static int Main()
  {
    string s1 = "a string";
    string s2 = s1;
    Console.WriteLine("s1 is " + s1);
    Console.WriteLine("s2 is " + s2);
    s1 = "another string";
    Console.WriteLine("s1 is now " + s1);
    Console.WriteLine("s2 is now " + s2);
    return 0;
  }
}
其输出结果为:
s1 is a string
s2 is a string
s1 is now another string
s2 is now a string
换言之,改变s1的值对s2没有影响,这与我们期待的引用类型正好相反。当用值"a string"初始化s1时,就在堆上分配了一个string对象。在初始化s2时,引用也指向这个对象,所以s2的值也是"a string"。但是现在要改变s1的值,而不是替换原来的值时,堆上就会为新值分配一个新对象。s2变量仍指向原来的对象,所以它的值没有改变。这实际上是运算符重载的结果,运算符重载详见第5章。基本上,string类实现为其语义遵循一般的、直观的字符串规则。
字符串字面量放在双引号中("...");如果试图把字符串放在单引号中,编译器就会把它当作char,从而引发错误。C#字符串和char一样,可以包含Unicode、16进制数转义序列。因为这些转义序列以一个反斜杠开头,所以不能在字符串中使用这个非转义的反斜杠字符。而需要用两个反斜杠字符(\\)来表示它:
string filepath = "C:\\ProCSharp\\First.cs";
即使用户相信自己可以在任何情况下都记住要这么做,但键入两个反斜杠字符会令人迷惑。幸好,C#提供了另一种替代方式。可以在字符串字面量的前面加上字符@,在这个字符后的所有字符都看作是其原来的含义——它们不会解释为转义字符:
string filepath = @"C:\ProCSharp\First.cs";
甚至允许在字符串字面量中包含换行符:
string jabberwocky = @"'Twas brillig and the slithy toves
Did gyre and gimble in the wabe.";
那么jabberwocky的值就是:
'Twas brillig and the slithy toves
Did gyre and gimble in the wabe.
2.5 流控制
本节将介绍C#语言的重要语句:控制程序流的语句,它们不是按代码在程序中的排列位置顺序执行的。
2.5.1 条件语句
条件语句可以根据条件是否满足或根据表达式的值控制代码的执行分支。C#有两个分支代码的结构:if语句,测试特定条件是否满足;switch语句,它比较表达式和许多不同的值。
1. if语句
对于条件分支,C#继承了C和C++的if...else结构。对于用过程语言编程的人来说,其语法是非常直观的:
if (condition)
  statement(s)
else
  statement(s)
如果在条件中要执行多个语句,就需要用花括号({ ... })把这些语句组合为一个块。(这也适用于其他可以把语句组合为一个块的C#结构,例如for和while循环)。
bool isZero;
if (i == 0)
{
  isZero = true;
  Console.WriteLine("i is Zero");
}
else
{
  isZero = false;
  Console.WriteLine("i is Non-zero");
}
其语法与C++和Java类似,但与VB不同。VB开发人员注意,C#中没有与VB的EndIf对应的语句,其规则是if的每个子句都只包含一个语句。如果需要多个语句,如上面的例子所示,就应把这些语句放在花括号中,这会把整组语句当作一个语句块来处理。
还可以单独使用if语句,不加else语句。也可以合并else if子句,测试多个条件。
using System;

namespace Wrox.ProCSharp.Basics
{
  class MainEntryPoint
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Type in a string");
      string input;
      input = Console.ReadLine();
      if (input == "")
      {
        Console.WriteLine("You typed in an empty string");
      }
      else if (input.Length < 5)
      {
        Console.WriteLine("The string had less than 5 characters");
      }
      else if (input.Length < 10)
      {
        Console.WriteLine("The string had at least 5 but less than 10
          characters");
      }
      Console.WriteLine("The string was " + input);
    }
  }
}
添加到if子句中的else if语句的个数没有限制。
注意在上面的例子中,我们声明了一个字符串变量input,让用户在命令行上输入文本,把文本填充到input中,然后测试该字符串变量的长度。代码还说明了在C#中如何进行字符串处理。例如,要确定input的长度,可以使用input.Length。
对于if,要注意的一点是如果条件分支中只有一条语句,就无需使用花括号:
if (i == 0)
  Console.WriteLine("i is Zero");            // This will only execute if i == 0
Console.WriteLine("i can be anything");        // Will execute whatever the
                               // value of i
但是,为了保持一致,许多程序员只要使用if语句,就使用花括号。
前面介绍的if语句还演示了比较值的一些C#运算符。特别注意,与C++和Java一样,C#使用“==”对变量进行等于比较。此时不要使用“=”,“=”用于赋值。
在C#中,if子句中的表达式必须等于布尔值。C++程序员应特别注意这一点;与C++不同,C#中的if语句不能直接测试整数(例如从函数中返回的值),而必须明确地把返回的整数转换为布尔值true 或 false,例如,比较值0和null:
if (DoSomething() != 0)
{
  // Non-zero value returned
}
else
{
  // Returned zero
}
这个限制用于防止C++中某些常见的运行错误,特别是在C++中,当应使用“==”时,常常误输入“=”,导致不希望的赋值。在C#中,这常常会导致一个编译错误,因为除非在处理bool值,否则“=”不会返回bool。
2. switch语句
switch…case语句适合于从一组互斥的分支中选择一个执行分支。C++和Java程序员应很熟悉它,该语句类似于VB中的Select Case语句。
其形式是switch参数的后面跟一组case子句。如果switch参数中表达式的值等于某个case子句旁边的某个值,就执行该case子句中的代码。此时不需要使用花括号把语句组合到块中;只需使用break语句标记每个case代码的结尾即可。也可以在switch语句中包含一个default子句,如果表达式不等于任何case子句的值,就执行default子句的代码。下面的switch语句测试integerA变量的值:
switch (integerA)
{
  case 1:
    Console.WriteLine("integerA =1");
    break;
  case 2:
    Console.WriteLine("integerA =2");
    break;
  case 3:
    Console.WriteLine("integerA =3");
    break;
  default:
    Console.WriteLine("integerA is not 1,2, or 3");
    break;
}
注意case的值必须是常量表达式——不允许使用变量。
C和C++程序员应很熟悉switch…case语句,而C#的switch…case语句更安全。特别是它禁止所有case中的失败条件。如果激活了块中靠前的一个case子句,后面的case子句就不会被激活,除非使用goto语句特别标记要激活后面的case子句。编译器会把没有break语句的每个case子句标记为错误:
Control cannot fall through from one case label ('case 2:') to another
在有限的几种情况下,这种失败是允许的,但在大多数情况下,我们不希望出现这种失败,而且这会导致出现很难察觉的逻辑错误。让代码正常工作,而不是出现异常,这样不是更好吗?
但在使用goto语句时(C#支持),会在switch…cases中重复出现失败。如果确实想这么做,就应重新考虑设计方案了。下面的代码说明了如何使用goto模拟失败,得到的代码会非常混乱:
// assume country and language are of type string
switch(country)
{
  case "America":
    CallAmericanOnlyMethod();
    goto case "Britain";
  case "France":
    language = "French";
    break;
  case "Britain":
    language = "English";
    break;
}
但这有一种例外情况。如果一个case子句为空,就可以从这个case跳到下一个case上,这样就可以用相同的方式处理两个或多个case子句了(不需要goto语句)。
switch(country)
{
  case "au":
  case "uk":
  case "us":
    language = "English";
    break;
  case "at":
  case "de":
    language = "German";
    break;
}
在C#中,switch语句的一个有趣的地方是case子句的排放顺序是无关紧要的,甚至可以把default子句放在最前面!因此,任何两个case都不能相同。这包括值相同的不同常量,所以不能这样编写:
// assume country is of type string
const string england = "uk";
const string britain = "uk";
switch(country)
{
  case england:
  case britain:     // this will cause a compilation error
    language = "English";
    break;
}
上面的代码还说明了C#中的switch语句与C++中的switch语句的另一个不同之处:在C#中,可以把字符串用作测试变量。
2.5.2 循环
C#提供了4种不同的循环机制(for、while、do...while和foreach),在满足某个条件之前,可以重复执行代码块。for、while和do...while循环与C++中的对应循环相同。
1. for循环
C#的for循环提供的迭代循环机制是在执行下一次迭代前,测试是否满足某个条件,其语法如下:
for (initializer; condition; iterator)
  statement(s)
其中:
●    initializer是指在执行第一次迭代前要计算的表达式(通常把一个局部变量初始化为循环计数器);
●    condition是在每次迭代新循环前要测试的表达式(它必须等于true,才能执行下一次迭代);
●    iterator是每次迭代完要计算的表达式(通常是递增循环计数器)。当condition等于false时,迭代停止。
for循环是所谓的预测试循环,因为循环条件是在执行循环语句前计算的,如果循环条件为假,循环语句就根本不会执行。
for循环非常适合于一个语句或语句块重复执行预定的次数。下面的例子就是for循环的典型用法,这段代码输出从0~99的整数:
for (int i = 0; i < 100; i = i+1)   // this is equivalent to
                  // For i = 0 To 99 in VB.
{
  Console.WriteLine(i);
}
这里声明了一个int类型的变量i,并把它初始化为0,用作循环计数器。接着测试它是否小于100。因为这个条件等于true,所以执行循环中的代码,显示值0。然后给该计数器加1,再次执行该过程。当i等于100时,循环停止。
实际上,上述编写循环的方式并不常用。C#在给变量加1时有一种简化方式,即不使用i = i+1,而简写为i++:
for (int i = 0; i < 100; i++)
{
//etc.
C#的for循环语法比VB中的For…Next循环的功能强大得多,因为迭代器可以是任何语句。在VB中,只能对循环控制变量加减某个数字。在C#中,则可以做任何事,例如,让循环控制变量乘以2。
嵌套的for循环非常常见,在每次迭代外部的循环时,内部循环都要彻底执行完毕。这种模式通常用于在矩形多维数组中遍历每个元素。最外部的循环遍历每一行,内部的循环遍历某行上的每个列。下面的代码显示数字行,它还使用另一个Console方法Console.Write(),该方法的作用与Console.WriteLine()相同,但不在输出中添加回车换行符:
using System;

namespace Wrox.ProCSharp.Basics
{
  class MainEntryPoint
  {
    static void Main(string[ ] args)
    {
      // This loop iterates through rows...
      for (int i = 0; i < 100; i+=10)
      {
        // This loop iterates through columns...
        for (int j = i; j < i + 10; j++)
        {
          Console.Write(" " + j);
        }
        Console.WriteLine();
      }
    }
  }
}
尽管j是一个整数,但它会自动转换为字符串,以便进行连接。C++开发人员要注意,这比在C++中处理字符串容易得多,VB开发人员则已经习惯于此了。
C程序员应注意上述例子中的一个特殊功能。在每次迭代后续的外部循环时,最内部循环的计数器变量都要重新声明。这种语法不仅在C#中可行,在C++中也是合法的。
上述例子的结果是:
csc NumberTable.cs
Microsoft (R) Visual C# .NET Compiler version 8.00.40607.16
for Microsoft (R) .NET Framework version 2.0.40607
Copyright (C) Microsoft Corporation 2001-2003. All rights reserved.

0 1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29
30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49
50 51 52 53 54 55 56 57 58 59
60 61 62 63 64 65 66 67 68 69
70 71 72 73 74 75 76 77 78 79
80 81 82 83 84 85 86 87 88 89
90 91 92 93 94 95 96 97 98 99
尽管在技术上,可以在for循环的测试条件中计算其他变量,而不计算计数器变量,但这不太常见。也可以在for循环中忽略一个表达式(甚或所有表达式)。但此时,要考虑使用while循环。
2. while循环
while循环与C++和Java中的while循环相同,与VB中的While...Wend循环相同。与for循环一样,while也是一个预测试的循环。其语法是类似的,但while循环只有一个表达式:
while(condition)
  statement(s);
与for循环不同的是,while循环最常用于下述情况:在循环开始前,不知道重复执行一个语句或语句块的次数。通常,在某次迭代中,while循环体中的语句把布尔标记设置为false,结束循环,如下面的例子所示。
bool condition = false;
while (!condition)
{
  // This loop spins until the condition is true
  DoSomeWork();
  condition = CheckCondition();   // assume CheckCondition() returns a bool
}
所有的C#循环机制,包括while循环,如果只重复执行一条语句,而不是一个语句块,都可以省略花括号。许多程序员都认为最好在任何情况下都加上花括号。
3. do…while循环
do...while循环是while循环的后测试版本。它与C++和Java中的do...while循环相同,与VB中的Loop...While循环相同,该循环的测试条件要在执行完循环体之后执行。因此do...while循环适合于至少执行一次循环体的情况:
bool condition;
do
{
  // this loop will at least execute once, even if Condition is false
  MustBeCalledAtLeastOnce();
  condition = CheckCondition();
} while (condition);
4. foreach循环
foreach循环是我们讨论的最后一种C#循环机制。其他循环机制都是C和C++的最早期版本,而foreach语句是新增的循环机制(借用于VB),也是非常受欢迎的一种循环。
foreach循环可以迭代集合中的每一项。现在不必考虑集合的概念,第9章将介绍集合。知道集合是一种包含其他对象的对象即可。从技术上看,要使用集合对象,它必须支持IEnumerable接口。集合的例子有C#数组、System.Collection命名空间中的集合类,以及用户定义的集合类。从下面的代码中可以了解foreach循环的语法,其中假定arrayOfInts是一个整型数组:
foreach (int temp in arrayOfInts)
{
  Console.WriteLine(temp);
}
其中,foreach循环每次迭代数组中的一个元素。它把每个元素的值放在int型的变量temp中,然后执行一次循环迭代。
注意,不能改变集合中各项(上面的temp)的值,所以下面的代码不会编译:
foreach (int temp in arrayOfInts)
{
  temp++;
  Console.WriteLine(temp);
}
如果需要迭代集合中的各项,并改变它们的值,就应使用for循环。
2.5.3 跳转语句
C#提供了许多可以立即跳转到程序中另一行代码的语句,在此,先介绍goto语句。
1. goto语句
goto语句可以直接跳转到程序中用标签指定的另一行(标签是一个标识符,后跟一个冒号):
goto Label1;
  Console.WriteLine("This won't be executed");
Label1:
  Console.WriteLine("Continuing execution from here");
goto语句有两个限制。不能跳转到像for循环这样的代码块中,也不能跳出类的范围,不能退出try...catch块后面的finally块(第12章将介绍如何用try...catch...finally块处理异常)。
goto语句的名声不太好,在大多数情况下不允许使用它。一般情况下,使用它肯定不是面向对象编程的好方式。但是有一个地方使用它是相当方便的——在switch语句的case子句之间跳转,这是因为C#的switch语句在故障处理方面非常严格。前面介绍了其语法。
2. break语句
前面简要提到过break语句——在switch语句中使用它退出某个case语句。实际上,break也可以用于退出for、foreach、while或do...while循环,循环结束后,就执行循环后面的语句。
如果该语句放在嵌套的循环中,就执行最内部循环后面的语句。如果break放在switch语句或循环外部,就会产生编译时错误。
3. continue语句
continue语句类似于break,也必须在for、foreach、while或 do...while循环中使用。但它只从循环的当前迭代中退出,然后在循环的下一次迭×××始重新执行,而不是退出循环。
4. return语句
return语句用于退出类的方法,把控制返回方法的调用者,如果方法有返回类型,return语句必须返回这个类型的值,如果方法没有返回类型,应使用没有表达式的return语句。
2.6 枚举
枚举是用户定义的整数类型。在声明一个枚举时,要指定该枚举可以包含的一组可接受的实例值。不仅如此,还可以给值指定易于记忆的名称。如果在代码的某个地方,要试图把一个不在可接受范围内的值赋予枚举的一个实例,编译器就会报告一个错误。这个概念对于VB程序员来说是新的。C++支持枚举,但C#枚举要比C++枚举强大得多。
从长远来看,创建枚举可以节省大量的时间,减少许多麻烦。使用枚举比使用无格式的整数至少有如下三个优势:
●    如上所述,枚举可以使代码更易于维护,有助于确保给变量指定合法的、期望的值。
●    枚举使代码更清晰,允许用描述性的名称表示整数值,而不是用含义模糊的数来表示。
●    枚举使代码更易于键入。在给枚举类型的实例赋值时,VS .NET IDE会通过IntelliSense弹出一个包含可接受值的列表框,减少了按键次数,并能够让我们回忆起可选的值。
定义如下的枚举:
public enum TimeOfDay
{
  Morning = 0,
  Afternoon = 1,
  Evening = 2
}
在本例中,在枚举中使用一个整数值,来表示一天的每个阶段。现在可以把这些值作为枚举的成员来访问。例如,TimeOfDay.Morning返回数字0。使用这个枚举一般是把合适的值传送给方法,在switch语句中迭代可能的值。
class EnumExample
{
  public static int Main()
  {
    WriteGreeting(TimeOfDay.Morning);
    return 0;
  }

  static void WriteGreeting(TimeOfDay timeOfDay)
  {
    switch(timeOfDay)
    {
      case TimeOfDay.Morning:
        Console.WriteLine("Good morning!");
        break;
      case TimeOfDay.Afternoon:
        Console.WriteLine("Good afternoon!");
        break;
      case TimeOfDay.Evening:
        Console.WriteLine("Good evening!");
        break;
      default:
        Console.WriteLine("Hello!");
        break;
    }
  }
}
在C#中,枚举的真正强大之处是它们在后台会实例化为派生于基类System.Enum的结构。这表示可以对它们调用方法,执行有用的任务。注意因为.NET Framework的执行方式,在语法上把枚举当做结构是不会有性能损失的。实际上,一旦代码编译好,枚举就成为基本类型,与int和float类似。
可以获取枚举的字符串表示,例如使用前面的TimeOfDay枚举:
TimeOfDay time = TimeOfDay.Afternoon;
Console.WriteLine(time.ToString());
会返回字符串Afternoon。
另外,还可以从字符串中获取枚举值:
TimeOfDay time2 = (TimeOfDay) Enum.Parse(typeof(TimeOfDay), "afternoon", true);
Console.WriteLine((int)time2);
这段代码说明了如何从字符串获取枚举值,并转换为整数。要从字符串中转换,需要使用静态的Enum.Parse()方法,这个方法带3个参数,第一个参数是要使用的枚举类型。其句法是关键字typeof后跟放在括号中的枚举类名。typeof运算符将在第5章详细论述。第二个参数是要转换的字符串,第三个参数是一个bool,指定在进行转换时是否忽略大小写。最后,注意Enum.Parse()方法实际上返回一个对象引用—— 我们需要把这个字符串显式转换为需要的枚举类型(这是一个拆箱操作的例子)。对于上面的代码,将返回1,作为一个对象,对应于TimeOfDay. Afternoon的枚举值。在显式转换为int时,会再次生成1。
System.Enum上的其他方法可以返回枚举定义中的值的个数、列出值的名称等。详细信息参见MSDN文档。
2.7 数组
本章不打算详细介绍数组,因为第9章将详细论述数组和集合。但本章将介绍编写一维数组的句法。在声明C#中的数组时,要在各个元素的变量类型后面,加上一组方括号(注意数组中的所有元素必须有相同的数据类型)。
提示:
VB用户注意,C#中的数组使用方括号,而不是圆括号。C++用户很熟悉方括号,但应仔细查看这里给出的代码,因为声明数组变量的C#语法与C++语法并不相同。
例如,int表示一个整数,而int[]表示一个整型数组:
int[] integers;
要初始化特定大小的数组,可以使用new关键字,在类型名后面的方括号中给出大小:
// Create a new array of 32 ints
int[] integers = new int[32];
所有的数组都是引用类型,并遵循引用的语义。因此,即使各个元素都是基本的值类型,integers数组也是引用类型。如果以后编写如下代码:
int[] copy = integers;
该代码也只是把变量copy指向同一个数组,而不是创建一个新数组。
要访问数组中的单个元素,可以使用通常的语法,在数组名的后面,把元素的下标放在方括号中。所有的C#数组都使用基于0的下标方式,所以要用下标0引用第一个变量:
integers[0] = 35;
同样,用下标值31引用有32个元素的数组中的最后一个元素:
integers[31] = 432;
C#的数组句法也非常灵活,实际上,C#可以在声明数组时不进行初始化,这样以后就可以在程序中动态地指定其大小。利用这项技术,可以创建一个空引用,以后再使用new关键字把这个引用指向请求动态分配的内存位置:
int[] integers;
integers = new int[32];
可以使用下面的语法查看一个数组包含多少个元素:
int numElements = integers.Length;   // integers is any reference to an array
2.8 命名空间
如前所述,命名空间提供了一种组织相关类和其他类型的方式。与文件或组件不同,命名空间是一种逻辑组合,而不是物理组合。在C#文件中定义类时,可以把它包括在命名空间定义中。以后,在定义另一个类,在另一个文件中执行相关操作时,就可以在同一个命名空间中包含它,创建一个逻辑组合,告诉使用类的其他开发人员:这两个类是如何相关的以及如何使用它们:
namespace CustomerPhoneBookApp
{
  using System;

  public struct Subscriber
  {
    // Code for struct here...
  }
}
把一个类型放在命名空间中,可以有效地给这个类型指定一个较长的名称,该名称包括类型的命名空间,后面是句点(.)和类的名称。在上面的例子中,Subscriber结构的全名是CustomerPhoneBookApp.Subscriber。这样,有相同短名的不同的类就可以在同一个程序中使用了。
也可以在命名空间中嵌套其他命名空间,为类型创建层次结构:
namespace Wrox
{
  namespace ProCSharp
  {
    namespace Basics
    {
      class NamespaceExample
      {
        // Code for the class here...
      }
    }
  }
}
每个命名空间名都由它所在命名空间的名称组成,这些名称用句点分隔开,首先是最外层的命名空间,最后是它自己的短名。所以ProCSharp命名空间的全名是Wrox.ProCSharp,NamespaceExample类的全名是Wrox.ProCSharp.Basics.NamespaceExample。
使用这个语法也可以组织自己的命名空间定义中的命名空间,所以上面的代码也可以写为:
namespace Wrox.ProCSharp.Basics
{
  class NamespaceExample
  {
    // Code for the class here...
  }
}
注意不允许在另一个嵌套的命名空间中声明多部分的命名空间。
命名空间与程序集无关。同一个程序集中可以有不同的命名空间,也可以在不同的程序集中定义同一个命名空间中的类型。
2.8.1 using语句
显然,命名空间相当长,键入起来很繁琐,用这种方式指定某个类也是不必要的。如本章开头所述,C#允许简写类的全名。为此,要在文件的顶部列出类的命名空间,前面加上using关键字。在文件的其他地方,就可以使用其类型名称来引用命名空间中的类型了:
using System;
using Wrox.ProCSharp;
如前所述,所有的C#源代码都以语句using System;开头,这仅是因为Microsoft提供的许多有用的类都包含在System命名空间中。
如果using指令引用的两个命名空间包含同名的类,就必须使用完整的名称(或者至少较长的名称),确保编译器知道访问哪个类型,例如,类NamespaceExample同时存在于Wrox. ProCSharp.Basics和Wrox.ProCSharp.OOP命名空间中,如果要在命名空间Wrox.ProCSharp中创建一个类Test,并在该类中实例化一个NamespaceExample类,就需要指定使用哪个类:
using Wrox.ProCSharp;

class Test
{
  public static int Main()
  {
    Basics.NamespaceExample nSEx = new Basics.NamespaceExample();
    //do something with the nSEx variable
    return 0;
  }
}  
因为using语句在C#文件的开头,C和C++也把#include语句放在这里,所以从C++迁移到C#的程序员常把命名空间与C++风格的头文件相混淆。不要犯这种错误,using语句在这些文件之间并没有真正建立物理链接。C#也没有对应于C++头文件的部分。
公司应花一定的时间开发一种命名空间模式,这样其开发人员才能快速定位他们需要的功能,而且公司内部使用的类名也不会与外部的类库相冲突。本章后面将介绍建立命名空间模式的规则和其他命名约定。
2.8.2 命名空间的别名
using关键字的另一个用途是给类和命名空间指定别名。如果命名空间的名称非常长,又要在代码中使用多次,但不希望该命名空间的名称包含在using指令中(例如,避免类名冲突),就可以给该命名空间指定一个别名,其语法如下:
using alias = NamespaceName;
下面的例子(前面例子的修订版本)给Wrox.ProCSharp.Basics命名空间指定Introduction别名,并使用这个别名实例化了一个NamespaceExample对象,这个对象是在该命名空间中定义的。注意命名空间别名的修饰符是::。因此将先从Introduction命名空间别名开始搜索。如果在相同的作用域中引入了一个Introduction类,就会发生冲突。即使出现了冲突,::操作符也允许引用别名。NamespaceExample类有一个方法GetNamespace(),该方法调用每个类都有的GetType()方法,以访问表示类的类型的Type对象。下面使用这个对象来返回类的命名空间名:
using System;
using Introduction = Wrox.ProCSharp.Basics;
class Test
{
  public static int Main()
  {
    Introduction::NamespaceExample NSEx =
      new Introduction::NamespaceExample();
    Console.WriteLine(NSEx.GetNamespace());
    return 0;
  }
}  
 
namespace Wrox.ProCSharp.Basics
{
  class NamespaceExample
  {
    public string GetNamespace()
    {
      return this.GetType().Namespace;
    }
  }
}
2.9 Main()方法
本章的开头提到过,C#程序是从方法Main()开始执行的。这个方法必须是类或结构的静态方法,并且其返回类型必须是int或void。
虽然显式指定public修饰符是很常见的,因为按照定义,必须在程序外部调用该方法,但我们给该方法指定什么访问级别并不重要,即使把该方法标记为private,它也可以运行。
2.9.1 多个Main()方法
在编译C#控制台或Windows应用程序时,默认情况下,编译器会在与上述签名匹配的类中查找Main方法,并使这个类方法成为程序的入口。如果有多个Main方法,编译器就会返回一个错误,例如,考虑下面的代码MainExample.cs:
using System;

namespace Wrox.ProCSharp.Basics
{
  class Client
  {
    public static int Main()
    {
      MathExample.Main();
      return 0;
    }
  }

  class MathExample
  {
    static int Add(int x, int y)
    {
      return x + y;
    }

    public static int Main()
    {
      int i = Add(5,10);
      Console.WriteLine(i);
      return 0;
    }
  }
}
上述代码中包含两个类,它们都有一个Main()方法。如果按照通常的方式编译这段代码,就会得到下述错误:
csc MainExample.cs
Microsoft (R) Visual C# .NET Compiler version 8.00.40607.16
for Microsoft (R) .NET Framework version 2.00.40607
Copyright (C) Microsoft Corporation 2001-2003. All rights reserved.

MainExample.cs(7,23): error CS0017: Program 'MainExample.exe' has more than one entry point defined: 'Wrox.ProCSharp.Basics.Client.Main()'
MainExample.cs(21,23): error CS0017: Program 'MainExample.exe' has more than one entry point defined: 'Wrox.ProCSharp.Basics.MathExample.Main()'
但是,可以使用/main选项,其后跟Main()方法所属类的全名(包括命名空间),明确告诉编译器把哪个方法作为程序的入口点:
csc MainExample.cs /main:Wrox.ProCSharp.Basics.MathExample
2.9.2 给Main()方法传送参数
前面的例子只介绍了不带参数的Main()方法。但在调用程序时,可以让CLR包含一个参数,将命令行参数转送给程序。这个参数是一个字符串数组,传统称为args(但C#可以接受任何名称)。在启动程序时,可以使用这个数组,访问通过命令行传送过来的选项。
下面的例子ArgsExample.cs是在传送给Main方法的字符串数组中迭代,并把每个选项的值写入控制台窗口:
using System;

namespace Wrox.ProCSharp.Basics
{
  class ArgsExample
  {
    public static int Main(string[] args)
    {
      for (int i = 0; i < args.Length; i++)
      {
        Console.WriteLine(args
);
      }
      return 0;
    }
  }
}
通常使用命令行就可以编译这段代码。在运行编译好的可执行文件时,可以在程序名的后面加上参数,例如:
ArgsExample /a /b /c
/a
/b
/c
2.10 有关编译C#文件的更多内容
前面介绍了如何使用csc.exe编译控制台应用程序,但其他类型的应用程序应如何编译?如果要引用一个类库,该怎么办?MSDN文档介绍了C#编译器的所有编译选项,这里只介绍其中最重要的选项。
要回答第一个问题,应使用/target选项(常简写为/t)来指定要创建的文件类型。文件类型可以是表2-8所示的类型中的一种。
表 2-8
选   项    输   出
/t:exe    控制台应用程序 (默认)
/t:library    带有清单的类库
/t:module    没有清单的组件
/t:winexe    Windows应用程序 (没有控制台窗口)

如果想得到一个可由.NET运行库加载的非可执行文件(例如DLL),就必须把它编译为一个库。如果把C#文件编译为一个模块,就不会创建任何程序集。虽然模块不能由运行库加载,但可以使用/addmodule选项编译到另一个清单中。
另一个需要注意的选项是/out,该选项可以指定由编译器生成的输出文件名。如果没有指定/out选项,编译器就会使用输入的C#文件名,加上目标类型的扩展名来建立输出文件名(例如.exe表示Windows或控制台应用程序,.dll表示类库)。注意/out和/t(或/target)选项必须放在要编译的文件名前面。
默认状态下,如果在未引用的程序集中引用类型,可以使用/reference或/r选项,后跟程序集的路径和文件名。下面的例子说明了如何编译类库,并在另一个程序集中引用这个库。它包含两个文件:
●    类库
●    控制台应用程序,该应用程序调用库中的一个类。
第一个文件MathLibrary.cs包含DLL的代码,为了简单起见,它只包含一个公共类Math和一个方法,该方法把两个int类型的数据加在一起:
namespace Wrox.ProCSharp.Basics
{
  public class MathLib
  {
    public int Add(int x, int y)
    {
      return x + y;
    }
  }
}
使用下述命令把这个C#文件编译为. NET DLL:
csc /t:library MathLibrary.cs
控制台应用程序MathClient.cs将简单地实例化这个对象,调用其Add方法,在控制台窗口中显示结果:
using System;

namespace Wrox.ProCSharp.Basics
{
  class Client
  {
    public static void Main()
    {
      MathLib mathObj = new MathLib();
      Console.WriteLine(mathObj.Add(7,8));
    }
  }
}
使用/r选项编译这个文件,使之指向新编译的DLL:
csc MathClient.cs /r:MathLibrary.dll
当然,下面就可以像往常一样运行它了:在命令提示符上输入MathClient,其结果是显示数字15—— 加运算的结果。
2.11 控制台I/O
现在,您应基本熟悉了C#的数据类型以及如何在操作这些数据类型的程序中完成任务。本章还要使用Console类的几个静态方法来读写数据,这些方法在编写基本的C#程序时非常有效,下面就详细介绍它们。
要从控制台窗口中读取一行文本,可以使用Console.ReadLine()方法,它会从控制台窗口中取一个输入流(在用户按下回车键时停止),并返回输入的字符串。写入控制台也有两个对应的方法,前面已经使用过它们:
●    Console. Write()方法将指定的值写入控制台窗口。
●    Console.WriteLine()方法类似,但在输出结果的最后添加一个换行符。
所有预定义类型(包括object) 都有这些函数的各种形式(重载),所以在大多数情况下,在显示值之前不必把它们转换为字符串。
例如,下面的代码允许用户输入一行文本,并显示该文本:
string s = Console.ReadLine();
Console.WriteLine(s);
Console.WriteLine()还允许用与C的printf()函数类似的方式显示格式化的结果。要以这种方式使用WriteLine(),应传入许多参数。第一个参数是花括号中包含标记的字符串,在这个花括号中,要把后续的参数插入到文本中。每个标记都包含一个基于0的索引,表示列表中参数的序号。例如,"{0}"表示列表中的第一个参数,所以下面的代码:
int i = 10;
int j = 20;
Console.WriteLine("{0} plus {1} equals {2}", i, j, i + j);
会显示:
10 plus 20 equals 30
也可以为值指定宽度,调整文本在该宽度中的位置,正值表示右对齐,负值表示左对齐。为此可以使用格式{n,w},其中n是参数索引,w是宽度值。
int i = 940;
int j = 73;
Console.WriteLine(" {0,4}\n+{1,4}\n––––\n {2,4}", i, j, i + j);
结果如下:
940
+ 73
   
1013
最后,还可以添加一个格式字符串,和一个可选的精度值。这里没有列出格式字符串的完整列表,因为如第8章所述,我们可以定义自己的格式字符串。但用于预定义类型的主要格式字符串如表2-9所示。

表 2-9
字 符 串    说   明
C    本地货币格式
D    十进制格式,把整数转换为以10为基数的数,如果给定一个精度说明符,就加上前导0
E    科学计数法(指数)格式。精度说明符设置小数位数(默认为6)。格式字符串的大小写("e" 或 "E")确定指数符号的大小写
F    固定点格式,精度说明符设置小数位数,可以为0
G    普通格式,使用E 或 F格式取决于哪种格式较简单
N    数字格式,用逗号表示千分符,例如32,767.44
P    百分数格式
X    16进制格式,精度说明符用于加上前导0

注意格式字符串都不需要考虑大小写,除e/E之外。
如果要使用格式字符串,应把它放在给出参数个数和字段宽度的标记后面,并用一个冒号把它们分隔开。例如,要把decimal值格式化为货币格式,且使用计算机上的地区设置,其精度为两位小数,则使用C2:
decimal i = 940.23m;
decimal j = 73.7m;
Console.WriteLine(" {0,9:C2}\n+{1,9:C2}\n ––––––\n {2,9:C2}", i, j, i + j);
在美国,其结果是:
  $940.23
+   $73.70
             
$1,013.93
最后一个技巧是,可以使用占位符来代替这些格式字符串,例如:
double d = 0.234;
Console.WriteLine("{0:#.00}", d);
其结果为0.23,因为如果在符号(#)的位置上没有字符,就会忽略该符号(#),如果0的位置上有一个字符,就用这个字符代替0,否则就显示0。
2.12 使用注释
本节的内容表面上看起来很简单——给代码添加注释。
2.12.1 源文件中的内部注释
在本章开头提到过,C#使用传统的C风格注释方式:单行注释使用// ...,多行注释使用   /* ... */:
// This is a single-line comment
/* This comment
spans multiple lines */
单行注释中的任何内容,即//后面的内容都会被编译器忽略。多行注释中/* 和 */之间的所有内容也会被忽略。显然不能在多行注释中包含*/组合,因为这会被当作注释的结尾。
实际上,可以把多行注释放在一行代码中:
Console.WriteLine(/*Here's a comment! */ "This will compile");
像这样的内联注释在使用时应小心,因为它们会使代码难以理解。但这样的注释在调试时是非常有用的,例如,在运行代码时要临时使用另一个值:
DoSomething(Width, /*Height*/ 100);
当然,字符串字面值中的注释字符会按照一般的字符来处理:
string s = "/* This is just a normal string */";
2.12.2 XML文档说明
如前所述,除了C风格的注释外,C#还有一个非常好的功能,本章将讨论这一功能。根据特定的注释自动创建XML格式的文档说明。这些注释都是单行注释,但都以3个斜杠(///)开头,而不是通常的两个斜杠。在这些注释中,可以把包含类型和类型成员的文档说明的XML标识符放在代码中。
编译器可以识别表2-10中所示的标识符。
表 2-10
标 识 符    说   明
<c>    把行中的文本标记为代码,例如<c>int i = 10;</c>
<code>    把多行标记为代码
<example>    标记为一个代码示例
<exception>    说明一个异常类(编译器要验证其语法)
<include>    包含其他文档说明文件的注释(编译器要验证其语法)
<list>    把列表插入到文档说明中
<param>    标记方法的参数(编译器要验证其语法)
<paramref>    表示一个单词是方法的参数(编译器要验证其语法)
<permission>    说明对成员的访问(编译器要验证其语法)
<remarks>    给成员添加描述
<returns>    说明方法的返回值
<see>    提供对另一个参数的交叉引用(编译器要验证其语法)
<seealso>    提供描述中的“参见”部分(编译器要验证其语法)
<summary>    提供类型或成员的简短小结
<value>    描述属性

要了解它们的工作方式,可以在上一节的MathLibrary.cs文件中添加一些XML注释,并称之为Math.cs。我们给类及其Add方法添加一个<summary>注释,也给Add方法添加一个<returns>元素和两个<param>元素:
// Math.cs
namespace Wrox.ProCSharp.Basics
{

  ///<summary>
  ///   Wrox.ProCSharp.Basics.Math class.
  ///   Provides a method to add two integers.
  ///</summary>
  public class Math
  {
    ///<summary>
    ///   The Add method allows us to add two integers
    ///</summary>
    ///<returns>Result of the addition (int)</returns>
    ///<param name="x">First number to add</param>
    ///<param name="y">Second number to add</param>
    public int Add(int x, int y)
    {
      return x + y;
    }
  }
}
C#编译器可以把XML元素从特定的注释中提取出来,并使用它们生成一个XML文件。要让编译器为程序集生成XML文档说明,需在编译时指定/doc选项,其后需跟上要创建的文件名:
csc /t:library /doc:Math.xml Math.cs
如果XML注释没有生成格式正确的XML文档,编译器就生成一个错误。
上面的代码会生成一个XML文件Math.xml,如下所示。
<?xml version="1.0"?>
<doc>
  <assembly>
    <name>Math</name>
  </assembly>
  <members>
    <member name="T:Wrox.ProCSharp.Basics.Math">
      <summary>
        Wrox.ProCSharp.Basics.Math class.
        Provides a method to add two integers.
      </summary>
    </member>
    <member name=
        "M:Wrox.ProCSharp.Basics.Math.Add(System.Int32,System.Int32)">
      <summary>
        The Add method allows us to add two integers
      </summary>
      <returns>Result of the addition (int)</returns>
      <param name="x">First number to add</param>
      <param name="y">Second number to add</param>
    </member>
  </members>
</doc>
注意,编译器为我们做了一些工作——它创建了一个<assembly>元素,并为该文件中的每个类型或类型成员添加一个<member>元素。每个<member>元素都有一个name特性,其中包含成员的全名,前面有一个字母表示其类型:"T:"表示这是一个类型,"F:" 表示这是一个字段,"M:" 表示这是一个成员。
2.13 C#预处理器指令
除了前面介绍的常用关键字外,C#还有许多名为“预处理器指令”的命令。这些命令从来不会转化为可执行代码中的命令,但会影响编译过程的各个方面。例如,使用预处理器指令可以禁止编译器编译代码的某一部分。如果计划发布两个版本的代码,即基本版本和有更多功能的企业版本,就可以使用这些预处理器指令。在编译软件的基本版本时,使用预处理器指令还可以禁止编译器编译与额外功能相关的代码。另外,在编写提供调试信息的代码时,也可以使用预处理器指令。实际上,在销售软件时,一般不希望编译这部分代码。
预处理器指令的开头都有符号#。
注意:
C++开发人员应知道在C和C++中,预处理器指令是非常重要的,但是,在C#中,并没有那么多的预处理器指令,它们的使用也不太频繁。C#提供了其他机制来实现许多C++指令的功能,例如定制特性。还要注意,C#并没有一个像C++那样的独立预处理器,所谓的预处理器指令实际上是由编译器处理的。尽管如此,C#仍保留了一些预处理器指令,因为这些命令对预处理器有一定的影响。
下面简要介绍预处理器指令的功能。
2.13.1 #define和 #undef
#define的用法如下所示:
#define DEBUG
它告诉编译器存在给定名称的符号,在本例中是DEBUG。这有点类似于声明一个变量,但这个变量并没有真正的值,只是存在而已。这个符号不是实际代码的一部分,而只在编译器编译代码时存在。在C#代码中它没有任何意义。
#undef正好相反—— 删除符号的定义:
#undef DEBUG
如果符号不存在,#undef就没有任何作用。同样,如果符号已经存在,#define也不起作用。
必须把#define和#undef命令放在C#源代码的开头,在声明要编译的任何对象的代码之前。
#define本身并没有什么用,但当与其他预处理器指令(特别是#if)结合使用时,它的功能就非常强大了。
注意:
这里应注意一般的C#语法的一些变化。预处理器指令不用分号结束,一般是一行上只有一个命令。这是因为对于预处理器指令,C#不再要求命令用分号结束。如果它遇到一个预处理器指令,就会假定下一个命令在下一行上。
2.13.2 #if, #elif, #else和#endif
这些指令告诉编译器是否要编译某个代码块。考虑下面的方法:
  int DoSomeWork(double x)
  {
    // do something
    #if DEBUG
      Console.WriteLine("x is " + x);
    #endif
  }
这段代码会像往常那样编译,但Console.WriteLine命令包含在#if子句内。这行代码只有在前面的#define命令定义了符号DEBUG后才执行。当编译器遇到#if语句后,将先检查相关的符号是否存在,如果符号存在,就只编译#if块中的代码。否则,编译器会忽略所有的代码,直到遇到匹配的#endif指令为止。一般是在调试时定义符号DEBUG,把不同的调试相关代码放在#if子句中。在完成了调试后,就把#define语句注释掉,所有的调试代码会奇迹般地消失,可执行文件也会变小,最终用户不会被这些调试信息弄糊涂(显然,要做更多的测试,确保代码在没有定义DEBUG的情况下也能工作)。这项技术在C和C++编程中非常普通,称为条件编译(conditional compilation)。
#elif (=else if)和#else指令可以用在#if块中,其含义非常直观。也可以嵌套#if块:
#define ENTERPRISE
#define W2K

// further on in the file

#if ENTERPRISE
  // do something
  #if W2K
    // some code that is only relevant to enterprise
    // edition running on W2K
  #endif
#elif PROFESSIONAL
  // do something else
#else
  // code for the leaner version
#endif
注意:
与C++中的情况不同,使用#if不是条件编译代码的惟一方式,C#还通过Conditional特性提供了另一种机制,详见第11章。
#if和 #elif还支持一组逻辑运算符!、==、!=和 ||。如果符号存在,就被认为是true,否则为false,例如:
#if W2K && (ENTERPRISE==false)   // if W2K is defined but ENTERPRISE isn't
2.13.3 #warning和# error
另外两个非常有用的预处理器指令是#warning和#error,当编译器遇到它们时,会分别产生一个警告或错误。如果编译器遇到#warning指令,会给用户显示#warning指令后面的文本,之后编译继续进行。如果编译器遇到#error指令,就会给用户显示后面的文本,作为一个编译错误信息,然后会立即退出编译,不会生成IL代码。
使用这两个指令可以检查#define语句是不是做错了什么事,使用#warning语句可以让自己想起做过什么事:
#if DEBUG && RELEASE
  #error "You've defined DEBUG and RELEASE simultaneously! "
#endif

#warning "Don't forget to remove this line before the boss tests the code! "
  Console.WriteLine("*I hate this job*");
2.13.4 #region和#endregion
#region和 #endregion指令用于把一段代码标记为有给定名称的一个块,如下所示。
#region Member Field Declarations
  int x;
  double d;
  Currency balance;
#endregion
这看起来似乎没有什么用,它不影响编译过程。这些指令的优点是它们可以被某些编辑器识别,包括Visual Studio .NET编辑器。这些编辑器可以使用这些指令使代码在屏幕上更好地布局。第14章会详细介绍它们。
2.13.5 #line
#line指令可以用于改变编译器在警告和错误信息中显示的文件名和行号信息。这个指令用得并不多。如果编写代码时,在把代码发送给编译器前,要使用某些软件包改变键入的代码,就可以使用这个指令,因为这意味着编译器报告的行号或文件名与文件中的行号或编辑的文件名不匹配。#line指令可以用于恢复这种匹配。也可以使用语法#line default把行号恢复为默认的行号:
#line 164 "Core.cs"   // we happen to know this is line 164 in the file
            // Core.cs, before the intermediate
            // package mangles it.

// later on

#line default     // restores default line numbering
2.13.6 #pragma
#pragma指令可以抑制或恢复指定的编译警告。与命令行选项不同,#pragma指令可以在类或方法上执行,对抑制什么警告和抑制的时间进行更精细的控制。下面的例子禁止字段使用警告,然后在编译MyClass类后恢复该警告。
#pragma warning disable 169
public class MyClass
{
int neverUsedField;
}
#pragma warning restore 169
2.14 C#编程规则
本节介绍编写C#程序时应注意的规则。
2.14.1 用于标识符的规则
本节将讨论变量、类、方法等的命名规则。注意本节所介绍的规则不仅是规则,也是C#编译器强制使用的。
标识符是给变量、用户定义的类型(例如类和结构)和这些类型的成员指定的名称。标识符区分大小写,所以interestRate 和 InterestRate是不同的变量。确定在C#中可以使用什么标识符有两个规则:
●    它们必须以一个字母或下划线开头,但可以包含数字字符;
●    不能把C#关键字用作标识符。
C#包含如表2-11所示的保留关键字。
表 2-11
abstract    do    In    protected    true
as    double    Int    public    try
base    else    Interface    readonly    typeof
bool    enum    Internal    ref    uint
break    event    Is    return    ulong
byte    explicit    lock    sbyte    unchecked
case    extern    long    sealed    unsafe
catch    false    namespace    short    ushort
char    finally    new    sizeof    using
checked    fixed    null    stackalloc    virtual
class    float    object    static    volatile
const    for    operator    string    void
continue    foreach    out    struct    while
decimal    goto    override    switch    
default    if    params    this    
delegate    Implicit    private    throw    

如果需要把某一保留字用作标识符(例如,访问一个用另一种语言编写的类),可以在标识符的前面加上前缀@符号,指示编译器其后的内容是一个标识符,而不是C#关键字(所以abstract不是有效的标识符,而@abstract是)。
最后,标识符也可以包含Unicode字符,用语法\uXXXX来指定,其中XXXX是Unicode字符的四位16进制代码。下面是有效标识符的一些例子:
●    Name
●    überfluß
●    _Identifier
●    \u005fIdentifier
最后两个标识符是相同的,可以互换(005f是下划线字符的Unicode代码),所以在相同的作用域内不要声明两次。注意虽然从语法上看,标识符中可以使用下划线字符,但在大多数情况下,最好不要这么做,因为它不符合Microsoft的变量命名规则,这种命名规则可以确保开发人员使用相同的命名规则,易于阅读每个人编写的代码。
2.14.2 用法约定
在任何开发环境中,通常有一些传统的编程风格。这些风格不是语言的一部分,而是约定,例如,变量如何命名,类、方法或函数如何使用等。如果使用某语言的大多数开发人员都遵循相同的约定,不同的开发人员就很容易理解彼此的代码,有助于程序的维护。例如,Visual Basic 6的一个公共(但不统一)约定是,表示字符串的变量名以小写字母s或str开头,如Dim sResult As String或 Dim strMessage As String。约定主要取决于语言和环境。例如,在Windows平台上编程的C++开发人员一般使用前缀psz或 lpsz表示字符串:char *pszResult; char *lpszMessage;,但在UNIX机器上,则不使用任何前缀:char *Result; char *Message;。
从本书中的示例代码中可以总结出,C#中的约定是命名变量时不使用任何前缀:string Result; string Message;。
注意:
用带有前缀字母的变量名来表示某个数据类型,这种约定称为Hungarian表示法。这样,其他阅读该代码的开发人员就可以立即从变量名中了解它代表什么数据类型。在有了智能编辑器和IntelliSense之后,人们普遍认为Hungarian表示法是多余的。
但是,在许多语言中,用法约定是从语言的使用过程中逐渐演变而来的,Microsoft编写的C#和整个.NET Framework都有非常多的用法约定,详见.NET/C# MSDN文档说明。这说明,从一开始,.NET程序就有非常高的互操作性,开发人员可以以此来理解代码。用法规则还得益于20年来面向对象编程的发展,因此相关的新闻组已经仔细考虑了这些用法规则,而且已经为开发团体所接受。所以我们应遵守这些约定。
但要注意,这些规则与语言规范是不同的。用户应尽可能遵循这些规则。但如果有很好的理由不遵循它们,也不会有什么问题。例如,不遵循这些用法约定,也不会出现编译错误。一般情况下,如果不遵循用法规则,就必须有一个说得过去的理由。规则应是一个正确的决策,而不是让人头痛的东西。在阅读本书的后续内容时,应注意到在本书的许多示例中,都没有遵循该约定,这通常是因为某些规则适用于大型程序,而不适合于本书中的小示例。如果编写一个完整的软件包,就应遵循这些规则,但它们并不适合于只有20行代码的独立程序。在许多情况下,遵循约定会使这些示例难以理解。
编程风格的规则非常多。这里只介绍一些比较重要的规则,以及最适合于用户的规则。如果用户要让代码完全遵循用法规则,就需要参考MSDN文档说明。
1. 命名约定
使程序易于理解的一个重要方面是给对象选择命名的方式,包括变量名、方法名、类名、枚举名和命名空间的名称。
显然,这些名称应反映对象的功能,且不与其他名称冲突。在.NET Framework中,一般规则也是变量名要反映变量实例的功能,而不是反映数据类型。例如,Height就是一个比较好的变量名,而IntegerValue就不太好。但是,这种规则是一种理想状态,很难达到。在处理控件时,大多数情况下使用ConfirmationDialog 和 ChooseEmployeeListBox等变量名比较好,这些变量名说明了变量的数据类型。
名称的约定包括以下几个方面:
(1) 名称的大小写
在许多情况下,名称都应使用Pascal大小写命名形式。 Pascal 大小写形式是指名称中单词的第一个字母大写: EmployeeSalary, ConfirmationDialog, PlainTextEncoding。注意,命名空间、类、以及基类中的成员等的名称都应遵循该规则,最好不要使用带有下划线字符的单词,即名称不应是employee_salary。其他语言中常量的名称常常全部都是大写,但在C#中最好不要这样,因为这种名称很难阅读,而应全部使用Pascal 大小写形式的命名约定:
  const int MaximumLength;
我们还推荐使用另一种大小写模式:camel大小写形式。这种形式类似于Pascal 大小写形式,但名称中第一个单词的第一个字母不是大写:employeeSalary、confirmationDialog、plainTextEncoding。有三种情况可以使用camel大小写形式。
●    类型中所有私有成员字段的名称都应是camel大小写形式:
public int subscriberId;
但要注意成员字段名常常用一个下划线开头:
public int _subscriberId;
●    传递给方法的所有参数都应是camel大小写形式:
public void RecordSale(string salesmanName, int quantity);
●    camel大小写形式也可以用于区分同名的两个对象—— 比较常见的情况是属性封装一个字段:
private string employeeName;

public string EmployeeName
{
get  
  {
    return employeeName;
  }
}
如果这么做,则私有成员总是使用camel大小写形式,而公共的或受保护的成员总是使用Pascal 大小写形式,这样使用这段代码的其他类就只能使用Pascal 大小写形式的名称了(除了参数名以外)。
还要注意大小写问题。C#是区分大小写的,所以在C#中,仅大小写不同的名称在语法上是正确的,如上面的例子。但是,程序集可能在VB .NET应用程序中调用,而VB .NET是不区分大小写的,如果使用仅大小写不同的名称,就必须使这两个名称不能在程序集的外部访问。(上例是可行的,因为仅私有变量使用了camel大小写形式的名称)。否则,VB .NET中的其他代码就不能正确使用这个程序集。

(2) 名称的风格
名称的风格应保持一致。例如,如果类中的一个方法叫ShowConfirmationDialog(),其他方法就不能叫ShowDialogWarning()或 WarningDialogShow(),而应是ShowWarningDialog()。
(3) 命名空间的名称
命名空间的名称非常重要,一定要仔细设计,以避免一个命名空间中对象的名称与其他对象同名。记住,命名空间的名称是.NET区分共享程序集中对象名的惟一方式。如果软件包的命名空间使用的名称与另一个软件包相同,而这两个软件包都安装在一台计算机上,就会出问题。因此,最好用自己的公司名创建顶级的命名空间,再嵌套后面技术范围较窄、用户所在小组或部门、或类所在软件包的命名空间。Microsoft建议使用如下的命名空间:<CompanyName>. <TechnologyName>,例如:
WeaponsOfDestructionCorp.RayGunControllers
WeaponsOfDestructionCorp.Viruses
(4) 名称和关键字
名称不应与任何关键字冲突,这是非常重要的。实际上,如果在代码中,试图给某个对象指定与C#关键字同名的名称,就会出现语法错误,因为编译器会假定该名称表示一个语句。但是,由于类可能由其他语言编写的代码访问,所以不能使用其他.NET语言中的关键字作为对象的名称。一般说来,C++关键字类似于C#关键字,不太可能与C++混淆,Visual C++常用的关键字则用两个下划线字符开头。与C#一样,C++关键字都是小写字母,如果要遵循公共类和成员使用Pascal风格的名称的约定,则在它们的名称中至少有一个字母是大写,因此不会与C++关键字冲突。另一方面,VB的问题会多一些,因为VB的关键字要比C#的多,而且它不区分大小写,不能依赖于Pascal风格的名称来区分类和成员。
表2-12列出了VB中的关键字和标准函数调用,无论对C#公共类使用什么大小写组合,这些名称都不应使用。
表 2-12
   
Abs    Do    Loc    RGB
Add    Double    Local    Right
AddHandler    Each    Lock    RmDir
AddressOf    Else    LOF    Rnd
And    Empty    Long    SaveSettings
Ansi    End    Loop    Second
AppActivate    Enum    LTrim    Seek
Append    EOF    Me    Select
As    Erase    Mid    SetAttr
Asc    Err    Minute    SetException
Assembly    Error    MIRR    Shared
Atan    Event    MkDir    Shell
(续表)  
Auto    Exit    Module    Short
Beep    Exp    Month    Sign
Binary    Explicit    MustInherit    Sin
BitAnd    ExternalSource    MustOverride    Single
BitNot    False    MyBase    SLN
BitOr    FileAttr    MyClass    Space
BitXor    FileCopy    Namespace    Spc
Boolean    FileDateTime    New    Split
ByRef    FileLen    Next    Sqrt
Byte    Filter    Not    Static
ByVal    Finally    Nothing    Step
Call    Fix    NotInheritable    Stop
Case    For    NotOverridable    Str
Catch    Format    Now    StrComp
CBool    FreeFile    NPer    StrConv
CByte    Friend    NPV    Strict
CDate    Function    Null    String
CDbl    FV    Object    Structure
CDec    Get    Oct    Sub
ChDir    GetAllSettings    Off    Switch
ChDrive    GetAttr    On    SYD
Choose    GetException    Open    SyncLock
Chr    GetObject    Option    Tab
CInt    GetSetting    Optional    Tan
Class    GetType    Or    Text
Clear    GoTo    Overloads    Then
CLng    Handles    Overridable    Throw
Collection    Hour    ParamArray    Timer
Command    If    Pmt    TimeSerial
Compare    Iif    PPmt    TimeValue
Const    Implements    Preserve    To
Cos    Imports    Print    Today
CreateObject    In    Private    Trim
CShort    Inherits    Property    Try
CSng    Input    Public    TypeName
CStr    InStr    Put    TypeOf
(续表)  
CurDir    Int    PV    UBound
Date    Integer    QBColor    UCase
DateAdd    Interface    Raise    Unicode
DateDiff    Ipmt    RaiseEvent    Unlock
DatePart    IRR    Randomize    Until
DateSerial    Is    Rate    Val
DateValue    IsArray    Read    Weekday
Day    IsDate    ReadOnly    While
DDB    IsDbNull    ReDim    Width
Decimal    IsNumeric    Remove    With
Declare    Item    RemoveHandler    WithEvents
Default    Kill    Rename    Write
Delegate    Lcase    Replace    WriteOnly
DeleteSetting    Left    Reset    Xor
Dim    Lib    Resume    Year
Dir    Line    Return    

2. 属性和方法的使用
类中出现混乱的一个方面是一个数是用属性还是方法来表示。这没有硬性规定,但一般情况下,如果该对象的外观和操作都像一个变量,就应使用属性来表示它(属性详见第3章),即:
●    客户机代码应能读取它的值,最好不要使用只写属性,例如,应使用SetPassword()方法,而不是Password只写属性。
●    读取该值不应花太长的时间。实际上,如果它是一个属性,通常表示读取过程花的时间相对较短。
●    读取该值不应有任何不希望的负面效应。设置属性的值,不应有与该属性不直接相关的负面效应。设置对话框的宽度会改变该对话框在屏幕上的外观,这是可以的,因为它与属性是相关的。
●    应可以用任何顺序设置属性。在设置属性时,最好不要因为还没有设置另一个相关的属性而抛出一个异常。例如,如果为了使用访问数据库的类,需要设置ConnectionString、UserName和Password,应确保了已经执行了该类,这样用户才能按照任何顺序设置它们。
●    顺序读取属性也应有相同的效果。如果属性的值可能会出现预料不到的改变,就应把它编写为一个方法。在监视汽车运动的类中,把speed编写为属性就不是一种好的方式,而应使用GetSpeed(),另一方面,应把Weight 和EngineSize编写为属性,因为对于给定的对象,它们是不会改变的。
如果要编码的对象满足上述所有条件,就应对它使用属性,否则就应使用方法。
3. 字段的用法
字段的用法非常简单。字段应总是私有的,但在某些情况下也可以把常量或只读字段设置为公有,原因是如果把字段设置为公有,就可以在以后扩展或修改类。
遵循上面的规则就可以编写出好的代码,而且这些规则应与面向对编程的风格一起使用。
Microsoft在保持一致性方面相当谨慎,在编写.NET基类时就可以遵循它自己的规则。在编写.NET代码时应很好地遵循这些规则,对于基类来说,就是类、成员、命名空间的命名方式和类层次结构的工作方式等,如果编写代码的风格与基类的编写风格相同,就不会犯什么错误。
2.15 小结
本章介绍了一些C#基本语法,包括编写简单的C#程序需要掌握的内容。我们讲述了许多基础知识,但其中有许多是熟悉C风格语言(甚或JavaScript)的开发人员能立即领悟的。本章的主要内容包括:
●    变量的作用域和访问级别
●    声明各种数据类型的变量
●    在C#程序中控制执行流
●    注释和XML文档说明
●    预处理器指令
●    用法规则和命名约定,在编写C#代码时应遵循这些规则,使代码符合一般的.NET规范,这样其他人就很容易理解您所编写的代码了。
C#语法与C++/Java语法非常类似,但仍存在一些小区别。在许多领域,将这些语法与功能结合起来,会使编码更快速,例如高质量的字符串处理功能。C#还有一个强大的已定义类型系统,该系统基于值类型和引用类型的区别。下面两章将进一步介绍C#的面向对象编程特性