第二章 C#基础

2.1 引言
 
 下面采用传统的方式,看看一个最简单的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 .NET生成一个基本项目,再把自己的代码添加进去。但是,前面几章的目的是讲授C#语言,并使过程尽可能简单,在第12章之前避免涉及Visual Studio .NET。我们使代码显示为简单的文件,这样您就可以使用任何文本编辑器键入它们,并在命令行上对其进行编译。

2.2.2 编译并运行程序
通过对源文件运行C#命令行编译器(csc.exe)来编译这个程序:

csc First.cs

如果使用csc命令在命令行上编译代码,就应注意.NET命令行工具,包括csc,只有在设置了某些环境变量后才能使用。根据安装.NET(和Visual Studio .NET)的方式,这里显示的结果可能与您机器上的结果不同。

注意:

如果没有设置环境变量,有两种解决方法。第一种方法是在运行csc之前,在命令行上运行批处理文件%Microsoft Visual Studio .NET%/Vc7/bin/vcvars32.bat。其中%Microsoft Visual Studio .NET是安装Visual Studio .NET的文件夹。第二种方法(更简单)是使用Visual Studio .NET命令行代替通常的命令提示窗口。Visual Studio .NET命令提示在“开始”菜单—“程序”—Microsoft Visual Studio .NET 2003-Microsoft Visual Studio .NET Tools子菜单下。它只是一个命令提示窗口,打开时会自动运行vcvars32.bat。

编译代码,会生成一个可执行文件First.exe。在命令行或Windows Explorer上,象运行任何可执行文件那样运行该文件,得到如下结果:

csc First.cs

Microsoft (R) Visual C# .NET Compiler version 7.10.3052.4

for Microsoft (R) .NET Framework version 1.1.4322

Copyright (C) Microsoft Corporation 2001-2002. All rights reserved.

First

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风格语言的新用户,忽略该分号常常是导致编译错误的一个最主要的原因。

在上面的代码示例中,前几行代码是处理命名空间(如第1章所述),这是把相关类组合在一起的方式。Java和C++开发人员应很熟悉这个概念,但对于VB6开发人员来说是新概念。C#命名空间与C++命名空间或Java的包基本相同,但VB6中没有对应的概念。Namespace关键字声明了应与类相关的命名空间。其后花括号中的所有代码都被认为是在这个命名空间中。编译器在using指令指定的命名空间中查找没有在当前命名空间中定义、但在代码中引用的类。这非常类似于Java中的import语句和C++中的using命名空间语句。

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 .NET中,在这种情况下,控制台窗口会消失。

然后调用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 = 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不能在这个作用域内声明两次,因为这会给变量j赋予不同的含义,已用在~parent or current~作用域内的变量j表示其他内容)

其原因是:变量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被定义在类级(class level)上,在该类删除前是不会超出作用域的(在本例中,当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 类 型说   明范   围
sbyteSystem.SByte8位有符号的整数–128 到 127 (–27 到 27–1)
shortSystem.Int1616位有符号的整数–32 768 到 32 767 (–215 到 215–1)
intSystem.Int3232位有符号的整数–2 147 483 648 到 2 147 483 647 (–231 到 231–1)
longSystem.Int6464位有符号的整数–9 223 372 036 854 775 808 到 9 223 372 036 854 775 807 (–263到 263–1)
byteSystem.Byte8位无符号的整数0 到 255 (0 到 28–1)
ushortSystem.Uint1616位无符号的整数0 到 65535 (0 到 216–1) 
uintSystem.Uint3232位无符号的整数0 到 4 294 967 295 (0 到 232–1)
ulongSystem.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编码是一个子集。
 
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类型。
 
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语句不能直接测试整数(例如从函数中返回的值)。在C#中,必须明确地把返回的整数转换为布尔值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++中的对应循环相同。首先看看for   循环。
 
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循环非常常见,在每次迭代外部的循环时,内部循环都要彻底执行完毕。这种模式通常用于在矩形多维数组中遍历每个元素。最外部的循环遍历每一行,内部的循环遍历某行上的每个列。下面的代码可以用作NumberTable例子,显示数字行,它还使用另一个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 7.10.3052.4
 
for Microsoft (R) .NET Framework version 1.1.4322
 
Copyright (C) Microsoft Corporation 2001-2002. 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块(第11章将介绍如何用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 枚举
( 本章字数:9478 更新时间:2007-4-26 19:08:17 )
 
 
 枚举是用户定义的整数类型。在声明一个枚举时,要指定该枚举可以包含的一组可接受的实例值。不仅如此,还可以给值指定易于记忆的名称。如果在代码的某个地方,要试图把一个不在可接受值范围内的值赋予枚举的一个实例,编译器就会报告一个错误。这个概念对于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 数组
( 本章字数:3850 更新时间:2007-4-26 19:08:17 )
 
 
 本章不打算详细介绍数组,因为第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 命名空间
( 本章字数:14156 更新时间:2007-4-26 19:08:17 )
 
 
 如前所述,命名空间提供了一种组织相关类和其他类型的方式。与文件或组件不同,命名空间是一种逻辑组合,而不是物理组合。在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...
 
}
 
 }
 
}
 
}
 
每个命名空间名都由它所在命名空间的名称组成,这些名称用句点分隔开,首先是最外层的命名空间,最后是它自己的短名。所以ProfessionalCSharp命名空间的全名是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对象,这个对象是在该命名空间中定义的。它有一个方法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()方法
( 本章字数:10611 更新时间:2007-4-26 19:08:17 )
 
 
 本章的开头提到过,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 7.10.3052.4
 
for Microsoft (R) .NET Framework version 1.1.4322
 
Copyright (C) Microsoft Corporation 2001-2002. All rights reserved.
 
 
 
MainExample.cs(7,25): error CS0017: Program ~MainExample.exe~ has more than one entry point defined: ~Wrox.ProCSharp.Basics.Client.Main()~
 
MainExample.cs(21,25): 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[i]);
 
}
 
return 0;
 
 }
 
}
 
}
 
通常使用命令行就可以编译这段代码。在运行编译好的可执行文件时,可以在程序名的后面加上参数,例如:
 
ArgsExample /a /b /c
 
/a
 
/b
 
/c
 
 
2.10 有关编译C#文件的更多内容
( 本章字数:11747 更新时间:2007-4-26 19:08:17 )
 
 
 前面介绍了如何使用csc.exe编译控制台应用程序,但其他类型的应用程序应如何编译?如果要引用一个类库,该怎么办?MSDN文档介绍了C#编译器的所有编译选项,这里只介绍其中最重要的选项。
 
要回答第一个问题,应使用/target选项(常简写为/t)来指定要创建的文件类型。文件类型可以是表2-13所示的类型中的一种。
 
表 2-13
 
选   项
 输 出
 
/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
( 本章字数:14951 更新时间:2007-4-26 19:08:17 )
 
 
 现在,您应基本熟悉了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-14所示。
 
表 2-14
 
字 符 串
 说   明
 
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 使用注释
( 本章字数:26398 更新时间:2007-4-26 19:08:17 )
 
 
 本节的内容表面上看起来很简单——给代码添加注释。
 
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-15中所示的标识符。
 
表 2-15
 
标 识 符
 说   明
 
 
 把行中的文本标记为代码,例如int i = 10;
 
 
 把多行标记为代码
 
 
 标记为一个代码示例
 
 
 说明一个异常类(编译器要验证其语法)
 
 
 包含其他文档说明文件的注释(编译器要验证其语法)
 
 
(续表)
 
标 识 符
 说   明
 
 
 把列表插入到文档说明中
 
 
 标记方法的参数(编译器要验证其语法)
 
 
 表示一个单词是方法的参数(编译器要验证其语法)
 
 
 说明对成员的访问(编译器要验证其语法)
 
 
 给成员添加描述
 
 
 说明方法的返回值
 
 
 提供对另一个参数的交叉引用(编译器要验证其语法)
 
 
 提供描述中的“参见”部分(编译器要验证其语法)
 
 
 提供类型或成员的简短小结
 
 
 描述属性
 
 
 
 
要了解它们的工作方式,可以在上一节的MathLibrary.cs文件中添加一些XML注释,并称之为Math.cs。我们给类及其Add方法添加一个注释,也给Add方法添加一个元素和两个元素:
 
// Math.cs
 
namespace Wrox.ProCSharp.Basics
 
{
 
 
 
///
 
/// Wrox.ProCSharp.Basics.Math class.
 
/// Provides a method to add two integers.
 
///
 
public class Math
 
{
 
 ///
 
 /// The Add method allows us to add two integers
 
 ///
 
 ///Result of the addition (int)
 
 ///First number to add
 
 ///Second number to add
 
 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,如下所示。
 
 
 
 
 Math
 
 
 
 
 
 
 Wrox.ProCSharp.Basics.Math class.
 
 Provides a method to add two integers.
 
 
 
 
 
 
 "M:Wrox.ProCSharp.Basics.Math.Add(System.Int32,System.Int32)">
 
 
 The Add method allows us to add two integers
 
 
Result of the addition (int)
 
First number to add
 
Second number to add
 
 
 
 
 
注意,编译器为我们做了一些工作——它创建了一个元素,并为该文件中的每个类或类成员添加一个元素。每个元素都有一个name特性,其中包含成员的全名,前面有一个字母表示其类型:"T:"表示这是一个类型,"F:" 表示这是一个字段,"M:" 表示这是一个成员。
 
2.13 C#预处理器指令
( 本章字数:15609 更新时间:2007-4-26 19:08:17 )
 
 
 除了前面介绍的常用关键字外,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特性的机制,详见第10章。
 
#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编辑器。这些编辑器可以使用这些指令使代码在屏幕上更好地布局。第12章介绍Visual Studio .NET时会详细介绍它们。
 
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.14 C#编程规则
( 本章字数:149708 更新时间:2007-4-26 19:08:17 )
 
 
 本节介绍编写C#程序时应注意的规则。
 
2.14.1 用于标识符的规则
本节将讨论变量、类、方法等的命名规则。注意本节所介绍的规则不仅是规则,也是C#编译器强制使用的。
 
标识符是给变量、用户定义的类型(例如类和结构)和这些类型的成员指定的名称。标识符区分大小写,所以interestRate 和 InterestRate是不同的变量。确定在C#中可以使用什么标识符有两个规则,
 
● 它们必须以一个字母或下划线开头,但可以包含数字字符;
 
● 不能把C#关键字用作标识符。
 
C#包含如表2-16所示的保留关键字。
 
表 2-16
 
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
 
● uberflu?
 
● _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建议使用如下的命名空间:.,例如:
 
WeaponsOfDestructionCorp.RayGunControllers
 
WeaponsOfDestructionCorp.Viruses
 
(4) 名称和关键字
 
名称不应与任何关键字冲突,这是非常重要的。实际上,如果在代码中,试图给某个对象指定与C#关键字同名的名称,就会出现语法错误,因为编译器会假定该名称表示一个语句。但是,由于类可能由其他语言编写的代码访问,所以不能使用其他.NET语言中的关键字作为对象的名称。一般说来,C++关键字类似于C#关键字,不太可能与C++混淆,Visual C++常用的关键字则用两个下划线字符开头。与C#一样,C++关键字都是小写字母,如果要遵循公共类和成员使用Pascal风格的名称的约定,则在它们的名称中至少有一个字母是大写,因此不会与C++关键字冲突。另一方面,VB.NET的问题会多一些,因为VB.NET的关键字要比C#的多,而且它不区分大小写,不能依赖于Pascal风格的名称来区分类和成员。
 
表2-17列出了VB.NET中的关键字和标准函数调用,无论对于C#公共类使用什么大小写组合,这些名称都不应使用。
 
表 2-17
 
Abs
 Do
 Loc
 RGB
 
Add
 Double
 Local
 Right
 
AddHandler
 Each
 Lock
 RmDir
 
AddressOf
 Else
 LOF
 Rnd
 
 
 
(续表)
 
Alias
 ElseIf
 Log
 RTrim
 
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
 
 
 
(续表)
 
Close
 Hex
 Overrides
 TimeOfDay
 
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代码时应很好地遵循这些规则,对于基类来说,就是类、成员、命名空间的命名方式和类层次结构的工作方式等,如果编写代码的风格与基类的编写风格相同,就不会犯什么错误。 
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值