2024年3月28日发(作者:)

图 1-4

本章还讨论了IL的特性,特别是其强数据类型和面向对象的特性。探讨了这些

特性如何影响面向.NET的语言,包括C#,并阐述了IL的强类型本质如何支持语

言的互操作性,以及CLR服务,例如垃圾收集和安全性。

本章的最后讨论了C#如何用作基于几个.NET技术(包括)的应用程序的

基础。

第2章将介绍如何用C#语言编写代码。

第2章 C#基础

理解了C#的用途后,就可以学习如何使用它。本章将介绍C#编程的基础知识,

并假定您具备C#编程的基本知识,这是后续章节的基础。本章的主要内容如下:

声明变量

变量的初始化和作用域

C#的预定义数据类型

在C#程序中使用循环和条件语句指定执行流

枚举

命名空间

Main()方法

基本的命令行C#编译器选项

使用e执行控制台I/O

在C#和Visual Studio .NET中使用文档编制功能

C#标识符和关键字

C#编程的推荐规则和约定

阅读完本章后,您就有足够的C#知识编写简单的程序了,但还不能使用继承或

其他面向对象的特征。这些内容将在本书后面的几章中讨论。

2.1 引言

如前所述,C#是一种面向对象的语言。在快速浏览C#语句的基础时,我们假定

您已经很好地掌握了面向对象(OO)编程的概念。换言之,我们希望您懂得类、对

象、接口和继承的含义。如果以前使用过C++或Java,就应有很好的面向对象编

程(OOP)的基础。但是,如果您不具备OOP的背景知识,这个主题有许多很好的

信息资源。本书的附录A就详细介绍了OOP。附录A可以从上下

载。

如果您对VB6、C++或 Java中的一种语言有丰富的编程经验,就应注意在介绍C

#基础知识时,我们对C#、C++、Java和VB6进行了许多比较。但是,您也许愿

意阅读一本有关C#和自己所选语言的比较的图书,来学习C#。如果是这样,可

以从Wrox Press网站()上下载不同的文档来学习C#。

2.2 第一个C#程序

下面采用传统的方式,看看一个最简单的C#程序——这是一个把信息写到屏幕上

的控制台应用程序。

2.2.1 代码

在文本编辑器(例如Notepad)中键入下面的代码,把它保存为.cs文件(例如Fir

):

using System;

namespace

{

class MyFirstCSharpClass

{

static void Main()

{

ine("This isn't at all like Java!");

ne();

return;

}

}

}

注意:

在后面的几章中,介绍了许多代码示例。编写C#程序最常用的技巧是使用Visual Stud

io 2005生成一个基本项目,再把自己的代码添加进去。但是,前面几章的目的是讲授C#

语言,并使过程尽可能简单,在第14章之前避免涉及Visual Studio 2005。我们使代码显示

为简单的文件,这样您就可以使用任何文本编辑器键入它们,并在命令行上对其进行编译。

2.2.2 编译并运行程序

对源文件运行C#命令行编译器(),编译这个程序:

csc

如果使用csc命令在命令行上编译代码,就应注意.NET命令行工具,包括csc,

只有在设置了某些环境变量后才能使用。根据安装.NET(和Visual Studio 2005)

的方式,这里显示的结果可能与您机器上的结果不同。

注意:

如果没有设置环境变量,有两种解决方法。第一种方法是在运行csc之前,在命令行上

运行批处理文件%Microsoft Visual Studio 2005%。其中%Micr

osoft Visual Studio 2005是安装Visual Studio 2005的文件夹。第二种方法(更简单)是使用

Visual Studio 2005命令行代替通常的命令提示窗口。Visual Studio 2005命令提示在“开始”

菜单—“程序”—Microsoft Visual Studio 2005-Microsoft Visual Studio Tools子菜单下。

它只是一个命令提示窗口,打开时会自动运行。

编译代码,会生成一个可执行文件。在命令行或Windows Explorer

上,象运行任何可执行文件那样运行该文件,得到如下结果:

csc

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.

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的包基本相同,但V

B6中没有对应的概念。namespace关键字声明了应与类相关的命名空间。其后花

括号中的所有代码都被认为是在这个命名空间中。编译器在using指令指定的命

名空间中查找没有在当前命名空间中定义、但在代码中引用的类。这非常类似于

Java中的import语句和C++中的using namespace语句。

using System;

namespace

{

在文件中使用using指令的原因是下面要使用一个库类

le。using System指令允许把这个类简写为Console(类似于System命名空间中

的其他类)。标准的System命名空间包含了最常用的.NET类型。我们用C#做的

所有工作都依赖于.NET基类,认识到这一点是非常重要的;在本例中,我们使

用了System命名空间中的Console类,以写入控制台窗口。

注意:

几乎所有的C#程序都使用System命名空间中的类,所以假定本章所有的代码文件都包

含using System;语句。

C#没有用于输入和输出的内置关键字,而是完全依赖于.NET类。

接着,声明一个类,它表面上称为MyFirstClass。但是,因为该类位于Wrox.P

命名空间中,所以其完整的名称是.M

yFirstCSharpClass:

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中,Stat

ic关键字有不同的含义)。把返回类型设置为void,在本例中,不包含任何参数。

最后,看看代码语句。

ine("This isn't at all like Java!");

ne();

return;

在本例中,我们只调用了e类的WriteLine()方法,把一行文本

写到控制台窗口上。WriteLine()是一个静态方法,在调用之前不需要实例化Co

nsole对象。

ne()读取用户的输入,添加这行代码会让应用程序等待用户按下

回车键,之后退出应用程序。在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;

ine(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

{

public class ScopeTest

{

public static int Main()

{

for (int i = 0; i < 10; i++)

{

ine(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--)

{

ine(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

ine(j + i);

}

return 0;

}

如果试图编译它,就会产生如下错误:

(12,14): error CS0136: A local variable named 'j' cannot be declare

d in this scope because it would give a different meaning to 'j', which is already us

ed in a 'parent or current' scope to denote something else

其原因是:变量j是在for循环开始前定义的,在执行for循环时应处于其作用

域内,在Main方法结束执行后,变量j才超出作用域,第二个j(不合法)则在

循环的作用域内,该作用域嵌套在Main方法的作用域内。编译器无法区别这两

个变量,所以不允许声明第二个变量。这也是与C++不同的地方,在C++中,允

许隐藏变量。

2. 字段和局部变量的作用域冲突

在某些情况下,可以区分名称相同(尽管其完全限定的名称不同)、作用域相同的

两个标识符。此时编译器允许声明第二个变量。原因是C#在变量之间有一个基

本的区分,它把声明为类型级的变量看作是字段,而把在方法中声明的变量看作

局部变量。

考虑下面的代码:

using System;

namespace

{

class ScopeTest2

{

static int j = 20;

public static void Main()

{

int j = 30;

ine(j);

return;

}

}

}

即使在Main方法的作用域内声明了两个变量j,这段代码也会编译—— j被定义

在类级上,在该类删除前是不会超出作用域的(在本例中,当Main方法中断,程

序结束时,才会删除该类)。此时,在Main方法中声明的新变量j隐藏了同名的

类级变量,所以在运行这段代码时,会显示数字30。

但是,如果要引用类级变量,该怎么办?可以使用语法ame,在

对象的外部引用类的字段或结构。在上面的例子中,我们访问静态方法中的一个

静态字段(静态字段详见下一节),所以不能使用类的实例,只能使用类本身的名

称:

public static void Main()

{

int j = 30;

ine(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章)。

常量总是静态的。但注意,不必(实际上,是不允许)在常量声明中包含修饰符st

atic。

在程序中使用常量至少有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 ();

= 30; // Value is a field defined in Vector class

y = x;

ine();

= 50;

ine();

要理解的重要一点是在执行这段代码后,只有一个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#认可的基本预定义类型并没有内置于语言中,而是内置于.NE

T Framework中。例如,在C#中声明一个int类型的数据时,声明的实际上是.

NET结构32的一个实例。这听起来似乎很深奥,但其意义深远:这

表示在语法上,可以把所有的基本数据类型看作是支持某些方法的类。例如,要

把int i转换为string,可以编写下面的代码:

string s = ng();

应强调的是,在这种便利语法的背后,类型实际上仍存储为基本类型。基本类型

在概念上用.NET结构表示,所以肯定没有性能损失。

下面看看C#中定义的类型。我们将列出每个类型,以及它们的定义和对应.NET

类型(CTS 类型)的名称。C#有15个预定义类型,其中13个是值类型,2个是引

用类型(string和object)。

2.4.3 预定义的值类型

内置的值类型表示基本数据类型,例如整型和浮点类型、字符类型和bool类型。

1. 整型

C#支持8个预定义整数类型,如表2-1所示。

表 2-1

名 称 CTS 类 型 说 明 范 围

sbyte

short

int

long

byte

ushort

uint

ulong

16

32

64

16

32

64

8位有符号的整数

16位有符号的整数

32位有符号的整数

64位有符号的整数

8位无符号的整数

16位无符号的整数

32位无符号的整数

64位无符号的整数

–128 到 127 (–2

7

到2

7

–1)

–32 768 到 32 767 (–2

15

到2

15

–1)

–2 147 483 648 到 2 147 483 647(–2

31

到2

31

–1)

–9 223 372 036 854 775 808到9 223 372 036 854

775 807(–2

63

到2

63

–1)

0到255(0到2

8

–1)

0到65535(0到2

16

–1)

0到4 294 967 295(0到2

32

–1)

0到18 446 744 073 709 551 615(0到2

64

–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

名称

float

double

CTS类型

说 明

32位单精度浮点数

64位双精度浮点数

位 数

7

15/16

范围 (大致)

±1.5 × 10

-45

到 ±3.4 × 10

38

±5.0 × 10

-324

到 ±1.7 × 10

308

float数据类型用于较小的浮点数,因为它要求的精度较低。double数据类型比

float数据类型大,提供的精度也大一倍(15位)。

如果在代码中没有对某个非整数值(如12.3)硬编码,则编译器一般假定该变量

是double。如果想指定值为float,可以在其后加上字符F(或f):

float f = 12.3F;

3. decimal类型

另外,decimal类型表示精度更高的浮点数,如表2-3所示。

表 2-3

名 称

decimal

CTS类型

System.

Decimal

说 明

128位高精度十

进制数表示法

位 数

28

范围(大致)

±1.0×10

-28

到±7.9 ×

10

28

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

名 称

bool

CTS 类 型

n

true或false

bool值和整数值不能相互转换。如果变量(或函数的返回类型)声明为bool类型,

就只能使用值true或false。如果试图使用0表示false,非0值表示true,

就会出错。

5. 字符类型

为了保存单个字符的值,C#支持char数据类型,如表2-5所示。

表 2-5

名 称

char

CTS 类 型

表示一个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

名 称

object

string

CTS 类

说 明

根类型,CTS中的其他类型都是从它派生而来的(包括值类型)

Unicode字符串

1. object类型

许多编程语言和类结构都提供了根类型,层次结构中的其他对象都从它派生而

来。C#和.NET也不例外。在C#中,object类型就是最终的父类型,所有内在和

用户定义的类型都从它派生而来。这是C#的一个重要特性,它把C#与VB和C+

+区分开来,但其行为与Java非常类似。所有的类型都隐含地最终派生于Syst

类,这样,object类型就可以用于两个目的:

可以使用object引用绑定任何子类型的对象。例如,第5章将说明如何使用obj

ect类型把堆栈中的一个值对象装箱,再移动到堆中。对象引用也可以用于反射,

此时必须有代码来处理类型未知的对象。这类似于C++中的void指针或VB中的

Variant数据类型。

object类型执行许多基本的一般用途的方法,包括Equals()、GetHashCode()、Ge

tType()和ToString()。用户定义的类需要使用一种面向对象技术—— 重写(见第4

章),提供其中一些方法的替代执行代码。例如,重写ToString()时,要给类提供

一个方法,提供类本身的字符串表示。如果类中没有提供这些方法的实现代码,

编译器就会使用object类型中的实现代码,它们在类中的执行不一定正确。

后面的章节将详细讨论object类型。

2. string类型

有C和C++开发经验的人员可能在使用C风格的字符串时不太顺利。C或C++字

符串不过是一个字符数组,因此客户机程序员必须做许多工作,才能把一个字符

串复制到另一个字符串上,或者连接两个字符串。实际上,对于一般的C++程序

员来说,执行包装了这些操作细节的字符串类是一个非常头痛的耗时过程。VB

程序员的工作就比较简单,只需使用string类型即可。而Java程序员就更幸运

了,其String类在许多方面都类似于C#字符串。

C#有string关键字,在翻译为.NET类时,它就是。有了它,像

字符串连接和字符串复制这样的操作就很简单了:

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;

ine("s1 is " + s1);

ine("s2 is " + s2);

s1 = "another string";

ine("s1 is now " + s1);

ine("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:";

即使用户相信自己可以在任何情况下都记住要这么做,但键入两个反斜杠字符会

令人迷惑。幸好,C#提供了另一种替代方式。可以在字符串字面量的前面加上字

符@,在这个字符后的所有字符都看作是其原来的含义——它们不会解释为转义

字符:

string filepath = @"C:";

甚至允许在字符串字面量中包含换行符:

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;

ine("i is Zero");

}

else

{

isZero = false;

ine("i is Non-zero");

}

其语法与C++和Java类似,但与VB不同。VB开发人员注意,C#中没有与VB的

EndIf对应的语句,其规则是if的每个子句都只包含一个语句。如果需要多个

语句,如上面的例子所示,就应把这些语句放在花括号中,这会把整组语句当作

一个语句块来处理。

还可以单独使用if语句,不加else语句。也可以合并else if子句,测试多个

条件。

using System;

namespace

{

class MainEntryPoint

{

static void Main(string[] args)

{

ine("Type in a string");

string input;

input = ne();

if (input == "")

{

ine("You typed in an empty string");

}

else if ( < 5)

{

ine("The string had less than 5 characters");

}

else if ( < 10)

{

ine("The string had at least 5 but less than 10

characters");

}

ine("The string was " + input);

}

}

}

添加到if子句中的else if语句的个数没有限制。

注意在上面的例子中,我们声明了一个字符串变量input,让用户在命令行上输

入文本,把文本填充到input中,然后测试该字符串变量的长度。代码还说明了

在C#中如何进行字符串处理。例如,要确定input的长度,可以使用

ngth。

对于if,要注意的一点是如果条件分支中只有一条语句,就无需使用花括号:

if (i == 0)

ine("i is Zero"); // This will only execute if i == 0

ine("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子句,如果表达式不等于任何ca

se子句的值,就执行default子句的代码。下面的switch语句测试integerA

变量的值:

switch (integerA)

{

case 1:

ine("integerA =1");

break;

case 2:

ine("integerA =2");

break;

case 3:

ine("integerA =3");

break;

default:

ine("integerA is not 1,2, or 3");

break;

}

注意case的值必须是常量表达式——不允许使用变量。

C和C++程序员应很熟悉switch…case语句,而C#的switch…case语句更安全。

特别是它禁止所有case中的失败条件。如果激活了块中靠前的一个case子句,

后面的case子句就不会被激活,除非使用goto语句特别标记要激活后面的cas

e子句。编译器会把没有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循环非常适合于一个语句或语句块重复执行预定的次数。下面的例子就是f

or循环的典型用法,这段代码输出从0~99的整数:

for (int i = 0; i < 100; i = i+1) // this is equivalent to

// For i = 0 To 99 in VB.

{

ine(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方法(),该方法的作用与ine()相同,

但不在输出中添加回车换行符:

using System;

namespace

{

class MainEntryPoint

{

static void Main(string[ ] args)

{

// This loop iterates

for (int i = 0; i < 100; i+=10)

{

// This loop iterates

for (int j = i; j < i + 10; j++)

{

(" " + j);

}

ine();

}

}

}

}

尽管j是一个整数,但它会自动转换为字符串,以便进行连接。C++开发人员要

注意,这比在C++中处理字符串容易得多,VB开发人员则已经习惯于此了。

C程序员应注意上述例子中的一个特殊功能。在每次迭代后续的外部循环时,最

内部循环的计数器变量都要重新声明。这种语法不仅在C#中可行,在C++中也是

合法的。

上述例子的结果是:

csc

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中的Wend循环相

同。与for循环一样,while也是一个预测试的循环。其语法是类似的,但whi

le循环只有一个表达式:

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中的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#数组、tion

命名空间中的集合类,以及用户定义的集合类。从下面的代码中可以了解forea

ch循环的语法,其中假定arrayOfInts是一个整型数组:

foreach (int temp in arrayOfInts)

{

ine(temp);

}

其中,foreach循环每次迭代数组中的一个元素。它把每个元素的值放在int型

的变量temp中,然后执行一次循环迭代。

注意,不能改变集合中各项(上面的temp)的值,所以下面的代码不会编译:

foreach (int temp in arrayOfInts)

{

temp++;

ine(temp);

}

如果需要迭代集合中的各项,并改变它们的值,就应使用for循环。

2.5.3 跳转语句

C#提供了许多可以立即跳转到程序中另一行代码的语句,在此,先介绍goto语

句。

1. goto语句

goto语句可以直接跳转到程序中用标签指定的另一行(标签是一个标识符,后跟

一个冒号):

goto Label1;

ine("This won't be executed");

Label1:

ine("Continuing execution from here");

goto语句有两个限制。不能跳转到像for循环这样的代码块中,也不能跳出类

的范围,不能退出atch块后面的finally块(第12章将介绍如何用

finally块处理异常)。

goto语句的名声不太好,在大多数情况下不允许使用它。一般情况下,使用它

肯定不是面向对象编程的好方式。但是有一个地方使用它是相当方便的——在s

witch语句的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会通过Int

elliSense弹出一个包含可接受值的列表框,减少了按键次数,并能够让我们回忆

起可选的值。

定义如下的枚举:

public enum TimeOfDay

{

Morning = 0,

Afternoon = 1,

Evening = 2

}

在本例中,在枚举中使用一个整数值,来表示一天的每个阶段。现在可以把这些

值作为枚举的成员来访问。例如,g返回数字0。使用这个枚

举一般是把合适的值传送给方法,在switch语句中迭代可能的值。

class EnumExample

{

public static int Main()

{

WriteGreeting(g);

return 0;

}

static void WriteGreeting(TimeOfDay timeOfDay)

{

switch(timeOfDay)

{

case g:

ine("Good morning!");

break;

case oon:

ine("Good afternoon!");

break;

case g:

ine("Good evening!");

break;

default:

ine("Hello!");

break;

}

}

}

在C#中,枚举的真正强大之处是它们在后台会实例化为派生于基类

m的结构。这表示可以对它们调用方法,执行有用的任务。注意因为.NET Frame

work的执行方式,在语法上把枚举当做结构是不会有性能损失的。实际上,一

旦代码编译好,枚举就成为基本类型,与int和float类似。

可以获取枚举的字符串表示,例如使用前面的TimeOfDay枚举:

TimeOfDay time = oon;

ine(ng());

会返回字符串Afternoon。

另外,还可以从字符串中获取枚举值:

TimeOfDay time2 = (TimeOfDay) (typeof(TimeOfDay), "afternoon", t

rue);

ine((int)time2);

这段代码说明了如何从字符串获取枚举值,并转换为整数。要从字符串中转换,

需要使用静态的()方法,这个方法带3个参数,第一个参数是要使

用的枚举类型。其句法是关键字typeof后跟放在括号中的枚举类名。typeof运

算符将在第5章详细论述。第二个参数是要转换的字符串,第三个参数是一个b

ool,指定在进行转换时是否忽略大小写。最后,注意()方法实际上

返回一个对象引用—— 我们需要把这个字符串显式转换为需要的枚举类型(这是

一个拆箱操作的例子)。对于上面的代码,将返回1,作为一个对象,对应于Ti

meOfDay. Afternoon的枚举值。在显式转换为int时,会再次生成1。

上的其他方法可以返回枚举定义中的值的个数、列出值的名称等。

详细信息参见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 is any reference to an array

2.8 命名空间

如前所述,命名空间提供了一种组织相关类和其他类型的方式。与文件或组件不

同,命名空间是一种逻辑组合,而不是物理组合。在C#文件中定义类时,可以

把它包括在命名空间定义中。以后,在定义另一个类,在另一个文件中执行相关

操作时,就可以在同一个命名空间中包含它,创建一个逻辑组合,告诉使用类

的其他开发人员:这两个类是如何相关的以及如何使用它们:

namespace CustomerPhoneBookApp

{

using System;

public struct Subscriber

{

// Code for

}

}

把一个类型放在命名空间中,可以有效地给这个类型指定一个较长的名称,该名

称包括类型的命名空间,后面是句点(.)和类的名称。在上面的例子中,Subscr

iber结构的全名是iber。这样,有相同短名的

不同的类就可以在同一个程序中使用了。

也可以在命名空间中嵌套其他命名空间,为类型创建层次结构:

namespace Wrox

{

namespace ProCSharp

{

namespace Basics

{

class NamespaceExample

{

// Code for the

}

}

}

}

每个命名空间名都由它所在命名空间的名称组成,这些名称用句点分隔开,首先

是最外层的命名空间,最后是它自己的短名。所以ProCSharp命名空间的全名是

arp,NamespaceExample类的全名是

spaceExample。

使用这个语法也可以组织自己的命名空间定义中的命名空间,所以上面的代码也

可以写为:

namespace

{

class NamespaceExample

{

// Code for the

}

}

注意不允许在另一个嵌套的命名空间中声明多部分的命名空间。

命名空间与程序集无关。同一个程序集中可以有不同的命名空间,也可以在不同

的程序集中定义同一个命名空间中的类型。

2.8.1 using语句

显然,命名空间相当长,键入起来很繁琐,用这种方式指定某个类也是不必要的。

如本章开头所述,C#允许简写类的全名。为此,要在文件的顶部列出类的命名空

间,前面加上using关键字。在文件的其他地方,就可以使用其类型名称来引用

命名空间中的类型了:

using System;

using arp;

如前所述,所有的C#源代码都以语句using System;开头,这仅是因为Microso

ft提供的许多有用的类都包含在System命名空间中。

如果using指令引用的两个命名空间包含同名的类,就必须使用完整的名称(或

者至少较长的名称),确保编译器知道访问哪个类型,例如,类NamespaceExamp

le同时存在于Wrox. 和命名空间中,

如果要在命名空间arp中创建一个类Test,并在该类中实例化一个

NamespaceExample类,就需要指定使用哪个类:

using arp;

class Test

{

public static int Main()

{

aceExample nSEx = new aceExample();

//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;

下面的例子(前面例子的修订版本)给命名空间指定Int

roduction别名,并使用这个别名实例化了一个NamespaceExample对象,这个

对象是在该命名空间中定义的。注意命名空间别名的修饰符是::。因此将先从I

ntroduction命名空间别名开始搜索。如果在相同的作用域中引入了一个Intro

duction类,就会发生冲突。即使出现了冲突,::操作符也允许引用别名。Name

spaceExample类有一个方法GetNamespace(),该方法调用每个类都有的GetTyp

e()方法,以访问表示类的类型的Type对象。下面使用这个对象来返回类的命名

空间名:

using System;

using Introduction = ;

class Test

{

public static int Main()

{

Introduction::NamespaceExample NSEx =

new Introduction::NamespaceExample();

ine(espace());

return 0;

}

}

namespace

{

class NamespaceExample

{

public string GetNamespace()

{

return e().Namespace;

}

}

}

2.9 Main()方法

本章的开头提到过,C#程序是从方法Main()开始执行的。这个方法必须是类或

结构的静态方法,并且其返回类型必须是int或void。

虽然显式指定public修饰符是很常见的,因为按照定义,必须在程序外部调用

该方法,但我们给该方法指定什么访问级别并不重要,即使把该方法标记为pri

vate,它也可以运行。

2.9.1 多个Main()方法

在编译C#控制台或Windows应用程序时,默认情况下,编译器会在与上述签名匹

配的类中查找Main方法,并使这个类方法成为程序的入口。如果有多个Main

方法,编译器就会返回一个错误,例如,考虑下面的代码:

using System;

namespace

{

class Client

{

public static int Main()

{

();

return 0;

}

}

class MathExample

{

static int Add(int x, int y)

{

return x + y;

}

public static int Main()

{

int i = Add(5,10);

ine(i);

return 0;

}

}

}

上述代码中包含两个类,它们都有一个Main()方法。如果按照通常的方式编译

这段代码,就会得到下述错误:

csc

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.

(7,23): error CS0017: Program '' has more than

one entry point defined: '()'

(21,23): error CS0017: Program '' has more tha

n one entry point defined: '()'

但是,可以使用/main选项,其后跟Main()方法所属类的全名(包括命名空间),

明确告诉编译器把哪个方法作为程序的入口点:

csc /main:ample

2.9.2 给Main()方法传送参数

前面的例子只介绍了不带参数的Main()方法。但在调用程序时,可以让CLR包

含一个参数,将命令行参数转送给程序。这个参数是一个字符串数组,传统称为

args(但C#可以接受任何名称)。在启动程序时,可以使用这个数组,访问通过

命令行传送过来的选项。

下面的例子是在传送给Main方法的字符串数组中迭代,并把

每个选项的值写入控制台窗口:

using System;

namespace

{

class ArgsExample

{

public static int Main(string[] args)

{

for (int i = 0; i < ; i++)

{

ine(args[i]);

}

return 0;

}

}

}

通常使用命令行就可以编译这段代码。在运行编译好的可执行文件时,可以在程

序名的后面加上参数,例如:

ArgsExample /a /b /c

/a

/b

/c

2.10 有关编译C#文件的更多内容

前面介绍了如何使用编译控制台应用程序,但其他类型的应用程序应如

何编译?如果要引用一个类库,该怎么办?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选

项,后跟程序集的路径和文件名。下面的例子说明了如何编译类库,并在另一个

程序集中引用这个库。它包含两个文件:

类库

控制台应用程序,该应用程序调用库中的一个类。

第一个文件包含DLL的代码,为了简单起见,它只包含一个公

共类Math和一个方法,该方法把两个int类型的数据加在一起:

namespace

{

public class MathLib

{

public int Add(int x, int y)

{

return x + y;

}

}

}

使用下述命令把这个C#文件编译为. NET DLL:

csc /t:library

控制台应用程序将简单地实例化这个对象,调用其Add方法,在

控制台窗口中显示结果:

using System;

namespace

{

class Client

{

public static void Main()

{

MathLib mathObj = new MathLib();

ine((7,8));

}

}

}

使用/r选项编译这个文件,使之指向新编译的DLL:

csc /r:

当然,下面就可以像往常一样运行它了:在命令提示符上输入MathClient,其

结果是显示数字15—— 加运算的结果。

2.11 控制台I/O

现在,您应基本熟悉了C#的数据类型以及如何在操作这些数据类型的程序中完

成任务。本章还要使用Console类的几个静态方法来读写数据,这些方法在编写

基本的C#程序时非常有效,下面就详细介绍它们。

要从控制台窗口中读取一行文本,可以使用ne()方法,它会从

控制台窗口中取一个输入流(在用户按下回车键时停止),并返回输入的字符串。

写入控制台也有两个对应的方法,前面已经使用过它们:

Console. Write()方法将指定的值写入控制台窗口。

ine()方法类似,但在输出结果的最后添加一个换行符。

所有预定义类型(包括object) 都有这些函数的各种形式(重载),所以在大多数

情况下,在显示值之前不必把它们转换为字符串。

例如,下面的代码允许用户输入一行文本,并显示该文本:

string s = ne();

ine(s);

ine()还允许用与C的printf()函数类似的方式显示格式化的结

果。要以这种方式使用WriteLine(),应传入许多参数。第一个参数是花括号中

包含标记的字符串,在这个花括号中,要把后续的参数插入到文本中。每个标记

都包含一个基于0的索引,表示列表中参数的序号。例如,"{0}"表示列表中的

第一个参数,所以下面的代码:

int i = 10;

int j = 20;

ine("{0} plus {1} equals {2}", i, j, i + j);

会显示:

10 plus 20 equals 30

也可以为值指定宽度,调整文本在该宽度中的位置,正值表示右对齐,负值表示

左对齐。为此可以使用格式{n,w},其中n是参数索引,w是宽度值。

int i = 940;

int j = 73;

ine(" {0,4}n+{1,4}n––––n {2,4}", i, j, i + j);

结果如下:

940

+ 73

1013

最后,还可以添加一个格式字符串,和一个可选的精度值。这里没有列出格式字

符串的完整列表,因为如第8章所述,我们可以定义自己的格式字符串。但用于

预定义类型的主要格式字符串如表2-9所示。

表 2-9

字 符 串

C

D

E

说 明

本地货币格式

十进制格式,把整数转换为以10为基数的数,如果给定一个精度说明符,就加上前导0

科学计数法(指数)格式。精度说明符设置小数位数(默认为6)。格式字符串的大小写("e" 或

"E")确定指数符号的大小写

F

G

N

P

X

固定点格式,精度说明符设置小数位数,可以为0

普通格式,使用E 或 F格式取决于哪种格式较简单

数字格式,用逗号表示千分符,例如32,767.44

百分数格式

16进制格式,精度说明符用于加上前导0

注意格式字符串都不需要考虑大小写,除e/E之外。

如果要使用格式字符串,应把它放在给出参数个数和字段宽度的标记后面,并用

一个冒号把它们分隔开。例如,要把decimal值格式化为货币格式,且使用计算

机上的地区设置,其精度为两位小数,则使用C2:

decimal i = 940.23m;

decimal j = 73.7m;

ine(" {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;

ine("{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 */

单行注释中的任何内容,即//后面的内容都会被编译器忽略。多行注释中/* 和

*/之间的所有内容也会被忽略。显然不能在多行注释中包含*/组合,因为这会被

当作注释的结尾。

实际上,可以把多行注释放在一行代码中:

ine(/*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

标 识 符

说 明

把行中的文本标记为代码,例如int i = 10;

把多行标记为代码

标记为一个代码示例

说明一个异常类(编译器要验证其语法)

包含其他文档说明文件的注释(编译器要验证其语法)

把列表插入到文档说明中

标记方法的参数(编译器要验证其语法)

表示一个单词是方法的参数(编译器要验证其语法)

说明对成员的访问(编译器要验证其语法)

给成员添加描述

说明方法的返回值

提供对另一个参数的交叉引用(编译器要验证其语法)

提供描述中的“参见”部分(编译器要验证其语法)

提供类型或成员的简短小结

描述属性

要了解它们的工作方式,可以在上一节的文件中添加一些XML

注释,并称之为。我们给类及其Add方法添加一个

注释,也

给Add方法添加一个元素和两个元素:

//

namespace

{

///

/// 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:

如果XML注释没有生成格式正确的XML文档,编译器就生成一个错误。

上面的代码会生成一个XML文件,如下所示。

Math

class.

Provides a method to add two integers.

"M:(32,32)">

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#预处理器指令

除了前面介绍的常用关键字外,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

ine("x is " + x);

#endif

}

这段代码会像往常那样编译,但ine命令包含在#if子句内。这

行代码只有在前面的#define命令定义了符号DEBUG后才执行。当编译器遇到#i

f语句后,将先检查相关的符号是否存在,如果符号存在,就只编译#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指令,会给用户显示#wa

rning指令后面的文本,之后编译继续进行。如果编译器遇到#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! "

ine("*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 "" // we happen to know this is line 164 in the file

// , 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

as

base

bool

break

byte

case

catch

char

checked

class

const

continue

do

double

else

enum

event

explicit

extern

false

finally

fixed

float

for

foreach

In

Int

Interface

Internal

Is

lock

long

namespace

new

null

object

operator

out

protected

public

readonly

ref

return

sbyte

sealed

short

sizeof

stackalloc

static

string

struct

true

try

typeof

uint

ulong

unchecked

unsafe

ushort

using

virtual

volatile

void

while

decimal

default

delegate

goto

if

Implicit

override

params

private

switch

this

throw

如果需要把某一保留字用作标识符(例如,访问一个用另一种语言编写的类),可

以在标识符的前面加上前缀@符号,指示编译器其后的内容是一个标识符,而不

是C#关键字(所以abstract不是有效的标识符,而@abstract是)。

最后,标识符也可以包含Unicode字符,用语法uXXXX来指定,其中XXXX是U

nicode字符的四位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表示法是多余的。

但是,在许多语言中,用法约定是从语言的使用过程中逐渐演变而来的,Micro

soft编写的C#和整个.NET Framework都有非常多的用法约定,详见.NET/C# MS

DN文档说明。这说明,从一开始,.NET程序就有非常高的互操作性,开发人员

可以以此来理解代码。用法规则还得益于20年来面向对象编程的发展,因此相

关的新闻组已经仔细考虑了这些用法规则,而且已经为开发团体所接受。所以我

们应遵守这些约定。

但要注意,这些规则与语言规范是不同的。用户应尽可能遵循这些规则。但如果

有很好的理由不遵循它们,也不会有什么问题。例如,不遵循这些用法约定,也

不会出现编译错误。一般情况下,如果不遵循用法规则,就必须有一个说得过去

的理由。规则应是一个正确的决策,而不是让人头痛的东西。在阅读本书的后续

内容时,应注意到在本书的许多示例中,都没有遵循该约定,这通常是因为某些

规则适用于大型程序,而不适合于本书中的小示例。如果编写一个完整的软件包,

就应遵循这些规则,但它们并不适合于只有20行代码的独立程序。在许多情况

下,遵循约定会使这些示例难以理解。

编程风格的规则非常多。这里只介绍一些比较重要的规则,以及最适合于用户的

规则。如果用户要让代码完全遵循用法规则,就需要参考MSDN文档说明。

1. 命名约定

使程序易于理解的一个重要方面是给对象选择命名的方式,包括变量名、方法名、

类名、枚举名和命名空间的名称。

显然,这些名称应反映对象的功能,且不与其他名称冲突。在.NET Framework

中,一般规则也是变量名要反映变量实例的功能,而不是反映数据类型。例如,

Height就是一个比较好的变量名,而IntegerValue就不太好。但是,这种规则

是一种理想状态,很难达到。在处理控件时,大多数情况下使用ConfirmationDia

log 和 ChooseEmployeeListBox等变量名比较好,这些变量名说明了变量的数据

类型。

名称的约定包括以下几个方面:

(1) 名称的大小写

在许多情况下,名称都应使用Pascal大小写命名形式。 Pascal 大小写形式是

指名称中单词的第一个字母大写: EmployeeSalary, ConfirmationDialog, Pl

ainTextEncoding。注意,命名空间、类、以及基类中的成员等的名称都应遵循

该规则,最好不要使用带有下划线字符的单词,即名称不应是employee_salary。

其他语言中常量的名称常常全部都是大写,但在C#中最好不要这样,因为这种

名称很难阅读,而应全部使用Pascal 大小写形式的命名约定:

const int MaximumLength;

我们还推荐使用另一种大小写模式:camel大小写形式。这种形式类似于Pasca

l 大小写形式,但名称中第一个单词的第一个字母不是大写: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是不区分大小写的,如果使用仅大小写不同的名称,就必须使这

两个名称不能在程序集的外部访问。(上例是可行的,因为仅私有变量使用了ca

mel大小写形式的名称)。否则,VB .NET中的其他代码就不能正确使用这个程序

集。

(2) 名称的风格

名称的风格应保持一致。例如,如果类中的一个方法叫ShowConfirmationDialo

g(),其他方法就不能叫ShowDialogWarning()或 WarningDialogShow(),而应

是ShowWarningDialog()。

(3) 命名空间的名称

命名空间的名称非常重要,一定要仔细设计,以避免一个命名空间中对象的名称

与其他对象同名。记住,命名空间的名称是.NET区分共享程序集中对象名的惟

一方式。如果软件包的命名空间使用的名称与另一个软件包相同,而这两个软件

包都安装在一台计算机上,就会出问题。因此,最好用自己的公司名创建顶级的

命名空间,再嵌套后面技术范围较窄、用户所在小组或部门、或类所在软件包的

命名空间。Microsoft建议使用如下的命名空间:.

Name>,例如:

Controllers

s

(4) 名称和关键字

名称不应与任何关键字冲突,这是非常重要的。实际上,如果在代码中,试图给

某个对象指定与C#关键字同名的名称,就会出现语法错误,因为编译器会假定

该名称表示一个语句。但是,由于类可能由其他语言编写的代码访问,所以不能

使用其他.NET语言中的关键字作为对象的名称。一般说来,C++关键字类似于C

#关键字,不太可能与C++混淆,Visual C++常用的关键字则用两个下划线字符

开头。与C#一样,C++关键字都是小写字母,如果要遵循公共类和成员使用Pas

cal风格的名称的约定,则在它们的名称中至少有一个字母是大写,因此不会与

C++关键字冲突。另一方面,VB的问题会多一些,因为VB的关键字要比C#的多,

而且它不区分大小写,不能依赖于Pascal风格的名称来区分类和成员。

表2-12列出了VB中的关键字和标准函数调用,无论对C#公共类使用什么大小

写组合,这些名称都不应使用。

表 2-12

Abs

Do

Add

Loc

Double

Each

Else

Empty

End

Enum

EOF

Erase

Err

Error

Event

RGB

Local

Lock

LOF

Long

Loop

LTrim

Me

Mid

Minute

MIRR

MkDir

Right

RmDir

Rnd

SaveSettings

Second

Seek

Select

SetAttr

SetException

Shared

Shell

AddHandler

AddressOf

And

Ansi

AppActivate

Append

As

Asc

Assembly

Atan

(续表)

Auto

Beep

Binary

BitAnd

BitNot

BitOr

BitXor

Boolean

Exit

Exp

Explicit

ExternalSource

False

FileAttr

FileCopy

FileDateTime

Module

Month

MustInherit

MustOverride

MyBase

MyClass

Namespace

New

Short

Sign

Sin

Single

SLN

Space

Spc

Split

ByRef

Byte

ByVal

Call

Case

Catch

CBool

CByte

CDate

CDbl

CDec

ChDir

ChDrive

Choose

Chr

CInt

Class

Clear

CLng

Collection

Command

Compare

Const

Cos

CreateObject

CShort

CSng

CStr

FileLen

Filter

Finally

Fix

For

Format

FreeFile

Friend

Function

FV

Get

GetAllSettings

GetAttr

GetException

GetObject

GetSetting

GetType

GoTo

Handles

Hour

If

Iif

Implements

Imports

In

Inherits

Input

InStr

Next

Not

Nothing

NotInheritable

NotOverridable

Now

NPer

NPV

Null

Object

Oct

Off

On

Open

Option

Optional

Or

Overloads

Overridable

ParamArray

Pmt

PPmt

Preserve

Print

Private

Property

Public

Put

Sqrt

Static

Step

Stop

Str

StrComp

StrConv

Strict

String

Structure

Sub

Switch

SYD

SyncLock

Tab

Tan

Text

Then

Throw

Timer

TimeSerial

TimeValue

To

Today

Trim

Try

TypeName

TypeOf

(续表)

CurDir

Date

DateAdd

DateDiff

DatePart

DateSerial

DateValue

Day

DDB

Decimal

Int

Integer

Interface

Ipmt

IRR

Is

IsArray

IsDate

IsDbNull

IsNumeric

PV

QBColor

Raise

RaiseEvent

Randomize

Rate

Read

ReadOnly

ReDim

Remove

UBound

UCase

Unicode

Unlock

Until

Val

Weekday

While

Width

With

Declare

Default

Delegate

DeleteSetting

Dim

Dir

Item

Kill

Lcase

Left

Lib

Line

RemoveHandler

Rename

Replace

Reset

Resume

Return

WithEvents

Write

WriteOnly

Xor

Year

2. 属性和方法的使用

类中出现混乱的一个方面是一个数是用属性还是方法来表示。这没有硬性规定,

但一般情况下,如果该对象的外观和操作都像一个变量,就应使用属性来表示它

(属性详见第3章),即:

客户机代码应能读取它的值,最好不要使用只写属性,例如,应使用SetPasswor

d()方法,而不是Password只写属性。

读取该值不应花太长的时间。实际上,如果它是一个属性,通常表示读取过程花

的时间相对较短。

读取该值不应有任何不希望的负面效应。设置属性的值,不应有与该属性不直接

相关的负面效应。设置对话框的宽度会改变该对话框在屏幕上的外观,这是可以

的,因为它与属性是相关的。

应可以用任何顺序设置属性。在设置属性时,最好不要因为还没有设置另一个相

关的属性而抛出一个异常。例如,如果为了使用访问数据库的类,需要设置Con

nectionString、UserName和Password,应确保了已经执行了该类,这样用户才能

按照任何顺序设置它们。

顺序读取属性也应有相同的效果。如果属性的值可能会出现预料不到的改变,就

应把它编写为一个方法。在监视汽车运动的类中,把speed编写为属性就不是一

种好的方式,而应使用GetSpeed(),另一方面,应把Weight 和EngineSize编写为

属性,因为对于给定的对象,它们是不会改变的。

如果要编码的对象满足上述所有条件,就应对它使用属性,否则就应使用方法。

3. 字段的用法

字段的用法非常简单。字段应总是私有的,但在某些情况下也可以把常量或只读

字段设置为公有,原因是如果把字段设置为公有,就可以在以后扩展或修改类。

遵循上面的规则就可以编写出好的代码,而且这些规则应与面向对编程的风格一

起使用。

Microsoft在保持一致性方面相当谨慎,在编写.NET基类时就可以遵循它自己的

规则。在编写.NET代码时应很好地遵循这些规则,对于基类来说,就是类、成

员、命名空间的命名方式和类层次结构的工作方式等,如果编写代码的风格与基

类的编写风格相同,就不会犯什么错误。

2.15 小结

本章介绍了一些C#基本语法,包括编写简单的C#程序需要掌握的内容。我们讲

述了许多基础知识,但其中有许多是熟悉C风格语言(甚或JavaScript)的开发

人员能立即领悟的。本章的主要内容包括:

变量的作用域和访问级别

声明各种数据类型的变量

在C#程序中控制执行流

注释和XML文档说明

预处理器指令

用法规则和命名约定,在编写C#代码时应遵循这些规则,使代码符合一般的.NE

T规范,这样其他人就很容易理解您所编写的代码了。

C#语法与C++/Java语法非常类似,但仍存在一些小区别。在许多领域,将这些

语法与功能结合起来,会使编码更快速,例如高质量的字符串处理功能。C#还有

一个强大的已定义类型系统,该系统基于值类型和引用类型的区别。下面两章将

进一步介绍C#的面向对象编程特性。

第3章 对象和类型

到目前为止,我们介绍了组成C#语言的主要内容——变量的声明、数据类型和

程序流语句,并简要介绍了一个只包含Main()方法的完整小例子。但还没有介

绍如何把这些内容组合在一起,构成一个完整的程序,其关键就在于对类的处理。

这就是本章的主题。本章的主要内容如下:

类和结构的区别

字段、属性和方法

按值和引用传送参数

方法重载

构造函数和静态构造函数

只读字段

Object类,其他类型都从该类派生而来

第4章将介绍继承以及与继承相关的特性。

提示:

本章将讨论与类相关的基本语法,但假定您已经熟悉了使用类的基本原则,例如,知道

构造函数和属性的含义,因此我们只是大致论述如何把这些原则应用于C#代码。如果您不

熟悉类的概念,请参阅附录A,并可从网站上下载本书的代码。

本章介绍的这些概念不一定得到了大多数面向对象语言的支持。例如对象构造函

数是您熟悉的、使用广泛的一个概念,但静态构造函数就是C#的新增内容,所

以我们将解释静态构造函数的工作原理。

3.1 类和结构

类和结构实际上都是创建对象的模板,每个对象都包含数据,并提供了处理和访

问数据的方法。类定义了每个类对象(称为实例)可以包含什么数据和功能。例如,

如果一个类表示一个顾客,就可以定义字段CustomerID、FirstName、LastName

和Address,以包含该顾客的信息。还可以定义处理存储在这些字段中的数据的

功能。接着,就可以实例化这个类的对象,以表示某个顾客,并为这个实例设置

这些字段,使用其功能。

class PhoneCustomer

{

public const string DayOfSendingBill ="Monday";

public int CustomerID;

public string FirstName;

public string LastName;

}

结构在内存中的存储方式(类是存储在堆(heap)上的引用类型,而结构是存储在

堆栈(stack)上的值类型)、访问方式和一些特征(如结构不支持继承)与类不同。

较小的数据类型使用结构可提高性能。但在语法上,结构与类非常相似,主要的

区别是使用关键字struct代替class来声明结构。例如,如果希望所有的Phon

eCustomer实例都存储在堆栈上,而不是存储在托管堆上,就可以编写下面的语

句:

struct PhoneCustomerStruct

{

public const string DayOfSendingBill = "Monday";

public int CustomerID;

public string FirstName;

public string LastName;

}

对于类和结构,都使用关键字new来声明实例:这个关键字创建对象并对其进行

初始化。在下面的例子中,类和结构的字段值都默认为0:

PhoneCustomer myCustomer = new PhoneCustomer(); //works for a class

PhoneCustomerStruct myCustomer2 = new PhoneCustomerStruct(); // works for

a struct

在大多数情况下,类要比结构常用得多。因此,我们先讨论类,然后指出类和结

构的区别,以及选择使用结构而不使用类的特殊原因。但除非特别说明,否则就

可以假定用于类的代码也适用于结构。

3.2 类成员

类中的数据和函数称为类的成员。Microsoft的正式术语对数据成员和函数成员

进行了区分。除了这些成员外,类还可以包含嵌套的类型(例如其他类)。类中的

所有成员都可以声明为public(此时可以在类的外部直接访问它们)或private

(此时,它们只能由类中的其他代码来访问)。与VB、C++和Java一样,C#在这

个方面还有变化,例如protected(表示成员仅能由该成员所在的类及其派生类

访问),第4章将详细解释各种访问级别。

3.2.1 数据成员

数据成员包含了类的数据—— 字段、常量和事件。数据成员可以是静态数据(与

整个类相关)或实例数据(类的每个实例都有它自己的数据副本)。通常,对于面

向对象的语言,类成员总是实例成员,除非用static进行了显式的声明。

字段是与类相关的变量。在前面的例子中已经使用了PhoneCustomer类中的字

段:

一旦实例化PhoneCustomer对象,就可以使用语法ame来访问这

些字段:

PhoneCustomer Customer1 = new PhoneCustomer();

ame = "Simon";

常量与类的关联方式同变量与类的关联方式一样。使用const关键字来声明常

量。如果它们声明为public,就可以在类的外部访问。

class PhoneCustomer

{

public const string DayOfSendingBill = "Monday";

public int CustomerID;

public string FirstName;

public string LastName;

}

事件是类的成员,在发生某些行为(例如改变类的字段或属性,或者进行了某种

形式的用户交互操作)时,它可以让对象通知调用程序。客户可以包含称为“事

件处理程序”的代码来响应该事件。第6章将详细介绍事件。

3.2.2 函数成员

函数成员提供了操作类中数据的某些功能,包括方法、属性、构造函数和终结器

(finalizer)、运算符以及索引器。

方法是与某个类相关的函数,它们可以是实例方法,也可以是静态方法。实例方

法处理类的某个实例,静态方法提供了更一般的功能,不需要实例化一个类(例

如ine()方法)。下一节介绍方法。

属性是可以在客户机上访问的函数组,其访问方式与访问类的公共字段类似。C

#为读写类上的属性提供了专用语法,所以不必使用那些名称中嵌有Get或Set

的偷工减料的方法。因为属性的这种语法不同于一般函数的语法,在客户代码中,

虚拟的对象被当做实际的东西。

构造函数是在实例化对象时自动调用的函数。它们必须与所属的类同名,且不能

有返回类型。构造函数用于初始化字段的值。

终结器类似于构造函数,但是在CLR检测到不再需要某个对象时调用。它们的名

称与类相同,但前面有一个~符号。C++程序员应注意,终结器在C#中比在C++

中用得少得多,因为CLR会自动进行垃圾收集,另外,不可能预测什么时候调用

终结器。第7章将介绍终结器。

运算符执行的最简单的操作就是+和–。在对两个整数进行相加操作时,严格地

说,就是对整数使用+运算符。C#还允许指定把已有的运算符应用于自己的类(运

算符重载)。第5章将详细论述运算符。

索引器允许对象以数组或集合的方式进行索引。第5章介绍索引器。

1. 方法

在VB、C和C++中,可以定义与类完全不相关的全局函数,但在C#中不能这样做。

在C#中,每个函数都必须与类或结构相关。

注意,正式的C#术语实际上并没有区分函数和方法。在这个术语中,“函数成

员”不仅包含方法,而且也包含类或结构的一些非数据成员。它包括索引器、运

算符、构造函数和析构函数等,甚至还有属性。这些都不是数据成员,字段、常

量和事件才是数据成员。本章将详细讨论方法。

(1) 方法的声明

在C#中,定义方法的语法与C风格的语言相同,与C++和Java中的语法也相同。

与C++的主要语法区别是,在C#中,每个方法都单独声明为public或private,

不能使用public:块把几个方法定义组合起来。另外,所有的C#方法都在类定

义中声明和定义。在C#中,不能像在C++中那样把方法的实现代码分隔开来。

在C#中,方法的定义包括方法的修饰符(例如方法的可访问性)、返回值的类型,

然后是方法名、输入参数的列表(用圆括号括起来)和方法体(用花括号括起来)。

[modifiers] return_type MethodName([parameters])

{

// Method body

}

每个参数都包括参数的类型名及在方法体中的引用名称。但如果方法有返回值,

return语句就必须与返回值一起使用,以指定出口点,例如:

public bool IsSquare(Rectangle rect)

{

return ( == );

}

这段代码使用一个表示矩形的.NET基类gle。

如果方法没有返回值,就把返回类型指定为void,因为不能省略返回类型。如

果方法不带参数,仍需要在方法名的后面写上一对空的圆括号()(就像本章前面

的Main()方法)。此时return语句就是可选的—— 当到达右花括号时,方法会

自动返回。注意方法可以包含任意多个return语句:

public bool IsPositive(int value)

{

if (value < 0)

return false;

return true;

}

(2) 调用方法

C#中调用方法的语法与C++和Java中的一样,C#和VB的惟一区别是在C#中调用

方法时,必须使用圆括号,这要比VB 6中有时需要括号,有时不需要括号的规

则简单一些。

下面的例子MathTest说明了类的定义和实例化、方法的定义和调用的语法。除

了包含Main()方法的类之外,它还定义了类MathTest,该类包含两个方法和一

个字段。

using System;

namespace arp. MathTestSample

{

class MainEntryPoint

{

static void Main()

{

// Try calling some static functions

ine("Pi is " + ());

int x = areOf(5);

ine("Square of 5 is " + x);

// Instantiate at MathTest object

MathTest math = new MathTest(); // this is C#'s way of

// instantiating a reference type

// Call non-static methods

= 30;

ine(

"Value field of math variable contains " + );

ine("Square of 30 is " + are());

}

}

// Define a class named MathTest on which we will call a method

class MathTest

{

public int value;

public int GetSquare()

{

return value*value;

}

public static int GetSquareOf(int x)

{

return x*x;

}

public static double GetPi()

{

return 3.14159;

}

}

}

运行mathTest示例,会得到如下结果:

csc

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.

Pi is 3.14159

Square of 5 is 25

Value field of math variable contains 30

Square of 30 is 900

从代码中可以看出,MathTest类包含一个字段和一个方法,该字段包含一个数

字,该方法计算数字的平方。这个类还包含两个静态方法,一个返回pi的值,

另一个计算把作为参数传入的数字的平方。

这个类有一些功能并不是C#程序设计的好例子。例如,GetPi()通常作为const

字段来执行,而好的设计应使用目前还没有介绍的概念。

C++和Java开发人员应很熟悉这个例子的大多数语法。如果您有VB的编程经验,

只需把MathTest类看作一个执行字段和方法的VB类模块。但无论使用什么语言,

都要注意两个要点。

(3) 给方法传递参数

参数可以通过引用或值传递给方法。在变量通过引用传递给方法时,被调用的方

法得到的就是这个变量,所以在方法内部对变量进行的任何改变在方法退出后仍

旧发挥作用。而如果变量是通过值传送给方法的,被调用的方法得到的是变量的

一个副本,也就是说,在方法退出后,对变量进行的修改会丢失。对于复杂的数

据类型,按引用传递的效率更高,因为在按值传递时,必须复制大量的数据。

在C#中,所有的参数都是通过值来传递的,除非特别说明。这与C++是相同的,

但与VB相反。但是,在理解引用类型的传递过程时需要注意。因为引用类型的

对象只包含对象的引用,它们只给方法传递这个引用,而不是对象本身,所以对

底层对象的修改会保留下来。相反,值类型的对象包含的是实际数据,所以传递

给方法的是数据本身的副本。例如,int通过值传递给方法,方法对该int的值

所作的任何改变都没有改变原int对象的值。但如果数组或其他引用类型(如类)

传递给方法后,方法会使用该引用改变这个数组中的值,而新值会反射到原来的

数组对象上。

下面的例子说明了这一点:

using System;

namespace arp. ParameterTestSample

{

class ParameterTest

{

static void SomeFunction(int[] ints, int i)

{

ints[0] = 100;

i = 100;

}

public static int Main()

{

int i = 0;

int[] ints = { 0, 1, 2, 4, 8 };

// Display the original values

ine("i = " + i);

ine("ints[0] = " + ints[0]);

ine("");

// After this method returns, ints will be changed,

// but i will not

SomeFunction(ints, i);

ine("i = " + i);

ine("ints[0] = " + ints[0]);

return 0;

}

}

}

结果如下:

csc

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.