目录
6.0.2 #if, #elif, #else和#endif
1.0 变量(略)
1.0.1 变量的初始化(略)
1.0.2 类型推断
类型推断使用var关键字。声明变量的语法有些变化。编译器可以根据变量的初始化值“推断”变量的类型。例如:
int someNumber = 0;
就变成:
var someNumber = 0;
即使someNumber从来没有声明为int,编译器也可以确定,只要someNumber在其作用域内,就是一个int。编译后,上面两个语句是等价的。
下面是另一个小例子:
using System;
namespace Wrox.ProCSharp.Basics{
class Program{
static void Main(string[] args){
var name = "Bugs Bunny";
var age = 25;
var isRabbit = true;
Type nameType = name.GetType();
Type ageType = age.GetType();
Type isRabbitType = isRabbit.GetType();
Console.WriteLine("name is type " + nameType.ToString());
Console.WriteLine("age is type " + ageType.ToString());
Console.WriteLine("isRabbit is type " + isRabbitType.ToString());
}
}
}
这个程序的输出如下:
name is type System.String
age is type System.Int32
isRabbit is type System.Bool
需要遵循一些规则。变量必须初始化。否则,编译器就没有推断变量类型的依据。初始化器不能为空,且必须放在表达式中。不能把初始化器设置为一个对象,除非在初始化器中创建了一个新对象。第3章在讨论匿名类型时将详细探讨。
声明了变量,推断出了类型后,变量的类型就不能改变了。变量的类型建立后,就遵循其他变量类型遵循的强类型化规则。
1.0.3 变量的作用域(略)
1.0.4 常量
顾名思义,常量是其值在使用过程中不会发生变化的变量。在声明和初始化变量时,在变量的前面加上关键字const,就可以把该变量指定为一个常量:
const int a = 100; // This value cannot be changed
Visual Basic和C++开发人员非常熟悉常量。但C++开发人员应注意,C#不支持C++常量的所有细微的特性。在C++中,变量不仅可以声明为常量,而且根据声明,还可以有常量指针、指向常量的变量指针、常量方法(不改变包含对象的内容)、方法的常量参数等。这些细微的特性在C#中都删除了,只能把局部变量和字段声明为常量。
常量具有如下特征:
● 常量必须在声明时初始化。指定了其值后,就不能再修改了。
● 常量的值必须能在编译时用于计算。因此,不能用从一个变量中提取的值来初始化常量。如果需要这么做,应使用只读字段(详见第3章)。
● 常量总是静态的。但注意,不必(实际上,是不允许)在常量声明中包含修饰符static。
在程序中使用常量至少有3个好处:
● 常量用易于理解的清楚的名称替代了含义不明确的数字或字符串,使程序更易于阅读。
● 常量使程序更易于修改。例如,在C#程序中有一个SalesTax常量,该常量的值为6%。如果以后销售税率发生变化,把新值赋给这个常量,就可以修改所有的税款计算结果,而不必查找整个程序,修改税率为0.06的每个项。
● 常量更容易避免程序出现错误。如果要把另一个值赋给程序中的一个常量,而该常量已经有了一个值,编译器就会报告错误。
2.0 预定义数据类型
2.4.1 值类型和引用类型
在开始介绍C#中的数据类型之前,理解C#把数据类型分为两种是非常重要的:
● 值类型
● 引用类型
下面几节将详细介绍值类型和引用类型的语法。从概念上看,其区别是值类型直接存储其值,而引用类型存储对值的引用。C#中的值类型基本上等价于Visual Basic或C++中的简单类型(整型、浮点型,但没有指针或引用)。引用类型与Visual Basic中的引用类型相同,与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++中声明指针和Visual Basic中的对象引用是相同的——在C++和Visual Basic中,都不会创建对象。要创建对象,就必须使用new关键字,如上所示。因为x和y引用同一个对象,所以对x的修改会影响y,反之亦然。因此上面的代码会显示30和50。
大多数更复杂的C#数据类型,包括我们自己声明的类都是引用类型。它们分配在堆中,其生存期可以跨多个函数调用,可以通过一个或几个别名来访问。CLR执行一种精细的算法,来跟踪哪些引用变量仍是可以访问的,哪些引用变量已经不能访问了。CLR会定期删除不能访问的对象,把它们占用的内存返回给操作系统。这是通过垃圾收集器实现的。
把基本类型(如int和bool)规定为值类型,而把包含许多字段的较大类型(通常在有类的情况下)规定为引用类型,C#设计这种方式的原因是可以得到最佳性能。如果要把自己的类型定义为值类型,就应把它声明为一个结构。
2.4.2 CTS类型
如第1章所述,C#认可的基本预定义类型并没有内置于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 预定义的值类型
内置的值类型表示基本数据类型,例如整型和浮点类型、字符类型和布尔类型。
1.整型
名称 | CTS 类 型 | 说 明 | 范 围 |
sbyte | System.SByte | 8位有符号的整数 | –128~127 |
short | System.Int16 | 16位有符号的整数 | –32 768~32 767 |
int | System.Int32 | 32位有符号的整数 | –2 147 483 648~2 147 483 647 |
long | System.Int64 | 64位有符号的整数 | –9 223 372 036 854 775 808~9 223 372 036 854 775 807 |
byte | System.Byte | 8位无符号的整数 | 0~255 |
ushort | System.Uint16 | 16位无符号的整数 | 0~65535 |
uint | System.Uint32 | 32位无符号的整数 | 0~4 294 967 295 |
ulong | System.Uint64 | 64位无符号的整数 | 0~18 446 744 073 709 551 615 |
当然,Visual Basic开发人员会发现有许多类型名称是新的。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位。所有整数类型的变量都能赋予十进制或十六进制的值,后者需要0x前缀:
long x = 0x12ab;
如果对一个整数是int、uint、long或是ulong没有任何显式的声明,则该变量默认为int类型。为了把键入的值指定为其他整数类型,可以在数字后面加上如下字符:
uint ui = 1234U;
long l = 1234L;
ulong ul = 1234UL;
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 |
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;
3. bool类型
C#的bool类型用于包含布尔值true或false。bool值和整数值不能相互隐式转换。如果变量(或函数的返回类型)声明为bool类型,就只能使用值true或false。如果试图使用0表示false,非0值表示true,就会出错。这与C++相同。
4. 字符类型
为了保存单个字符的值,C#支持char数据类型。C++的char表示一个8位字符,而C#的char包含16位。其部分原因是不允许在char类型与8位byte类型之间进行隐式转换。尽管8位足够编码英语中的每个字符和数字0~9了,但它们不够编码更大的符号系统中的每个字符(例如中文)。为了面向全世界,计算机行业正在从8位字符集转向16位的Unicode模式,ASCII编码是Unicode的一个子集。
char类型的字面量是用单引号括起来的,例如'A'。如果把字符放在双引号中,编译器会把它看作是字符串,从而产生错误。
除了把char表示为字符字面量之外,还可以用4位十六进制的Unicode值(例如'\u0041'),带有数据类型转换的整数值(例如(char)65),或十六进制数('\x0041')表示它们。它们还可以用转义序列表示,如表所示。
转义序列 | 字符 |
\' | 单引号 |
\" | 双引号 |
\\ | 反斜杠 |
\0 | 空 |
\a | 警告 |
\b | 退格 |
\f | 换页 |
\n | 换行 |
\r | 回车 |
\t | 水平制表符 |
\v | 垂直制表符 |
C++开发人员应注意,因为C#本身有一个string类型,所以不需要把字符串表示为char类型的数组。
2.4.4 预定义的引用类型
C#支持两个预定义的引用类型
名 称 | CTS 类 | 说 明 |
object | System.Object | 根类型,CTS中的其他类型都是从它派生而来的(包括值类型) |
string | System.String | Unicode字符串 |
1. object类型
许多编程语言和类结构都提供了根类型,层次结构中的其他对象都从它派生而来。C#和.NET也不例外。在C#中,object类型就是最终的父类型,所有内置类型和用户定义的类型都从它派生而来。这是C#的一个重要特性,它把C#与Visual Basic 6.0和C++区分开来,但其行为与Java非常类似。所有的类型都隐含地最终派生于System.Object类,这样,object类型就可以用于两个目的:
● 可以使用object引用绑定任何子类型的对象。例如,第6章将说明如何使用object类型把堆栈中的一个值对象装箱,再移动到堆中。object引用也可以用于反射,此时必须有代码来处理类型未知的对象。这类似于C++中的void指针或Visual Basic中的Variant数据类型。
● object类型执行许多一般用途的基本方法,包括Equals()、GetHashCode()、GetType()和ToString()。用户定义的类需要使用一种面向对象技术—— 重写(见第4章),提供其中一些方法的替代执行代码。例如,重写ToString()时,要给类提供一个方法,给出类本身的字符串表示。如果类中没有提供这些方法的实现代码,编译器就会使用object类型中的实现代码,它们在类中的执行不一定正确。
2. string类型
有C和C++开发经验的人员可能在使用C风格的字符串时不太顺利。C或C++字符串不过是一个字符数组,因此客户机程序员必须做许多工作,才能把一个字符串复制到另一个字符串上,或者连接两个字符串。实际上,对于一般的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变量仍指向原来的对象,所以它的值没有改变。
字符串字面量放在双引号中("...");如果试图把字符串放在单引号中,编译器就会把它当作char,从而引发错误。C#字符串和char一样,可以包含Unicode、十六进制数转义序列。因为这些转义序列以一个反斜杠开头,所以不能在字符串中使用这个非转义的反斜杠字符,而需要用两个反斜杠字符(\\)来表示它:
string filepath = "C:\\ProCSharp\\First.cs";
即使用户相信自己可以在任何情况下都记住要这么做,但键入两个反斜杠字符会令人迷惑。幸好,C#提供了另一种替代方式。可以在字符串字面量的前面加上字符@,在这个字符后的所有字符都看作是其原来的含义——它们不会解释为转义字符:
string filepath = @"C:\ProCSharp\First.cs";
3.0 命名空间
命名空间与程序集无关。同一个程序集中可以有不同的命名空间,也可以在不同的程序集中定义同一个命名空间中的类型。
3.0.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;
}
}
2.8.2 命名空间的别名
using关键字的另一个用途是给类和命名空间指定别名。如果命名空间的名称非常长,又要在代码中使用多次,但不希望该命名空间的名称包含在using指令中(例如,避免类名冲突),就可以给该命名空间指定一个别名,其语法如下:
using alias = NamespaceName;
下面的例子(前面例子的修订版本)给Wrox.ProCSharp.Basics命名空间指定Introduction别名,并使用这个别名实例化了一个NamespaceExample对象,这个对象是在该命名空间中定义的。注意命名空间别名的修饰符是::。因此将先从Introduction命名空间别名开始搜索。如果在相同的作用域中引入了一个Introduction类,就会发生冲突。即使出现了冲突,::操作符也允许引用别名。NamespaceExample类有一个方法GetNamespace(),该方法调用每个类都有的GetType()方法,以访问表示类的类型的Type对象。下面使用这个对象来返回类的命名空间名:
using System;
using Introduction = Wrox.ProCSharp.Basics;
class Test
{
public static int Main()
{
Introduction::NamespaceExample NSEx =
new Introduction::NamespaceExample();
Console.WriteLine(NSEx.GetNamespace());
return 0;
}
}
namespace Wrox.ProCSharp.Basics
{
class NamespaceExample
{
public string GetNamespace()
{
return this.GetType().Namespace;
}
}
}
4.0 Main()方法
本章的开头提到过,C#程序是从方法Main()开始执行的。这个方法必须是类或结构的静态方法,并且其返回类型必须是int或void。
虽然显式指定public修饰符是很常见的,因为按照定义,必须在程序外部调用该方法,但我们给该方法指定什么访问级别并不重要,即使把该方法标记为private,它也可以运行。
4.0.1 多个Main()方法
在编译C#控制台或Windows应用程序时,默认情况下,编译器会在类中查找与上述签名匹配的Main方法,并使这个类方法成为程序的入口。如果有多个Main方法,编译器就会返回一个错误消息.
但是,可以使用/main选项,其后跟Main()方法所属类的全名(包括命名空间),明确告诉编译器把哪个方法作为程序的入口点:
csc MainExample.cs /main:Wrox.ProCSharp.Basics.MathExample(命令行模式)
4.0.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;
}
}
}
5.0 使用注释
5.0.1 XML文档说明
如前所述,除了C风格的注释外,C#还有一个非常好的功能,本章将讨论这一功能。根据特定的注释自动创建XML格式的文档说明。这些注释都是单行注释,但都以3个斜杠(///)开头,而不是通常的两个斜杠。在这些注释中,可以把包含类型和类型成员的文档说明的XML标识符放在代码中。
标识符 | 说明 |
<c> | 把行中的文本标记为代码,例如<c>int i = 10;</c> |
<code> | 把多行标记为代码 |
<example> | 标记为一个代码示例 |
<exception> | 说明一个异常类(编译器要验证其语法) |
<include> | 包含其他文档说明文件的注释(编译器要验证其语法) |
<list> | 把列表插入到文档说明中 |
<param> | 标记方法的参数(编译器要验证其语法) |
<paramref> | 表示一个单词是方法的参数(编译器要验证其语法) |
<permission> | 说明对成员的访问(编译器要验证其语法) |
<remarks> | 给成员添加描述 |
<returns> | 说明方法的返回值 |
<see> | 提供对另一个参数的交叉引用(编译器要验证其语法) |
<seealso> | 提供描述中的“参见”部分(编译器要验证其语法) |
<summary> | 提供类型或成员的简短小结 |
<value> | 描述属性 |
要了解它们的工作方式,可以在上一节的MathLibrary.cs文件中添加一些XML注释,并称之为Math.cs。我们给类及其Add()方法添加一个<summary>元素,也给Add()方法添加一个<returns>元素和两个<param>元素:
// Math.cs
namespace Wrox.ProCSharp.Basics
{
///<summary>
/// Wrox.ProCSharp.Basics.Math class.
/// Provides a method to add two integers.
///</summary>
public class Math
{
///<summary>
/// The Add method allows us to add two integers
///</summary>
///<returns>Result of the addition (int)</returns>
///<param name="x">First number to add</param>
///<param name="y">Second number to add</param>
public int Add(int x, int y)
{
return x + y;
}
}
}
C#编译器可以把XML元素从特定的注释中提取出来,并使用它们生成一个XML文件。要让编译器为程序集生成XML文档说明,需在编译时指定/doc选项,后跟要创建的文件名:
csc /t:library /doc:Math.xml Math.cs
如果XML注释没有生成格式正确的XML文档,编译器就生成一个错误。
上面的代码会生成一个XML文件Math.xml,如下所示。
<?xml version="1.0"?>
<doc>
<assembly>
<name>Math</name>
</assembly>
<members>
<member name="T:Wrox.ProCSharp.Basics.Math">
<summary>
Wrox.ProCSharp.Basics.Math class.
Provides a method to add two integers.
</summary>
</member>
<member name=
"M:Wrox.ProCSharp.Basics.Math.Add(System.Int32,System.Int32)">
<summary>
The Add method allows us to add two integers.
</summary>
<returns>Result of the addition (int)</returns>
<param name="x">First number to add</param>
<param name="y">Second number to add</param>
</member>
</members>
</doc>
注意,编译器为我们做了一些工作——它创建了一个<assembly>元素,并为该文件中的每个类型或类型成员添加一个<member>元素。每个<member>元素都有一个name特性,其中包含成员的全名,前面有一个字母表示其类型:"T:"表示这是一个类型,"F:" 表示这是一个字段,"M:" 表示这是一个成员。
6.0 C#预处理器指令
除了前面介绍的常用关键字外,C#还有许多名为“预处理器指令”的命令。这些命令从来不会转化为可执行代码中的命令,但会影响编译过程的各个方面。例如,使用预处理器指令可以禁止编译器编译代码的某一部分。如果计划发布两个版本的代码,即基本版本和有更多功能的企业版本,就可以使用这些预处理器指令。在编译软件的基本版本时,使用预处理器指令还可以禁止编译器编译与额外功能相关的代码。另外,在编写提供调试信息的代码时,也可以使用预处理器指令。实际上,在销售软件时,一般不希望编译这部分代码。
预处理器指令的开头都有符号#。
6.0.1 #define和 #undef
#define的用法如下所示:
#define DEBUG
它告诉编译器存在给定名称的符号,在本例中是DEBUG。这有点类似于声明一个变量,但这个变量并没有真正的值,只是存在而已。这个符号不是实际代码的一部分,而只在编译器编译代码时存在。在C#代码中它没有任何意义。
#undef正好相反—— 删除符号的定义:
#undef DEBUG
如果符号不存在,#undef就没有任何作用。同样,如果符号已经存在,#define也不起作用。
必须把#define和#undef命令放在C#源代码的开头,在声明要编译的任何对象的代码之前。
#define本身并没有什么用,但与其他预处理器指令(特别是#if)结合使用时,它的功能就非常强大了。
注意:
这里应注意一般C#语法的一些变化。预处理器指令不用分号结束,一般一行上只有一个命令。这是因为对于预处理器指令,C#不再要求命令用分号结束。如果它遇到一个预处理器指令,就会假定下一个命令在下一行上。
6.0.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特性提供了另一种机制,详见第13章。
#if和 #elif还支持一组逻辑运算符!、==、!=和 ||。如果符号存在,就被认为是true,否则为false,例如:
#if W2K && (ENTERPRISE==false) // if W2K is defined but ENTERPRISE isn't
6.0.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*");
6.0.4 #region和#endregion
#region和 #endregion指令用于把一段代码标记为有给定名称的一个块,如下所示。
#region Member Field Declarations
int x;
double d;
Currency balance;
#endregion
这看起来似乎没有什么用,它不影响编译过程。这些指令的优点是它们可以被某些编辑器识别,包括Visual Studio编辑器。这些编辑器可以使用这些指令使代码在屏幕上更好地布局。
6.0.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
6.0.6 #pragma
#pragma指令可以抑制或恢复指定的编译警告。与命令行选项不同,#pragma指令可以在类或方法上执行,对抑制警告的内容和抑制的时间进行更精细的控制。下面的例子禁止字段使用警告,然后在编译MyClass类后恢复该警告。
#pragma warning disable 169
public class MyClass
{
int neverUsedField;
}
#pragma warning restore 169
7.0 用法约定
7.0.1 命名约定(略)
7.0.2. 属性和方法的使用
类中出现混乱的一个方面是一个数是用属性还是方法来表示。这没有硬性规定,但一般情况下,如果该对象的外观和操作都像一个变量,就应使用属性来表示它(属性详见第3章),即:
● 客户机代码应能读取它的值,最好不要使用只写属性,例如,应使用SetPassword()方法,而不是Password只写属性。
● 读取该值不应花太长的时间。实际上,如果它是一个属性,通常表示读取过程花的时间相对较短。
● 读取该值不应有任何不希望的负面效应。设置属性的值,不应有与该属性不直接相关的负面效应。设置对话框的宽度会改变该对话框在屏幕上的外观,这是可以的,因为它与属性是相关的。
● 应可以用任何顺序设置属性。在设置属性时,最好不要因为还没有设置另一个相关的属性而抛出一个异常。例如,如果为了使用访问数据库的类,需要设置ConnectionString、UserName和Password,应确保已经执行了该类,这样用户才能按照任何顺序设置它们。
● 顺序读取属性也应有相同的效果。如果属性的值可能会出现预料不到的改变,就应把它编写为一个方法。在监视汽车运动的类中,把speed编写为属性就不是一种好的方式,而应使用GetSpeed(),另一方面,应把Weight 和EngineSize编写为属性,因为对于给定的对象,它们是不会改变的。
如果要编码的对象满足上述所有条件,就应对它使用属性,否则就应使用方法。
7.0.3 字段的用法
字段是定义在方法外面的变量,是成员变量,主要是为了类的内部数据交换使用。字段的用法非常简单。字段应总是私有的,但在某些情况下也可以把常量或只读字段设置为公有,原因是如果把字段设置为公有,就可以在以后扩展或修改类。
遵循上面的规则就可以编写出好的代码,而且这些规则应与面向对编程的风格一起使用。
Microsoft在保持一致性方面相当谨慎,在编写.NET基类时遵循了它自己的规则。在编写.NET代码时应很好地遵循这些规则,对于基类来说,就是类、成员、命名空间的命名方式和类层次结构的工作方式等,我们自己的类与基类的风格相同,有助于提高可读性和可维护性。