[导入]C#教程
课程目录
第一课:简单的欢迎程序
第二课:表达式,类型和变量
第三课:选择控制语句
第四课:循环控制语句
第五课:方法
第六课:名称空间
第七课:类的入门
第八课:类的继承
第九课:多态性
第十课:属性
第十一课:索引指示器
第一课:简单的欢迎程序
在本文开始写作的时候,虽然商用C# 编译器尚未推出, 但你可以下载微软的.NET Frameworks SDK Beta 1.
本节课通过介绍几个简单的程序,使得你对C#有所入门。本节程要达到如下几个目的:
1.理解一个C#程序的基本结构。
2.初步了解"名称空间"的概念。
3.初步了解"类"的概念。
4.了解"Main"方法所做的工作。
5.学会如何读取命令行输入信息。
6.学会使用控制台输入/输出 (I/O)语句。
1.清单1-1. 一个简单的欢迎程序Welcome.cs
// Namespace Declaration
using System;
// Program start class
class WelcomeCSS {
// Main begins program execution.
public static void Main() {
// Write to console
Console.WriteLine("Welcome to the C# Station Tutorial!");
}
}
说明
1.清单1-1中的程序包括四个基本元素:名称空间的声明,类,"Main"方法和语句。
2.本例中对名称空间的声明,表明正在使用"System"这个名称空间。
名称空间内包含了一组可以被C#程序调用的代码。有了"using System;"这个声明,就表明程序可以引用该"System"名称空间内的代码,而无需在每个引用的前面加上"System"。关于这一点,我将在后面专门介绍名称空间的课程中详细介绍。
3.类"class WelcomeCSS"包含了程序所要用到的数据,和所要执行的方法的定义。
同诸如接口和结构这样的元素类似,类在程序中是用来描述对象的,这些元素都将会在后续课程中详细介绍。本例中的类不包含数据,只包含一个方法。该方法定义了该类的行为(或者称为该类所能做的事情)。
4.程序运行时,WelcomeCSS类中的该方法表明了该类所要完成的事情。
方法名"Main"作为保留字,作为程序的起点。"Main"前面是个名为"static"的修饰符。"static"修饰符表明该方法仅仅在该特定的类中工作,而不是在在该类的实例中工作。这是必需的,因为一旦程序启动后,并不存在对象的实例。类,对象和实例的具体用法将会在后面的课程中覆盖到。每个方法必须有个返回值类型。本例中,返回值类型是"void",它表明"Main"函数没有返回值。每个方法名的后面也都跟着个参数表,参数表包含有零个或者多个参数并用括号括起来。为了简单起见,没有在"Main"后面添加参数。后面的课程中,将介绍"Main"方法所允许采用的参数类型。
5."Main"方法通过"Console.WriteLine(...)" 语句表明其行为。
"Console" 是"System" 名称空间中的类。"WriteLine(...)"是"Console" 类中的方法。我们使用"."这个句点操作符来标记程序中的从属元素。注意到,我们也可以这样来书写:"System.Console.WriteLine(...)",这样的书写格式是很有趣的,它是根据"namespace.class.method" 的格式进行书写的。如果在程序的一开始,没有采用"using System"的声明,那么就必须严格遵守"System.Console.WriteLine(...)"这样的书写格式。该语句的执行结果是在控制台控制台上输出字符串"Welcome to the C# Station Tutorial!" 。
6.注释是由"//"标出的。
例子中的这些注释都是单行注释,表明从该注释符号的开始处到该行结束处,都是注释部分。如果你的注释要跨越若干行,即多行注释,可以以符号"/*"开始,以符号"*/"结束,其中所包含的全部是注释。你也可以在多行注释符号中包含单行注释。但是,不能在单行注释符号后面放上多行注释符号。程序编译时,将忽略掉注释部分。注释的目的是为了用简单的英语给程序所要完成的工作加上注解。
7.所有语句都以分号";"结束。
类和方法以"{"开始,以"}"结束。任何位于"{"和"}"之间的语句定义为块。块定义了程序元素的活动范围 (或者称为生命期和可见性),这些概念将在后面的课程中加以介绍。
8.可以编写出能够接受命令行输入信息的程序。
命令行输入信息的集合是在"Main"方法中进行处理的。清单1-2中的程序,可以从命令行中接受输入一个名字,之后在控制台上显示出来。
2.清单1-2. 读取命令行输入信息的程序NamedWelcome.cs
// Namespace Declaration
using System;
// Program start class
class NamedWelcome {
// Main begins program execution.
public static void Main(string[] args) {
// Write to console
Console.WriteLine("Hello, {0}!", args[0]);
Console.WriteLine("Welcome to the C# Station Tutorial!");
}
}
说明
1.记住,要把你的名字添加到命令行中。
例如,在命令行中打入"NamedWelcome Joe"。如果不这样做,程序就会崩溃,在后面的课程中,将介绍如何检测这种情况,以及如何避免这种情况的出现。
2.在清单1-2中,在"Main"方法的参数表中有个入口。
参数名是"args"。 在程序的后面部分就要引用该参数。"string[]"是参数"args"的类型。"string"类型用于存放字符。这些字符可以是一个单词,也可以是多个单词。方括号"[]"表示数组,"args"参数由命令行上的若干个单词构成。
3.在"Main"方法的语句中,多了一条"Console.WriteLine(...)"语句。
该语句中的参数表同以往的写法不同,其中有个格式字符串"{0}" 参数。 格式串中的第一个参数从数字0开始,第二个参数从数字1开始,依此类推。 "{0}" 参数意味着引号后面的参数值将会输出到该位置。现在让我们来看看引号后面的参数。
4."args[0]"参数,它指向"args"数组中的第一个字符串。
数组中的第一个元素是args[0], 第二个元素是args[1],依此类推。例如,如果我在命令行中写上"NamedWelcome Joe","args[0]"的值就为"Joe".
让我们回到在格式字符串中嵌入的"{0}" 参数吧,因为"args[0]"是格式串后面的第一个参数, 一旦执行该命令时,"args[0]"的值"Joe"就会替换掉格式串中的"{0}"。一旦执行命令:"NamedWelcome Joe",输出结果就会为:
>Hello, Joe!
>Welcome to the C# Station Tutorial!
通过控制台也可以把输入信息提供给程序。清单1-3演示了交互式处理用户输入的信息的方法。
3.清单1-3. 交互式处理输入信息的程序 InteractiveWelcome.cs
// Namespace Declaration
using System;
// Program start class
class NamedWelcome {
// Main begins program execution.
public static void Main() {
// Write to console/get input
Console.Write("What is your name?: ");
Console.Write("Hello, {0}! ", Console.ReadLine());
Console.WriteLine("Welcome to the C# Station Tutorial!");
}
}
说明
这一次,"Main"方法没有用到任何参数,但现在程序中有了三条语句,前面两条语句不同于第三条语句,它们是:"Console.Write(...)"而不是 "Console.WriteLine(...)"。区别是:"Console.Write(...)"语句把信息输出到控制台,之后光标停留在同一行,而"Console.WriteLine(...)"把信息输出,之后换行。
第一条语句仅仅输出"What is your name?: "到控制台。
第二条语句要等到其参数被适当地处理之后,才会输出信息。 格式串后面的第一个参数是:"Console.ReadLine()"。这就使得程序要等待用户在控制台输入信息,输入信息以回车或者换行结束。该方法的返回值替换了格式串中的"{0}"参数,并输出到控制台上。
最后一个语句也用来输出信息到控制台,这一点我们在前面已经介绍过。一旦运行了程序"InteractiveWelcome",其输出结果为:
>What is your Name?
>Hello, ! Welcome to the C# Station Tutorial!
小结
到现在为止,你已经了解了C#程序的基本结构,名称空间和类。你还了解到"Main"方法是C# 程序的入口,并学会了如何捕捉命令行的输入信息,以及如何进行交互式的I/O操作。
第二课:表达式,类型和变量
逻辑与 & 左
逻辑异或 ^ 左
逻辑或 | 左
条件与 && 左
条件或 || 左
条件 ?: 右
赋值等 = *= /= %= += -= <<= >>= &= ^= |= 右
左结合意味着运算符是从左到右进行运算的。右结合意味着所有的运算是从右到左进行的,如赋值运算符,要等到其右边的计算出来之后,才把结果放到左边的变量中。
2.清单 1-2. 单目运算符: Unary.cs
using System;
class Unary {
public static void Main() {
int unary = 0;
int preIncrement;
int preDecrement;
int postIncrement;
int postDecrement;
int positive;
int negative;
sbyte bitNot;
bool logNot;
preIncrement = ++unary;
Console.WriteLine("Pre-Increment: {0}", preIncrement);
preDecrement = --unary;
Console.WriteLine("Pre-Decrement: {0}", preDecrement);
postDecrement = unary--;
Console.WriteLine("Post-Decrement: {0}", postDecrement);
postIncrement = unary++;
Console.WriteLine("Post-Increment: {0}", postIncrement);
Console.WriteLine("Final Value of Unary: {0}", unary);
positive = -postIncrement;
Console.WriteLine("Positive: {0}", positive);
negative = +postIncrement;
Console.WriteLine("Negative: {0}", negative);
bitNot = 0;
bitNot = (sbyte)(~bitNot);
Console.WriteLine("Bitwise Not: {0}", bitNot);
logNot = false;
logNot = !logNot;
Console.WriteLine("Logical Not: {0}", logNot);
}
}
说明
1.当计算表达式的时候,在后置增一和后置减一运算符进行运算时,先返回其值,再进行增一或者减一运算。当使用前置加号和减号运算符进行运算时,是先进行增一或者减一的运算,然后再返回其结果值。
2.在清单1-2中, 变量unary初始化为0,进行++x 运算时,"unary"的值加1,再把其值1赋给"preIncrement"变量。在进行--x运算时,先把"unary"的值减到0, 再把值0赋给"preDecrement"变量。
3.进行x-运算时,先把"unary"的值0赋给"postDecrement" 变量,之后再把"unary"减到-1。进行x++运算时,先把"unary"的值-1赋给"postIncrement"变量,之后再对"unary"加1,使得"unary"变量现在的值为0。
4.变量"bitNot"初始值为0,进行按位取反运算,本例中,数0表示为二进制"00000000",按位取反之后变为-1,其二进制表示为"11111111"。
5.了解一下表达式"(sbyte)(~bitNot)", 任何对类型sbyte, byte, short 或者 ushort 类型数据的运算,返回结果都是整数。要把值赋给bitNot变量,我们必须使用cast (类型)运算符(强制类型转换),其中Type表示你希望转换成的类型(本例中为sbyte)。 Cast运算符把大范围类型的数据转换为小范围类型的数据时,须特别谨慎,因为此时有丢失数据的危险。一般来说,把小类型的数据赋给大类型变量,并没有问题, 因为大范围数据类型的变量具有足够的空间存放小类型数据。 注意在signed 和unsigned类型之间进行Cast运算时,也存在此类危险。 许多初级程序设计教程对变量的位表示作出了很好的讲解,同时也介绍了直接进行Cast运算的危险。
逻辑非(!)运算符可以处理布尔变量值。本例中,"logNot"变量从false 变为true。
上述程序的输出结果如下:
>Pre-Increment: 1
>Pre-Decrement 0
>Post-Decrement: 0
>Post-Increment -1
>Final Value of Unary: 0
>Positive: 1
>Netative: -1
>Bitwise Not: -1
>Logical Not: True
3.清单 1-3. 二元运算符 Binary.cs
using System;
class Binary {
public static void Main() {
int x, y, result;
float floatResult;
x = 7;
y = 5;
result = x+y;
Console.WriteLine("x+y: {0}", result);
result = x-y;
Console.WriteLine("x-y: {0}", result);
result = x*y;
Console.WriteLine("x*y: {0}", result);
result = x/y;
Console.WriteLine("x/y: {0}", result);
floatResult = (float)x/(float)y;
Console.WriteLine("x/y: {0}", floatResult);
result = x%y;
Console.WriteLine("x%y: {0}", result);
result += x;
Console.WriteLine("result+=x: {0}", result);
}
}
说明
清单1-3 演示了二元操作符的几个例子。加法(+),减法(-),乘法(*)和除法(/)的运算结果,就是我们通常进行的的四则运算的结果。
因为"floatResult"变量是浮点运算类型,所以整型变量"x"和"y" 被强制转换成浮点类型来计算FloatResult。
这里有个求余数的运算符,两个操作数相除,返回余数。
最后一条语句给出了另外一种赋值形式,这里用了(+=)运算符.无论什么时候你使用(+=)运算符,那么这个二进制运算符就应该在运算符左右两边都进行运算,然后把值赋给左边的参数。本语句相当于"result = result + x",并返回同样的值。
前面的课程中,你看到的使用次数较多的一种类型是"string" (字符串)类型。"string"类型是由包含在引号内的Unicode编码的字符构成。例如"This is a string."
另外一种数据类型是数组。数组可以看成是同种类型的元素构成的集合。当声明数组时,你要指定类型名,数组名,维数和数组大小。
4.清单 1-4. Array Operations: Array.cs
using System;
class Array {
public static void Main() {
int[] myInts = { 5, 10, 15 };
bool[][] myBools = new bool[2][];
myBools[0] = new bool[2];
myBools[1] = new bool[1];
double[,] myDoubles = new double[2, 2];
string[] myStrings = new string[3];
Console.WriteLine("myInts[0]: {0}, myInts[1]: {1}, myInts[2]: {2}", myInts[0], myInts[1], myInts[2]);
myBools[0][0] = true;
myBools[0][1] = false;
myBools[1][0] = true;
Console.WriteLine("myBools[0][0]: {0}, myBools[1][0]: {1}", myBools[0][0], myBools[1][0]);
myDoubles[0, 0] = 3.147;
myDoubles[0, 1] = 7.157;
myDoubles[1, 1] = 2.117;
myDoubles[1, 0] = 56.00138917;
Console.WriteLine("myDoubles[0, 0]: {0}, myDoubles[1, 0]: {1}", myDoubles[0, 0], myDoubles[1, 0]);
myStrings[0] = "Joe";
myStrings[1] = "Matt";
myStrings[2] = "Robert";
Console.WriteLine("myStrings[0]: {0}, myStrings[1]: {1}, myStrings[2]: {2}", myStrings[0], myStrings[1], myStrings[2]);
}
}
说明
清单 1-4 演示了数组的各种不同实现方法。第一个例子是"myInts"数组,它在声明的同时进行了初始化。
接着是个二维数组,可以把它理解为数组的数组。我们需要使用"new"运算符来实例化初始数组的大小,之后,再对每个子数组使用new运算符。
第三个例子是个二维数组。数组可以是多维的,每一维可以通过逗号隔开,也必须用"new"运算符进行实例化。
最后定义了一个一维的字符串数组。
每种情况下,对于数据元素的访问可以通过引用元素的位置(下标)来进行。数组的大小可以是任何整型值。其下标从0开始。
小结
到现在为止,你已经了解了C# 的变量,简单数据类型,数组和字符串。我们还学习了如何用C#的运算符构成表达式。
第三课:选择控制语句
本节课将介绍如何使用C#选择控制语句,第三课将达到如下几个目的:
1.学会"if"语句的用法。
2.学会"switch"语句的用法。
3.学会在"switch"语句中如何使用"break"语句。
4.理解"goto"语句的正确用法。
在前面几节课中,你所看到的程序都是顺序执行的。你无法控制输入语句,你所能做的就是跟着程序执行直到终止。本节课中,将介绍基于条件进行判断,从而选择进入相应的逻辑分支中去执行。
我们所介绍的第一个选择语句是"if"语句,它有三种基本形式:单条选择, 如果/否则,以及多情形选择。
1.清单3-1. IF语句的格式:IfSelection.cs
using System;
class IfSelect {
public static void Main() {
string myInput;
int myInt;
Console.Write("Please enter a number: ");
myInput = Console.ReadLine();
myInt = Int32.Parse(myInput);
// Single Decision and Action with brackets
if (myInt > 0) {
Console.WriteLine("Your number {0} is greater than zero.", myInt);
}
// Single Decision and Action without brackets
if (myInt < 0)
Console.WriteLine("Your number {0} is less than zero.", myInt);
// Either/Or Decision
if (myInt != 0) {
Console.WriteLine("Your number {0} is not equal to zero.", myInt);
}
else {
Console.WriteLine("Your number {0} is equal to zero.", myInt);
}
// Multiple Case Decision
if (myInt < 0 || myInt == 0) {
Console.WriteLine("Your number {0} is less than or equal to zero.", myInt);
}
else if (myInt > 0 && myInt <= 10) {
Console.WriteLine("Your number {0} is between 1 and 10.", myInt);
}
else if (myInt > 10 && myInt <= 20) {
Console.WriteLine("Your number {0} is between 11 and 20.", myInt);
}
else if (myInt > 20 && myInt <= 30) {
Console.WriteLine("Your number {0} is between 21 and 30.", myInt);
}
else {
Console.WriteLine("Your number {0} is greater than 30.", myInt);
}
}
}
说明
1.清单3-1中的IF语句的各种格式都使用了同一个输入变量"myInt"。
这是从用户获得交互内容的另一种方式。我们首先输出一行信息:"Please enter a number:"到控制台。"Console.ReadLine()"语句使得程序等待来自用户的输入,一旦用户输入一个数字,按回车键之后,该数字以字符串的形式返回到"myInput"变量中,由于我们需要的是一个整数,所以需要转换变量"myInput"成整型数据。用命令"Int32.Parse(myInput)"即可完成。 (Int32 等数据类型将在后面的课程中加以介绍。) 转换结果放到"myInt"变量中,这是个整数类型。
2.有了我们所需要的类型的数据,就可以用"if"语句来进行条件判断了。
对于第一种形式的IF语句,格式为: if (boolean expression) { statements }。该语句必须以关键字"if"开始。之后,括号中为布尔表达式。该布尔表达式必须计算出一个true或者false值。在本例中,我们检查用户的输入,看看输入值是否大于0,如果表达式运算结果为true,就执行大括号中的语句。(我们把大括号之间的语句部分称为"block"。) 块中有一个或者多个语句。如果布尔表达式的值为false,我们就忽略块中的语句,直接执行块后面的语句。
3.除了没有块之外,第二种"if"语句的格式非常类似于第一种。
因此,如果布尔表达式为true,将会执行布尔表达式之后的第一条语句。当布尔表达式的值为false,布尔表达式之后的第一条语句将被忽略掉,而直接执行其后的程序语句。如果你只有一条语句需要执行,就用该格式的"if"语句。如果你打算在布尔表达式的值为true时,执行两条或者两条以上的语句,就必须把它们放到块中。我个人的建议是:无论需要执行几条语句,要养成把if语句放到块中的习惯, 这就使得你避免犯如下错误:当添加了一条语句之后,忘记了添加一对括号。
4.大多数时候,你需要作出如下选择:当条件满足时做一件事,否则做另外一件事。
清单3-1中,程序演示了这种if语句格式的用法。 当布尔表达式为true时, 就立刻执行"if"后面的那条语句, 而当布尔表达式为false时,就执行"else"关键字后面的语句。
5.当要计算多个布尔表达式时,你可以使用if/else if/else 这种格式,上面的例子程序演示了这种形式,从关键字"if"开始, 一旦布尔表达式为真,就执行if后面的块。但是,这一次,在组合关键字"else if"后面还可以进行多个条件的判断。"else if"语句后面也有个布尔表达式,一旦该布尔表达式的值为true,就会执行紧接其后的块。这种情形可以一直持续下去,直到所有的情况都已经计算出来,但是整个"if/else if"序列必须以"else"结束。当所有的"if"或者"else if" 后面的布尔表达式的值都为false时,就执行关键字"else"后面的块。 对于if/else if/else格式的语句,每次仅仅执行一个其中部分的语句 。
6.上面的例子中,布尔表达式 (myInt < 0 || myInt == 0)包含了条件OR (||)运算符。
对于常规OR (|)运算符和条件OR (||)运算符来说,只要有运算符两边的子表达式之一为真,整个布尔表达式的值就为真。两个运算符的区别在于:正规OR 运算符(|)每次都对运算符(|)两边的表达式进行计算。而条件运算符OR (||)只有当第一个子表达式的值为false时,才计算第二个子表达式的值。
7.布尔表达式 (myInt > 0 && myInt <= 10)包含了条件运算符AND。
对于常规AND (&) 运算符和条件AND (&&)运算符来说,只有当运算符两边的子表达式的值都为真时,整个布尔表达式的值为真。两种运算符的区别在于:正规AND (&)运算符每次都计算运算符两边的子表达式的值,但是对于条件AND运算符来说,只有当第一个子表达式的值为真时,才计算第二个表达式的值。条件运算符(&& 和 ||) 通常称为运算优化的运算符,因为有时不需要计算整个表达式的值。这样就可以忽略掉不必要的逻辑表达式的计算,可以生成有效的代码。
同if/else if/else 格式的"if"语句类似,"switch"语句的用法如下:
2.清单3-2. 分支选择语句: SwitchSelection.cs
using System;
class SwitchSelect {
public static void Main() {
string myInput;
int myInt;
begin:
Console.Write("Please enter a number between 1 and 3: ");
myInput = Console.ReadLine();
myInt = Int32.Parse(myInput);
// switch with integer type
switch (myInt) {
case 1:
Console.WriteLine("Your number is {0}.", myInt);
break;
case 2:
Console.WriteLine("Your number is {0}.", myInt);
break;
case 3:
Console.WriteLine("Your number is {0}.", myInt);
break;
default:
Console.WriteLine("Your number {0} is not between 1 and 3.", myInt);
}
decide:
Console.Write("Type \"continue\" to go on or \"quit\" to stop: ");
myInput = Console.ReadLine();
// switch with string type
switch (myInput) {
case "continue":
goto begin;
case "quit":
Console.WriteLine("Bye.");
break;
default:
Console.WriteLine("Your input {0} is incorrect.", myInput);
goto decide;
}
}
}
说明
1.清单3-2 演示了多分支选择语句switch的用法。
关键字"switch"后面是switch 表达式。Switch表达式必须是以下类型中的一种: sbyte,byte,short,ushort, int,uint,long,ulong,char,string,或者enum类型。(enum类型将在后面的课程中加以介绍)。在清单3-2的第一个"switch"语句中, switch 表达式计算的是整数类型的数据。
2.在switch表达式后面是个switch 块, 当Switch表达式的值同某一个case后面的常量表达式的值相匹配时,就执行此case后面的语句,直到遇到"break"语句或者"goto"语句为止。每个分支以关键字"case"作为标号,其后为常量表达式,然后是分号(:)。本例子程序中,我们有"case 1:","case 2:"和"case 3:"。
3.你可以在所有的分支选择的后面加上一个"default"分支。
如果没有匹配的常量表达式,就进入default分支,并执行该分支的语句。虽然default标号是可选的,我建议你加上该分支。这将有助于处理一些意外事件 ,使得你的程序能够捕捉和处理不可预见的事件,从而使得程序更加可靠。
4.每个"case"标号必须以"break"语句作为结束。
"break"语句将使得程序退出switch语句,并从switch块后面的一条语句开始执行。对于"default"标号来说,"break"语句是可选的,因为有"break"语句和没有"break"语句,其运行结果是一样的。如果你在switch块中放置了一个"goto"语句,情况就不一样了。
5.清单3-2中的第二个"switch"语句演示了"goto"语句的用法。
"goto"语句可以让程序跳到关键字"goto"后面的标号中去执行。程序执行过程中,如果用户打入"continue", switch语句就匹配case "continue" 中的常量表达式,并执行"goto begin:"语句。程序就会离开"switch"语句,开始执行标号"begin:"后的第一条语句。这是个有效的循环,可以让你多次重复执行同样一段代码。一旦用户打入字符串"quit",循环将会结束。此时,将进入case "quit" 分支。该分支将输出信息"Bye."到控制台上,之后跳出switch语句,再结束程序。
一旦输入的字符串既不是"continue"也不是"quit",就会进入"default:"分支。于是,将会输出一个出错信息到控制台上,之后执行"goto decide:"命令。这就使得程序转跳到"decide:" 标号后面的第一条语句, 该语句执行后,将会询问用户是否愿意continue(继续)还是 quit(退出)。这是个有效的循环。
显然,"goto"语句功能强大,在受控的场合下,"goto"语句是很有用途的。但是,必须指出的是,"goto"如果出现被滥用的趋势,程序的调试和维护将会变得很困难。想象一下,如果程序到处可见goto语句,其流程将会变得难以阅读和理解。下节课中,将介绍在程序中创建循环语句的更好方法。
小结
现在,你已经了解了如何使用"if"语句的各种格式,也了解了如何使用"switch"语句。你也了解了使用"break"语句可以从"switch"语句中推出。最后,你也了解了如何使用"goto"语句跳到程序的另外一个部分。
第四课:循环控制语句
本节课将介绍如何使用C#控制语句中的循环语句,本课目的如下:
1.学会"while"循环的用法。
2.学会"do" 循环的用法。
3.学会"for" 循环的用法。
4.学会foreach循环的用法。
5.进一步了解"break"语句的用法。
6.如何使用"continue"语句。
在C#中,使用"goto"语句并不是一个最佳的建立循环的方法。本节课将介绍建立循环的常用方法。
第一个要介绍的语句是while循环语句
1.清单 4-1. While循环:While loop.cs
using System;
class Whileloop {
public static void Main() {
int myInt = 0;
while (myInt < 10) {
Console.Write("{0} ", myInt);
myInt++;
}
Console.WriteLine();
}
}
说明
1.清单 4-1演示了一个简单的while 循环。
以关键字"while"开始,后面是个布尔表达式。所有的控制语句都使用了布尔表达式。这就意味着表达式必须计算出true 或者false值。本例中,我们将检查myInt变量,看看它是否小于10。 因为myInt已经初始化为0,第一次计算时,该布尔表达式将返回true值。一旦布尔表达式的值为true,将执行该布尔表达式后面块中的语句。
2.在while块中,我们把数字和空格输出到控制台中,之后,对 myInt进行加1运算。一旦执行了while块中的语句之后,再次计算布尔表达式的值,这种情况将一直循环下去,直到该布尔表达式的值为false为止。 一旦布尔表达式的值为false, 程序将从while 块之后的第一条语句开始执行。在本例中,我们把数字0到9输出到控制台中,之后退出while块,再输出一个新行到控制台。
同 "while"循环类似的是"do" 循环语句。
2.清单 4-2. Do 循环: Do loop.cs
using System;
class Doloop {
public static void Main() {
string myChoice;
do {
// Print A Menu
Console.WriteLine("My Address Book\n");
Console.WriteLine("A - Add New Address");
Console.WriteLine("D - Delete Address");
Console.WriteLine("M - Modify Address");
Console.WriteLine("V - View Addresses");
Console.WriteLine("Q - Quit\n");
Console.WriteLine("Choice (A,D,M,V,or Q): ");
// Retrieve the user's choice
myChoice = Console.ReadLine();
// Make a decision based on the user's choice
switch(myChoice) {
case "A":
case "a":
Console.WriteLine("You wish to add an address.");
break;
case "D":
case "d":
Console.WriteLine("You wish to delete an address.");
break;
case "M":
case "m":
Console.WriteLine("You wish to modify an address.");
break;
case "V":
case "v":
Console.WriteLine("You wish to view the address list.");
break;
case "Q":
case "q":
Console.WriteLine("Bye.");
break;
default:
Console.WriteLine("{0} is not a valid choice", myChoice);
}
// Pause to allow the user to see the results
Console.Write("Press any key to continue...");
Console.ReadLine();
Console.WriteLine();
} while (myChoice != "Q" && myChoice != "q"); // Keep going until the user wants to quit
}
}
说明
1.清单 4-2 演示了"do"循环的例子。 "do" 循环的格式是: do { <语句> } while (<布尔表达式>);其中的语句可以是任何合法的C#语句,布尔表达式同以前的规定一样,其返回值要么为true,要么为false。
2.如果你打算输出清单4-2 中的信息或者菜单,之后再读取用户的输入,那么就使用"do" 格式的循环而不要使用"while" 格式的循环语句。因为在本例中,布尔表达式的值是在循环结束处进行计算的, "do" 循环可以保证:循环体中的语句至少执行一次。与此相对应的是:"while" 循环一开始时,计算布尔表达式的值,"while" 循环不能保证循环体中的语句能够至少执行一次。
3.让我们来回顾一下清单 4-2中的内容。
在Main()方法中,我们定义了变量 "myChoice"为字符串类型。之后,输出一些信息到控制台,即输出一个可让用户进行选择的菜单。我们必须要得到用户的输入,即Console.ReadLine()方法所返回的值,该值存放在myChoice变量中。我们必须先得到用户的输入,之后再处理。要完成这件事,一个有效的方法是就使用 "switch"语句。注意到:为了获得同样的功能,我们既匹配了小写字母,也匹配了大写字母。另外一点是:我们使用了 "default:" case,这是个良好的编程作风。
3.清单 4-3. For 循环: For loop.cs
using System;
class Forloop {
public static void Main() {
for (int i=0; i < 20; i++) {
if (i == 10)
break;
if (i % 2 == 0)
continue;
Console.Write("{0} ", i);
}
Console.WriteLine();
}
}
说明
1.清单 4-3 演示了"for" 循环的用法。
当你能够精确地知道循环的次数时, For 循环语句就派上了用场。本程序同清单 4-1中"while" 循环程序的运行结果一样。 "for" 循环中,括号中内容由三个由分号隔开的部分: "(<初始化表>; <布尔表达式>; <post-loop 动作表>)"组成
2.初始化表是个由逗号隔开的表达式,在"for" 循环的整个周期中,这些表达式仅仅计算一次。计算是在一开始进行的,并且在循环语句的执行之前进行。正如在清单 4-3所见,通常初始化表是先初始化一个作为计数器的整型变量。
3.一旦进行初始化后, "for" 循环就进入第二部分:布尔表达式的计算。 这里的布尔表达式可以写得很复杂,但是结果只能是true或者false。布尔表达式通常用来验证计数器变量的状态。
4.一旦布尔表达式的值为true时,就会执行"for" 循环大括号中的语句。通常情况下,这些语句从左大括号开始,不中断地一直执行到右大括号。但在清单4-3中,有几个例外,几个"if"语句改变了程序的流程。
第一个"if"语句检查"i"是否等于10,这里使用了"break"语句,可在此处跳出循环,转入 "for"块后面的第一条语句的执行。
第二条"if"语句使用了求余运算符,看看"i"是否能够被2整除,如果余数等于0,就执行 "continue"语句。控制将跳过循环体中余下的语句,转入下一轮新的循环。你需要在块中正确地组织好语句,一旦满足有关条件,就执行相应的语句。
5.一旦程序流程遇到continue语句,或者遇到块的结尾处的右括号, 就转入"for" 循环括号中的第三项:post-loop 动作表,该表由逗号隔开,在"for"块中的语句都执行完毕之后,就执行该动作表中的动作。 清单 4-3中的 post-loop 动作表中的动作很典型:计数器加1。一旦动作表执行完毕,流程就转到对布尔表达式值进行判别。循环将继续进行下去,直到布尔表达式的值为true。当布尔表达式的值为false,控制流程就转到 "for"块之后的第一条语句。
4.清单 4-4. The ForEach 循环: ForEachloop.cs
using System;
class ForEachloop {
public static void Main() {
string[] names = {"Cheryl", "Joe", "Matt", "Robert"};
foreach (string person in names) {
Console.WriteLine("{0} ", person);
}
}
}
说明
1."foreach" 循环列举出集合中所有的元素。
清单 4-4所使用的数组类型,就是这样的一个集合。(在"System.Collections"也可以由其他的数据类型作集合。). 在Main()方法中,我们所做的第一件事是:定义由四个字符串组成的数组names。
2."foreach"括号中的表达式是由关键字in隔开的两个项组成。in右边的项是集合名,in左边的项是变量名,用来存放该集合中的每个元素。
该循环的运行过程如下:每一次循环时,从集合中取出一个新的元素值,放到只读变量中去,括号中的整个表达式返回值为true, "foreach"块中的语句就能够执行。一旦集合中的元素都已经被访问到,整个表达式的值为false,控制流程就转入到 "foreach" 块后面的第一条可执行语句。
3.在清单 4-4的例子中,我们使用了字符串变量person用来存放names数组的每个元素,并且使用Console.WriteLine()方法输出person 变量的值。
小结
到现在为止,你已经了解了"while","do","for"和"foreach" 循环语句的用法。最后你也学习了如何设置有关条件,来改变循环体块中的语句的流程。
第五课:方法
本节课向你介绍C#的方法,其目的是:
1.了解方法的结构格式
2.了解静态和实例方法之间的区别
3.学会实例对象的使用
4.学会如何调用实例化的对象
5.学会方法的四种参数类型的使用
6.学会使用"this"引用
以往,对于每个程序来说,所有的工作都在Main()方法中实现。这对于功能简单的程序是合适的,因为仅仅用来学习一些概念。有个更好的方法来组织你的程序,那就是使用方法。方法是很有用的,因为方法可以让你在不同的单元中分开设计你的逻辑模块。
方法的结构格式如下:
属性 修饰符 返回值类型 方法名(参数) { 语句 }
我们将在后面的课程中,讨论属性和修饰符。方法的返回值可以是任何一种C#的数据类型,该返回值可以赋给变量,以便在程序的后面部分使用。方法名是唯一,可以被程序调用。为使得你的代码变得更容易理解和记忆,方法的取名可以同所要进行的操作联系起来。你可以传递数据给方法,也可以从方法中返回数据。它们由大括号包围起来。大括号中的语句实现了方法的功能。
1.清单5-1. 一个简单的方法: OneMethod.cs
using System;
class OneMethod {
public static void Main() {
string myChoice;
OneMethod om = new OneMethod();
do {
myChoice = om.getChoice();
// Make a decision based on the user's choice
switch(myChoice) {
case "A":
case "a":
Console.WriteLine("You wish to add an address.");
break;
case "D":
case "d":
Console.WriteLine("You wish to delete an address.");
break;
case "M":
case "m":
Console.WriteLine("You wish to modify an address.");
break;
case "V":
case "v":
Console.WriteLine("You wish to view the address list.");
break;
case "Q":
case "q":
Console.WriteLine("Bye.");
break;
default:
Console.WriteLine("{0} is not a valid choice", myChoice);
}
// Pause to allow the user to see the results
Console.Write("Press any key to continue...");
Console.ReadLine();
Console.WriteLine();
} while (myChoice != "Q" && myChoice != "q"); // Keep going until the user wants to quit
}
string getChoice() {
string myChoice;
// Print A Menu
Console.WriteLine("My Address Book\n");
Console.WriteLine("A - Add New Address");
Console.WriteLine("D - Delete Address");
Console.WriteLine("M - Modify Address");
Console.WriteLine("V - View Addresses");
Console.WriteLine("Q - Quit\n");
Console.WriteLine("Choice (A,D,M,V,or Q): ");
// Retrieve the user's choice
myChoice = Console.ReadLine();
return myChoice;
}
}
说明
1.清单5-1中的程序类似于第四课中的DoLoop程序。
区别在于:前一课中的程序打印出菜单内容,并在Main()方法中接受用户的输入,而本课中,该功能用一个名为getChoice()的方法实现,该方法的返回值类型是个字符串类型。在main方法中,在switch语句中用到了该串。方法"getChoice"实现了调用时所完成的工作。方法名后面的括号内是空的,因为调用getChoice()方法时,不需要传递任何数据。
2.在方法块中,我们首先定义了变量"myChoice"。
虽然它与 Main()方法中的"myChoice"变量同名同类型, 但它们是不同的两个变量,因为局部变量仅仅在其定义的块内可见。换句话说, getChoice()方法中的"myChoice" 同Main()方法中的"myChoice"变量没有丝毫联系。getChoice()方法打印出一个菜单到控制台,并读取用户的输入。"return" 语句把"myChoice"变量值返回给Main()方法中的调用者getChoice()。注意: "return"语句返回类型同该方法中定义的返回值类型相同,本例中,该返回值是个字符串。
3.在Main()方法中,在使用getChoice()之前,实例化了一个新的"OneMethod"对象。
这是因为:我们没有指明一个"静态"修饰符。(注意:Main()函数带有"静态"修饰符),getChoice()就成为一个实例的方法。 实例方法和静态方法的区别是:前者可以创建多个类的实例,每个实例有自己的单独的getChoice()方法。而一旦方法是静态的,就不存在方法的实例,你只能调用该静态方法的一个实现。
所以,正如前面所讲的,因为getChoice()并不是静态的,所以,我们必须实例化一个新对象来使用它。这是通过定义"OneMethod om = new OneMethod()"来进行的。在等号的左边,是对象引用"om",其类型是OneMethod。"om"是个对象的引用,这点很重要,"om"并不是对象自身,它是个引用OneMethod类型对象的变量。 在等号的右边,把新创建的OneMethod对象赋给引用"om"。 关键字"new"是个在堆上创建对象的新实例的C#运算符。此处完成的工作是: 在堆上创建一个新的OneMethod实例,并把它赋给om引用。一旦有了om引用的OneMethod对象实例,就可以通过om引用来对实例进行处理。
方法,域和其他类成员可以通过"." (点)运算符进行访问,标识和操纵。一旦需要调用方法getChoice(),就通过om引用,并使用点运算符"om.getChoice()"来进行。 getChoice() 块中的语句执行完毕之后即返回。为了捕捉到getChoice()的返回值,我们使用了赋值运算符"="。 返回串放到了Main()函数的局部变量 myChoice中,从那里,程序的其余部分按照前面课程中介绍的方式正常执行。
2.清单5-2. 方法参数:MethodParams.cs
using System;
class Address {
public string name;
public string address;
}
class MethodParams {
public static void Main() {
string myChoice;
MethodParams mp = new MethodParams();
do {
// show menu and get input from user
myChoice = mp.getChoice();
// Make a decision based on the user's choice
mp.makeDecision(myChoice);
// Pause to allow the user to see the results
Console.Write("Press any key to continue...");
Console.ReadLine();
Console.WriteLine();
} while (myChoice != "Q" && myChoice != "q"); // Keep going until the user wants to quit
}
// show menu and get user's choice
string getChoice() {
string myChoice;
// Print A Menu
Console.WriteLine("My Address Book\n");
Console.WriteLine("A - Add New Address");
Console.WriteLine("D - Delete Address");
Console.WriteLine("M - Modify Address");
Console.WriteLine("V - View Addresses");
Console.WriteLine("Q - Quit\n");
Console.WriteLine("Choice (A,D,M,V,or Q): ");
// Retrieve the user's choice
myChoice = Console.ReadLine();
return myChoice;
}
// make decision
void makeDecision(string myChoice) {
Address addr = new Address();
switch(myChoice) {
case "A":
case "a":
addr.name = "Joe";
addr.address = "C# Station";
this.addAddress(ref addr);
break;
case "D":
case "d":
addr.name = "Robert";
this.deleteAddress(addr.name);
break;
case "M":
case "m":
addr.name = "Matt";
this.modifyAddress(out addr);
Console.WriteLine("Name is now {0}.", addr.name);
break;
case "V":
case "v":
this.viewAddresses("Cheryl", "Joe", "Matt", "Robert");
break;
case "Q":
case "q":
Console.WriteLine("Bye.");
break;
default:
Console.WriteLine("{0} is not a valid choice", myChoice);
}
}
// insert an address
void addAddress(ref Address addr) {
Console.WriteLine("Name: {0}, Address: {1} added.", addr.name, addr.address);
}
// remove an address
void deleteAddress(string name) {
Console.WriteLine("You wish to delete {0}'s address.", name);
}
// change an address
void modifyAddress(out Address addr) {
//Console.WriteLine("Name: {0}.", addr.name); // causes error!
addr = new Address();
addr.name = "Joe";
addr.address = "C# Station";
}
// show addresses
void viewAddresses(params string[] names) {
foreach (string name in names) {
Console.WriteLine("Name: {0}", name);
}
}
}
说明
1.清单5-2是清单5-1的修改,主要是对程序进行了模块化,并添加了更多的实现,以便阐述参数传递的用法。
C#可以处理四种类型的参数:out(输出),ref(引用),params(数组)和value(值)。为了说明参数的用法,我们用两个字符串域创建地址类。
2.在Main()方法中,我们调用getChoice()来读取用户的输入,并且把字符串放到myChoice变量中。
之后,把myChoice变量作为makeDecision()函数的实在参数。在实现makeDecision()方法时,注意其形式参数为字符串myChoice。需要再次说明的是:这是个新的myChoice变量,不同于调用者的实在参数,仅仅是适用于本方法的局部变量。 因为makeDecision()方法的myChoice参数没有任何其他修饰符,故认为它是"值"参,即实在参数的值被拷贝到栈中,故作为值参的变量是局部的,任何对局部变量值的改变并不影响到调用者的实在参数的值。换句话说,值参仅仅是来自调用者的输入。
3.makeDecision()方法中的switch语句完成如下功能:
在每种情形下,都调用相应的方法。这些方法的调用不同于Main()方法。除了使用"mp" 引用,它们还使用了"this"关键字。"this"是对当前对象的引用。由于makeDecision()方法不是静态方法,当前对象已经被实例化,所以可以使用"this"引用来调用同一实例中的方法。
4.addAddress()方法用到了"ref"参数,即引用可作为参数来传递,即该引用被拷贝到栈中,其引用的对象同调用者的实参所引用的对象是同一个。
这意味着:任何对局部引用的对象的改变也就是对调用者所引用的对象的改变。你可以想象一下,这就相当于输入/输出参数。
5.modifyAddress()中有一个输出参数。
输出参数仅仅传递给被调用函数。一旦调用该方法时,在栈中的仅有的一个引用未被赋值,因为根据赋值的确定性原则,在该变量没有被赋值之前,就不能使用该变量。modifyAddress()方法的第一行作为注释,说明了这一点。你可以试着去掉注释符,编译一下程序,看看结果如何。一旦该变量被赋了值,在程序返回之后,输出参数就被拷贝到调用者的参数中。所以,在方法返回之前,必须给输出参数赋值。
小结
C# 语言的一个很有用途的参数类型是数组参数,它须是一维或多维的数组。在makeDecision()方法中,我们传递了用四个逗号隔开的字符串参数。参数的个数是变量。在viewAddresses()方法中,使用了foreach循环,逐一输出这些字符串。数组参数仅是一种输入性质的参数,任何对数组参数值的改变仅仅影响到局部的副本值。
概括地讲,你现在已经理解了方法的组织结构。你还了解了方法的实现可以采用的四种参数类型及其格式。 一旦你使用实例方法,就必须实例化该对象,静态方法则不同,后者可以在任何时候被调用。另外,你也了解了"this"引用是如何调用实例方法的。
第六课:名称空间
本节课将介绍C#的名称空间。其目的是:
1.了解什么是名称空间。
2.了解如何实现"using"指示符。
3.了解"alias" 指示符的用法。
4.了解名称空间的成员的内容。
在第一课中,你已经在简单的hello程序中看到了"using System;"指示符的使用。该指示符可以让你使用System名称空间中的成员。在第一课中,未及对此作出详细介绍,现在我们来解释一下名称空间的具体用法。一旦学完了本节课,你将了解"using"指示符及其相关内容。
作为C#的元素,名称空间可以用来帮助组织程序的结构,可以避免两套代码集中命名的冲突。在程序代码中,使用名称空间是个良好的编程习惯,因为这有助于重用你的程序代码。
1.清单6-1. The C# Station Namespace: NamespaceCSS.cs
// Namespace Declaration
using System;
// The C# Station Namespace
namespace csharp_station {
// Program start class
class NamespaceCSS {
// Main begins program execution.
public static void Main() {
// Write to console
Console.WriteLine("This is the new C# Station Namespace.");
}
}
}
说明
清单6-1演示了如何创建一个名称空间。把单词"namespace"放在"csharp_station"之前,就创建了一个名称空间。"csharp_station"名称空间内的大括号中包含了成员。
2.清单6-2 Nested Namespace 1: NestedNamespace1.cs
// Namespace Declaration
using System;
// The C# Station Tutorial Namespace
namespace csharp_station {
namespace tutorial {
// Program start class
class NamespaceCSS {
// Main begins program execution.
public static void Main() {
// Write to console
Console.WriteLine("This is the new C#
Station Tutorial Namespace.");
}
}
}
}
说明
名称空间可以建立一个代码的组织结构。一个良好的编程习惯是:用层次模式来组织你的名称空间。你可以把通用一些的名称放在最顶层,里层则放置一些专用一些的名称。这个层次系统可以用嵌套的名称空间表示。清单6-2演示了如何建立一个嵌套的名称空间。在不同的子名称空间内放置代码,从而组织好你的代码的结构。
3.清单6-3. Nested Namespace 2: NestedNamespace2.cs
// Namespace Declaration
using System;
// The C# Station Tutorial Namespace
namespace csharp_station.tutorial {
// Program start class
class NamespaceCSS {
// Main begins program execution.
public static void Main() {
// Write to console
Console.WriteLine("This is the new C# Station Tutorial Namespace.");
}
}
}
说明
清单6-3演示了另外一种编写嵌套的名称空间的方法。在"csharp_station"和"tutorial"之间置入点运算符,表明这是嵌套的名称空间。结果同清单6-2。 相比而言,清单6-3 更易书写。
4.清单6-4. Calling Namespace Members: NamespaceCall.cs
// Namespace Declaration
using System;
namespace csharp_station {
// nested namespace
namespace tutorial {
class myExample1 {
public static void myPrint1() {
Console.WriteLine("First Example of calling another namespace member.");
}
}
}
// Program start class
class NamespaceCalling {
// Main begins program execution.
public static void Main() {
// Write to console
tutorial.myExample1.myPrint1();
csharp_station.tutorial.myExample2.myPrint2();
}
}
}
// same namespace as nested namespace above
namespace csharp_station.tutorial {
class myExample2 {
public static void myPrint2() {
Console.WriteLine("Second Example of calling another namespace member.");
}
}
}
说明
1.清单6-4 的例子演示了用完整的名称指示,调用名称空间的成员。
一个完整的名称指示包括名称空间名,以及调用的方法名。程序的上半部分,在"csharp-station"名称空间内嵌套的名称空间"tutorial"中,定义了类"myExample1"及其方法"myPrint1"。 Main()方法中用完整的名称指示:"tutorial.myExample1.myPrint()" 来进行调用。 因为Main()方法和tutorial名称空间位于同一名称空间内,如果使用"csharp_station"的全称不是必需的。
2.清单6-4的下半部分,也是名称空间"csharp_station.tutorial"的一部分。
类"myExample1"和"myExample2"都属于该名称空间。另外,也可以把它们分别写入不同的文件,同时它们仍然属于同一名称空间。在Main()方法中,调用"myPrint2"方法时,采用了全称:"csharp_station.tutorial.myExample2.myPrint2()"。 在这里,必须使用全称中"csharp_station",因为"myExample2"定义在外部。
3.注意:这里对两个不同的类起了不同的名字:
"myExample1"和"myExample2"这是因为对于每个名称空间来说,其中的成员必须有唯一的名称。 记住:它们都处于同一名称空间中,不能取名相同。方法"myPrint1"和"myPrint2" 名称的不同仅仅是为了方便起见,即使同名也没有问题,因为它们属于不同的类。
5.清单6-5. The using Directive: UsingDirective.cs
// Namespace Declaration
using System;
using csharp_station.tutorial;
// Program start class
class
UsingDirective {
// Main begins program execution.
public static void Main() {
// Call namespace member
myExample.myPrint();
}
}
// C# Station Tutorial Namespace
namespace csharp_station.tutorial {
class myExample {
public static void myPrint() {
Console.WriteLine("Example of using a using directive.");
}
}
说明
调用方法时,如果你不想打入全称,可使用"using"指示符。在清单6-5中,有两个"using"指示符。第一个指示符是"using System",同本教程其它地方出现的"using"指示符相同。你不需要每次都打上"System",只需要打入该名称空间的成员方法名即可。在myPrint()中,"Console"是个"System"名称空间中的成员类,该类有个"WriteLine"的方法。该方法的全称是: "System.Console.WriteLine(...)"。
类似地,using指示符"using csharp_station.tutorial"可以让我们在使用 "csharp_station.tutorial" 名称空间的成员时,无需打入全称。所以,我们可以打入"myExample.myPrint()"。如果不使用"using"指示符,每次实现该方法时,我们就得打入"csharp_station.tutorial.myExample.myPrint()" 。
6.清单6-6. The Alias Directive: AliasDirective.cs
// Namespace Declaration
using System;
using csTut = csharp_station.tutorial.myExample; // alias
// Program start class
class AliasDirective {
// Main begins program execution.
public static void Main() {
// Call namespace member
csTut.myPrint();
myPrint();
}
// Potentially ambiguous method.
static void myPrint() {
Console.WriteLine("Not a member of
csharp_station.tutorial.myExample.");
}
}
// C# Station Tutorial Namespace
namespace csharp_station.tutorial {
class myExample {
public static void myPrint() {
Console.WriteLine("This is a member of csharp_station.tutorial.myExample.");
}
}
}
说明
1.有时,往往遇到取名较长的名称空间,而你可以把该名称变短些。
这样就增强了可读性,还避免了同名的冲突。清单6-6 演示了如何使用别名指示符,创建别名的格式例子是:"using csTut = csharp_station.tutorial.myExample"。表达式"csTut"可以取代"csharp_station.tutorial.myExample",用在本文件的任何地方。在Main()方法中就使用了"csTut"。
2.在Main()方法中,调用了"AliasDirective" 类中"myPrint" 方法。
这与"myExample" 类的"myPrint"方法同名。 虽然同名,这两个方法都各自正确地进行了调用,原因是:"myExample"类的"myPrint"方法用别名"csTut"表示。编译器能够准确地了解所要执行的是哪个方法。一旦漏掉了"csTut",编译器将两次调用"AliasDirective"类的"myPrint"方法。
3.另外一方面,如果我们没有创建别名指示符,而是添加了"using csharp_station.tutorial.myExample"之后,再调用myPrint(),编译器就会生成出错信息,因为它不知道究竟是调用. "csharp_station.tutorial.myExample.myPrint()"方法呢?还是去调用"AliasDirective.myPrint()"方法。所以使用名称空间是良好的编程习惯,可避免代码中的冲突现象。
小结
到现在为止,我们已经了解在名称空间中可以使用类,实际上,名称空间可以使用如下类型的数据:
类;结构;接口;枚举;代理
在后面的课程中我们将详细介绍这些数据类型。
概括来讲,你已经了解了什么是名称空间,如何定义自己的名称空间。如果你不想打入全称,可以使用"using"指示符。一旦你想缩短名称空间的长名,可以使用别名指示符。另外,除了类之外,你也了解了名称空间可以使用的其他一些数据类型。
第七课:类的入门
本节课将介绍C#中的类,其目的如下:
1.了解构造函数的实现
2.了解实例和静态成员的区别
3.了解析构函数的使用
4.熟悉类的成员类型
在本教程的一开始,你就已经接触到类的用法了。现在,既然已经对类有了感性认识,并知道如何定义一个类,本节课将
定义类的格式是:关键字"Class"后面跟着类名,类名之后的大括号中包含的是类的成员。构造函数没有返回值,且与类同名。清单7-1是一个关于类的例子。
清单 7-1. Example C# Classes: Classes.cs
// Namespace Declaration
using System;
// helper class
class OutputClass {
string myString;
// Constructor
public OutputClass(string inputString) {
myString = inputString;
}
// Instance Method
public void printString() {
Console.WriteLine("{0}", myString);
}
// Destructor
~OutputClass() {
// Some resource cleanup routines
}
}
// Program start class
class ExampleClass {
// Main begins program execution.
public static void Main() {
// Instance of OutputClass
OutputClass outCl = new OutputClass("This is printed by the output class.") { }
// Call Output class' method
outCl.printString();
}
}
说明
1.清单7-1 演示了两个类。
第一个类"OutputClass"包括:一个构造函数,一个实例方法,以及一个析构函数,还包括一个域名"myString"。构造函数是用来初始化类的数据成员的。本例中,OutputClass类的构造函数接收一个字符串参数,并把它拷贝到该类的myString域中。
2.构造函数并不是一定要用户自己定义的,例如ExampleClass类就用到了缺省的构造函数。
缺省的构造函数很简单,且不带参数。但这样的构造函数并没有多大用处。为了使得缺省的构造函数能够有些用途,用户自己可以对它初始化。例如:
public OutputClass() : this("Default Constructor String") { }
这种形式的构造函数可以包含在清单7-1中的OutputClass类中。该缺省的构造函数后跟着初始化符。分号":"标志着初始化开始,后面的"this"是关键字,表示OutputClass这个类所对应的对象。this可以有效地调用该对象中的构造函数。"this"关键字后面的参数为字符串类型。上面的初始化工作调用了OutputClass构造函数,该构造函数接收一个字符串类型的数据作为参数。初始化工作确保在类的域能够在类的实例化过程中被初始化。
上面的例子表明:一个类可以有多种格式的构造函数。具体调用哪一种构造函数取决于参数个数及参数类型。
类成员的类型
在C#中,有两种类型的类成员,实例和静态的。每次定义类的对象时,就创建了该类的一个实例。ExampleClass中的 Main() 方法创建了OutputClass类的一个名为"outCl"的实例。你可以用不同的名字创建OutputClass类的多个实例。这些实例都是各自独立的。例如,OutputClass类的两个实例创建如下:
OutputClass oc1 = new OutputClass("OutputClass1");
OutputClass oc2 = new OutputClass("OutputClass2");
于是,创建了OutputClass类的两个单独的实例,且各自带有单独的"myString"域和"printString()"方法。上例中,两个实例名为"oc1" 和"oc2"。 另外一方面,如果类成员是静态的,可以通过如下格式来访问: <classname>.<static class member>。
一旦OutputClass类有如下的静态方法:
static void staticPrinter() {
Console.WriteLine("There is only one of me.");
}
你就可以用下面的方式,从Main()中调用该函数:
OutputClass.staticPrinter();
注意:
调用类的静态成员必须通过类名而不是实例名。类的静态成员的副本仅有一个。
另外一种类型的构造函数是静态构造函数。 通过在构造函数名称的前面使用关键字"static",就可以定义一个静态的构造函数。 调用静态的构造函数的发生时间是:在创建类的实例之前 ,在调用类的静态成员之前,在调用派生类的静态构造函数之前。(在后续课程中将介绍),且仅被调用一次。
OutputClass也有一个析构函数,除了前面加上了"~"符号,就跟构造函数的格式一样。析构函数用于释放类所占用的资源。当C#垃圾搜集器决定把对象从内存中清除出去时,就会调用析构函数。
小结
现在,你已经了解了类的如下成员:域,方法,构造函数,析构函数。下面是类的完整的成员类型:
构造函数;析构函数;域;方法;属性;索引;代理 ;事件;嵌套类
上面没有介绍过的类型将在后续课程中讲解。
概括地讲,你现在已经学会了如何定义常规的和静态的构造函数,也了解了如何初始化类的域。如果没有必要实例化一个对象,可以创建静态的类成员。你也了解了用来释放资源的析构函数的用法。
第八课:类的继承
本节课将介绍C#中的继承,其目的如下:
1.基类的实现
2.类的继承
3.在派生类中初始化基类
4.如何调用基类成员
5.如何覆盖基类成员
继承是面向对象程序设计的主要特征之一,它可以让你重用代码,可以节省程序设计的时间。
1.清单8-1 继承: BaseClass.cs
using System;
public class ParentClass
{
public ParentClass()
{
Console.WriteLine("Parent Constructor.");
}
public void print()
{
Console.WriteLine("I'm a Parent Class.");
}
}
public class ChildClass : ParentClass
{
public ChildClass()
{
Console.WriteLine("Child Constructor.");
}
public static void Main()
{
ChildClass child = new ChildClass();
child.print();
}
}
Output:
Parent Constructor.
Child Constructor.
I'm a Parent Class.
说明
清单8-1演示了两个类的用法。上面的一个类名为ParentClass, main函数中用到的类名为ChildClass。要做的是创建一个使用父类ParentClass现有代码的子类ChildClass。
1.首先必须说明ParentClass是ChildClass的基类。
这是通过在ChildClass类中作出如下说明来完成的:"public class ChildClass : ParentClass"。在派生类标识符后面,用分号":" 来表明后面的标识符是基类。C#仅支持单一继承。因此,你只能指定一个基类。
2.ChildClass的功能几乎等同于ParentClass。
因此,也可以说ChildClass "就是" ParentClass。在ChildClass 的Main()方法中,调用print() 方法的结果,就验证这一点。该子类并没有自己的print()方法,它使用了ParentClass中的 print()方法。在输出结果中的第三行可以得到验证。
3.基类在派生类初始化之前自动进行初始化。
注意到清单8-1的输出结果。ParentClass 类的构造函数在ChildClass的构造函数之前执行。
2.清单 8-2. 派生类同基类进行通信: BaseTalk.cs
using System;
public class Parent
{
string parentString;
public Parent()
{
Console.WriteLine("Parent Constructor.");
}
public Parent(string myString)
{
parentString = myString;
Console.WriteLine(parentString);
}
public void print()
{
Console.WriteLine("I'm a Parent Class.");
}
}
public class Child : Parent
{
public Child() : base("From Derived")
{
Console.WriteLine("Child Constructor.");
}
public void print()
{
base.print();
Console.WriteLine("I'm a Child Class.");
}
public static void Main()
{
Child child = new Child();
child.print();
((Parent)child).print();
}
}
Output:
From Derived
Child Constructor.
I'm a Parent Class.
I'm a Child Class.
I'm a Parent Class.
说明
1.派生类在初始化的过程中可以同基类进行通信。
清单8-2演示了在子类的构造函数定义中是如何实现同基类通信的。分号":"和关键字base用来调用带有相应参数的基类的构造函数。输出结果中,第一行表明:基类的构造函数最先被调用,其实在参数是字符串"From Derived"。
2.有时,对于基类已有定义的方法,打算重新定义自己的实现。
Child类可以自己重新定义print()方法的实现。Child的print()方法覆盖了Parent中的 print 方法。结果是:除非经过特别指明,Parent类中的print方法不会被调用。
3.在Child类的 print() 方法中,我们特别指明:调用的是Parent类中的 print() 方法。
方法名前面为"base",一旦使用"base"关键字之后,你就可以访问基类的具有公有或者保护权限的成员。 Child类中的print()方法的执行结果出现上面的第三行和第四行。
4.访问基类成员的另外一种方法是:通过显式类型转换。
在Child类的Main()方法中的最后一条语句就是这么做的。记住:派生类是其基类的特例。这个事实告诉我们:可以在派生类中进行数据类型的转换,使其成为基类的一个实例。清单8-2的最后一行实际上执行了Parent类中的 print()方法。
小结
你已经了解了如何创建一个派生类及其基类。你可以对基类成员进行初始化,以及如何对方法进行隐式或者显式的调用。你也了解了派生类是其基类的一个特例。
第九课:多态性
本节课将介绍C#的多态性,其目的包括:
1.了解什么是多态性
2.如何定义一个虚方法
3.如何重载一个虚方法
4.如何在程序中运用多态性
面向对象程序设计中的另外一个重要概念是多态性。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。 可以把一组对象放到一个数组中,然后调用它们的方法,在这种场合下,多态性作用就体现出来了,这些对象不必是相同类型的对象。当然,如果它们都继承自某个类,你可以把这些派生类,都放到一个数组中。 如果这些对象都有同名方法,就可以调用每个对象的同名方法。本节课将向你介绍如何完成这些事情。
1.清单9-1. 带有虚方法的基类:DrawingObject.cs
using System;
public class DrawingObject
{
public virtual void Draw()
{
Console.WriteLine("I'm just a generic drawing object.");
}
}
说明
清单9-1 定义了DrawingObject类。这是个可以让其他对象继承的基类。该类有一个名为Draw()的方法。Draw()方法带有一个virtual修饰符,该修饰符表明:该基类的派生类可以重载该方法。DrawingObject类的 Draw()方法完成如下事情:输出语句"I'm just a generic drawing object."到控制台。
2.清单9-2. 带有重载方法的派生类:Line.cs, Circle.cs, and Square.cs
using System;
public class Line : DrawingObject
{
public override void Draw()
{
Console.WriteLine("I'm a Line.");
}
}
public class Circle : DrawingObject
{
public override void Draw()
{
Console.WriteLine("I'm a Circle.");
}
}
public class Square : DrawingObject
{
public override void Draw()
{
Console.WriteLine("I'm a Square.");
}
}
说明
清单9-2定义了三个类。这三个类都派生自DrawingObject类。每个类都有一个同名Draw()方法,这些Draw()方法中的每一个都有一个重载修饰符。重载修饰符可让该方法在运行时重载其基类的虚方法,实现这个功能的条件是:通过基类类型的指针变量来引用该类。
3.清单9-3. 实现多态性的程序:DrawDemo.cs
using System;
public class DrawDemo
{
public static int Main(string[] args)
{
DrawingObject[] dObj = new DrawingObject[4];
dObj[0] = new Line();
dObj[1] = new Circle();
dObj[2] = new Square();
dObj[3] = new DrawingObject();
foreach (DrawingObject drawObj in dObj)
{
drawObj.Draw();
}
return 0;
}
}
说明
清单9-3演示了多态性的实现,该程序使用了在清单 9-1 和清单9-2中定义的类。在DrawDemo类中的Main()方法中,创建了一个数组, 数组元素是DrawingObject 类的对象。该数组名为dObj,是由四个DrawingObject类型的对象组成。
接下来, 初始化dObj数组, 由于Line, Circle和Square类都是DrawingObject类的派生类,所以这些类可以作为dObj数组元素的类型。 如果C#没有这种功能,你得为每个类创建一个数组。继承的性质可以让派生对象当作基类成员一样用,这样就节省了编程工作量。
一旦数组初始化之后,接着是执行foreach循环,寻找数组中的每个元素。在每次循环中, dObj 数组的每个元素(对象)调用其Draw()方法。多态性体现在:在运行时,各自调用每个对象的Draw()方法。尽管dObj 数组中的引用对象类型是DrawingObject,这并不影响派生类重载DrawingObject 类的虚方法Draw()。 在dObj 数组中,通过指向DrawingObject 基类的指针来调用派生类中的重载的Draw()方法。
输出结果是:
I'm a Line.
I'm a Circle.
I'm a Square.
I'm just a generic drawing object.
在DrawDemo 程序中,调用了每个派生类的重载的Draw()方法。 最后一行中,执行的是DrawingObject类的虚方法Draw()。这是因为运行到最后,数组的第四个元素是DrawingObject类的对象。
小结
现在对多态性有所了解之后,你可以在派生类中,实现一个重载基类虚方法的方法。虚方法和重载的派生类方法之间的关系就体现出C#的多态性。
第十课:属性
本节课将介绍C#的属性,其目的包括:
1.理解什么是属性
2.如何实现属性
3.创建一个只读属性
4.创建一个只写属性
属性是C#中独具特色的新功能。通过属性来读写类中的域,这具有一定的保护功能。在其它语言中,这是通过实现特定的getter和setter方法来实现的。C#的属性具有保护功能,可以让你就象访问域一样访问属性。要了解属性的用法,我们先来看看如何用传统的方法对域进行封装。
1.清单 10-1. 传统的访问类的域的例子:Accessors.cs
using System;
public class PropertyHolder
{
private int someProperty = 0;
public int getSomeProperty()
{
return someProperty;
}
public void setSomeProperty(int propValue)
{
someProperty = propValue;
}
}
public class PropertyTester
{
public static int Main(string[] args)
{
PropertyHolder propHold = new PropertyHolder();
propHold.setSomeProperty(5);
Console.WriteLine("Property Value: {0}", propHold.getSomeProperty());
return 0;
}
}
说明
1.清单 10-1 演示了用传统方法访问类的域的例子。
PropertyHolder类有个我们感兴趣的域someProperty, PropertyHolder类带有两个方法:getSomeProperty和setSomeProperty。getSomeProperty方法返回someProperty域的值。SetSomeProperty方法设置域someProperty的值。
2.类PropertyTester使用类PropertyHolder中的方法来获取someProperty域的值。
Main方法中新创建了一个PropertyHolder对象,之后通过使用setSomeProperty方法,调用propHold对象的setSomeProperty方法,设置其值为5。之后,调用Console.WriteLine方法输出属性值。对propHold对象的getSomeProperty的调用,是用来获取属性值的。它输出"Property Value: 5"到控制台。
3.这种传统的访问域的信息的方法是很好的,因为它支持面向对象的封装的概念。
如果在对域someProperty的实现中,域的类型从int 类型变为byte类型,上述的方法仍然适用。现在,如果采用属性的话,其实现会做得更为平滑。
2.清单 10-2. 使用属性访问类的域:Properties.cs
using System;
public class PropertyHolder
{
private int someProperty = 0;
public int SomeProperty
{
get
{
return someProperty;
}
set
{
someProperty = value;
}
}
}
public class PropertyTester
{
public static int Main(string[] args)
{
PropertyHolder propHold = new PropertyHolder();
propHold.SomeProperty = 5;
Console.WriteLine("Property Value: {0}", propHold.SomeProperty);
return 0;
}
}
说明
1.清单 10-2 演示了如何创建和使用属性。
PropertyHolder类中有个"SomeProperty" 属性的实现。注意:属性名的首字母必须大写,这是属性名"SomeProperty"和域名"someProperty"的唯一区别。属性有两种访问操作:get和set。Get访问操作返回的是someProperty域的值。Set访问操作是设置someProperty域的值,其值为"value"的内容。Set访问符号后面的"value"是C#中的保留字。通常,在其他场合下使用"value"关键字会出错。
2.PropertyTester 类使用PropertyHolder类中的SomeProperty属性。
在Main方法的第一行中, 创建了PropertyHolder对象propHold。之后,把propHold对象的 someProperty 域的值设置为5,很简单,就象对域赋值一样,给属性赋值。
3.Console.WriteLine方法输出 propHold对象的someProperty域的值。
这是通过使用propHold对象的SomeProperty属性来完成的。很简单,就象对域赋值一样,赋值给属性。属性可以设置为只读的,这可以通过在属性的实现中只设一个get访问符号来实现。
3.清单 10-3. 只读属性: ReadOnlyProperty.cs
using System;
public class PropertyHolder
{
private int someProperty = 0;
public PropertyHolder(int propVal)
{
someProperty = propVal;
}
public int SomeProperty
{
get
{
return someProperty;
}
}
}
public class PropertyTester
{
public static int Main(string[] args)
{
PropertyHolder propHold = new PropertyHolder(5);
Console.WriteLine("Property Value: {0}", propHold.SomeProperty);
return 0;
}
}
说明
1.清单10-3 演示了如何实现只读属性。
PropertyHolder类中,SomeProperty 属性只有一个get访问操作,没有用到set访问操作。PropertyHolder类中还有个接受整型参数的构造函数。
2.在PropertyTester类的Main方法中,创建了新名为propHold的PropertyHolder类的对象。
propHold对象在实例化时,调用了带参数的PropertyHolder构造函数。在本例中,参数值为5,这对propHold 对象的someProperty域的值进行了初始化。
3.因为PropertyHolder 类的SomeProperty属性是只读的,所以没有其他的方法来设置someProperty域的值。
如果你插入了"propHold.SomeProperty = 7"语句到程序清单中,该程序编译将不会通过,因为SomeProperty是只读属性。在Console.WriteLine 方法中使用SomeProperty属性时,程序执行正常。这是因为该方法调用了SomeProperty属性的get访问操作,这是个只读操作。
4.清单 10-4. 只写属性: WriteOnlyProperty.cs
using System;
public class PropertyHolder
{
private int someProperty = 0;
public int SomeProperty
{
set
{
someProperty = value;
Console.WriteLine("someProperty is equal to {0}", someProperty);
}
}
}
public class PropertyTester
{
public static int Main(string[] args)
{
PropertyHolder propHold = new PropertyHolder();
propHold.SomeProperty = 5;
return 0;
}
}
说明
1.清单 10-4 演示了如何创建和使用只写属性。
这一次,在PropertyHolder类中的SomeProperty属性中,去掉了get访问操作,而加上了set访问操作。其功能是输出someProperty域的值。
2.在PropertyTester 类中的Main方法中,用缺省的构造函数对PropertyTester类进行初始化。
之后,使用propHold 对象的SomeProperty属性,设置该域的值为5。这就调用了propHold 对象的set访问操作, 把someProperty 域的值设置为5,最后,把"someProperty is equal to 5"的信息输出到控制台。
小结
现在,你已经了解了什么是属性,以及属性的使用方法,你也了解了使用属性和使用传统的类的方法之间的区别。属性可以是只读的,也可以是只写的,每种场合下的使用方法,你都有所了解。
第十一课:索引指示器
本节课将介绍C#的索引指示器,其目的包括:
1.了解什么是索引指示器
2.如何实现索引指示器
3.重载索引指示器
4.了解如何实现多参数的索引指示器
索引指示器并不难使用。它们的用法跟数组相同。在一个类内部,你可以按照你的意愿来管理一组数据的集合。这些对象可以是类成员的有限集合,也可以是另外一个数组,或者是一些复杂的数据结构。不考虑类的内部实现,其数据可以通过使用索引指示器来获得。如下是一个例子:
1.清单 11-1. 索引指示器的例子:IntIndexer.cs
using System;
///
/// A simple indexer example.
///
class IntIndexer
{
private string[] myData;
public IntIndexer(int size)
{
myData = new string[size];
for (int i=0; i < size; i++)
{
myData = "empty";
}
}
public string this[int pos]
{
get
{
return myData[pos];
}
set
{
myData[pos] = value;
}
}
static void Main(string[] args)
{
int size = 10;
IntIndexer myInd = new IntIndexer(size);
myInd[9] = "Some Value";
myInd[3] = "Another Value";
myInd[5] = "Any Value";
Console.WriteLine("\nIndexer Output\n");
for (int i=0; i < size; i++)
{
Console.WriteLine("myInd[{0}]: {1}", i, myInd);
}
}
}
说明
1.清单 11-1演示了如何实现一个索引指示器, IntIndexer类有个名为myData的字符串数组,该数组是私有成员,因而其外部成员是看不见的。该数组是在构造函数中进行初始化的,该构造函数带有一个整型size参数,用来初始化myData数组,初始化时 把单词"empty"作为每个数组元素的值。
2.IntIndexer类的下一成员是索引指示器(Indexer),由关键字this和方括号[int pos]标识出来。该成员带有一个位置参数pos。正如你已经猜测到,Indexer的实现同属性一样。Indexer有get 和set访问操作,就同属性中的用法一样。索引指示器(indexer)返回一个字符串,在定义索引指示器时,string这个类型名标志着其返回类型为字符串类型。
3.Main()方法完成如下事情:初始化一个新的IntIndexer对象,添加一些值,并且打印出结果。其输出结果如下:
Indexer Output
myInd[0]: empty
myInd[1]: empty
myInd[2]: empty
myInd[3]: Another Value
myInd[4]: empty
myInd[5]: Any Value
myInd[6]: empty
myInd[7]: empty
myInd[8]: empty
myInd[9]: Some Value
4.在不少程序语言中,通常都是使用整数作为下标来访问作为数组元素的,但C#的索引指示器不仅能够做到这一点,而且还能够更进一步。 定义索引指示器时,可以带有多个参数,每个参数的类型可以不同。添加的参数由逗号隔开,同方法中的的参数表一样。索引指示器的合法的参数类型包括:整型,枚举类型和字符串。另外,索引指示器也可以被重载。在清单 11-2中,我们修改了前面的程序,以便用来重载索引指示器 ,从而可以接受不同类型的参数。
2.清单 11-2. 重载的索引指示器: OvrIndexer.cs
using System;
///
/// Implements overloaded indexers.
///
class OvrIndexer
{
private string[] myData;
private int arrSize;
public OvrIndexer(int size)
{
arrSize = size;
myData = new string[size];
for (int i=0; i < size; i++)
{
myData = "empty";
}
}
public string this[int pos]
{
get
{
return myData[pos];
}
set
{
myData[pos] = value;
}
}
public string this[string data]
{
get
{
int count = 0;
for (int i=0; i < arrSize; i++)
{
if (myData == data)
{
count++;
}
}
return count.ToString();
}
set
{
for (int i=0; i < arrSize; i++)
{
if (myData == data)
{
myData = value;
}
}
}
}
static void Main(string[] args)
{
int size = 10;
OvrIndexer myInd = new OvrIndexer(size);
myInd[9] = "Some Value";
myInd[3] = "Another Value";
myInd[5] = "Any Value";
myInd["empty"] = "no value";
Console.WriteLine("\nIndexer Output\n");
for (int i=0; i < size; i++)
{
Console.WriteLine("myInd[{0}]: {1}", i, myInd);
}
Console.WriteLine("\nNumber of \"no value\" entries: {0}", myInd["no value"]);
}
}
说明
1.清单 11-2 演示了如何重载索引指示器。
带有整型参数pos的第一个索引指示器同清单11-1中的一样,但是,该程序中有个带有字符串参数的新的索引指示器。对于这个新的索引指示器来说,其get操作返回的是同参数值data相匹配的成员的个数。 Set操作把数组中同参数值匹配的元素值该变为value值。
2.在清单11-2的Main()方法中,演示了重载的索引指示器,它接受字符串参数。
该重载的索引指示器调用了set操作,通过使用下列命令: myInd["empty"] = "no value"; set操作把"no value"值赋给myInd 类中所有的值为"empty"的成员。 myInd类的每个成员都已经输出之后,就把最后一个数据输出到控制台,该数据统计数组成员值为"no value"的个数。 使用如下命令:myInd["no value"],就可调用get操作。输出结果如下:
Indexer Output
myInd[0]: no value
myInd[1]: no value
myInd[2]: no value
myInd[3]: Another Value
myInd[4]: no value
myInd[5]: Any Value
myInd[6]: no value
myInd[7]: no value
myInd[8]: no value
myInd[9]: Some Value
Number of "no value" entries: 7
3.在清单 11-2中,两个索引指示器共处在同一个类中, 这是可以的,因为它们有不同的特征。
一个索引指示器的特征是通过索引指示器参数表中的参数个数和类型表现出来的。类能够辨别出其特征,并调用相应的索引指示器。带有多个参数的索引指示器可以用如下格式来实现:
public object this[int param1, ..., int paramN]
{
get
{
// process and return some class data
}
set
{
// process and assign some class data
}
}
小结
现在你已经了解了索引指示器是用来做什么的,以及其用法。如同数组的用法一样,你可以创建索引指示器来访问类的成员。本文也提到了索引指示器的重载和多参数索引指示器。
文章来源:http://www.codeguru.cn/ShowThread.aspx?PostID=1085
第一课:简单的欢迎程序
第二课:表达式,类型和变量
第三课:选择控制语句
第四课:循环控制语句
第五课:方法
第六课:名称空间
第七课:类的入门
第八课:类的继承
第九课:多态性
第十课:属性
第十一课:索引指示器
第一课:简单的欢迎程序
在本文开始写作的时候,虽然商用C# 编译器尚未推出, 但你可以下载微软的.NET Frameworks SDK Beta 1.
本节课通过介绍几个简单的程序,使得你对C#有所入门。本节程要达到如下几个目的:
1.理解一个C#程序的基本结构。
2.初步了解"名称空间"的概念。
3.初步了解"类"的概念。
4.了解"Main"方法所做的工作。
5.学会如何读取命令行输入信息。
6.学会使用控制台输入/输出 (I/O)语句。
1.清单1-1. 一个简单的欢迎程序Welcome.cs
// Namespace Declaration
using System;
// Program start class
class WelcomeCSS {
// Main begins program execution.
public static void Main() {
// Write to console
Console.WriteLine("Welcome to the C# Station Tutorial!");
}
}
说明
1.清单1-1中的程序包括四个基本元素:名称空间的声明,类,"Main"方法和语句。
2.本例中对名称空间的声明,表明正在使用"System"这个名称空间。
名称空间内包含了一组可以被C#程序调用的代码。有了"using System;"这个声明,就表明程序可以引用该"System"名称空间内的代码,而无需在每个引用的前面加上"System"。关于这一点,我将在后面专门介绍名称空间的课程中详细介绍。
3.类"class WelcomeCSS"包含了程序所要用到的数据,和所要执行的方法的定义。
同诸如接口和结构这样的元素类似,类在程序中是用来描述对象的,这些元素都将会在后续课程中详细介绍。本例中的类不包含数据,只包含一个方法。该方法定义了该类的行为(或者称为该类所能做的事情)。
4.程序运行时,WelcomeCSS类中的该方法表明了该类所要完成的事情。
方法名"Main"作为保留字,作为程序的起点。"Main"前面是个名为"static"的修饰符。"static"修饰符表明该方法仅仅在该特定的类中工作,而不是在在该类的实例中工作。这是必需的,因为一旦程序启动后,并不存在对象的实例。类,对象和实例的具体用法将会在后面的课程中覆盖到。每个方法必须有个返回值类型。本例中,返回值类型是"void",它表明"Main"函数没有返回值。每个方法名的后面也都跟着个参数表,参数表包含有零个或者多个参数并用括号括起来。为了简单起见,没有在"Main"后面添加参数。后面的课程中,将介绍"Main"方法所允许采用的参数类型。
5."Main"方法通过"Console.WriteLine(...)" 语句表明其行为。
"Console" 是"System" 名称空间中的类。"WriteLine(...)"是"Console" 类中的方法。我们使用"."这个句点操作符来标记程序中的从属元素。注意到,我们也可以这样来书写:"System.Console.WriteLine(...)",这样的书写格式是很有趣的,它是根据"namespace.class.method" 的格式进行书写的。如果在程序的一开始,没有采用"using System"的声明,那么就必须严格遵守"System.Console.WriteLine(...)"这样的书写格式。该语句的执行结果是在控制台控制台上输出字符串"Welcome to the C# Station Tutorial!" 。
6.注释是由"//"标出的。
例子中的这些注释都是单行注释,表明从该注释符号的开始处到该行结束处,都是注释部分。如果你的注释要跨越若干行,即多行注释,可以以符号"/*"开始,以符号"*/"结束,其中所包含的全部是注释。你也可以在多行注释符号中包含单行注释。但是,不能在单行注释符号后面放上多行注释符号。程序编译时,将忽略掉注释部分。注释的目的是为了用简单的英语给程序所要完成的工作加上注解。
7.所有语句都以分号";"结束。
类和方法以"{"开始,以"}"结束。任何位于"{"和"}"之间的语句定义为块。块定义了程序元素的活动范围 (或者称为生命期和可见性),这些概念将在后面的课程中加以介绍。
8.可以编写出能够接受命令行输入信息的程序。
命令行输入信息的集合是在"Main"方法中进行处理的。清单1-2中的程序,可以从命令行中接受输入一个名字,之后在控制台上显示出来。
2.清单1-2. 读取命令行输入信息的程序NamedWelcome.cs
// Namespace Declaration
using System;
// Program start class
class NamedWelcome {
// Main begins program execution.
public static void Main(string[] args) {
// Write to console
Console.WriteLine("Hello, {0}!", args[0]);
Console.WriteLine("Welcome to the C# Station Tutorial!");
}
}
说明
1.记住,要把你的名字添加到命令行中。
例如,在命令行中打入"NamedWelcome Joe"。如果不这样做,程序就会崩溃,在后面的课程中,将介绍如何检测这种情况,以及如何避免这种情况的出现。
2.在清单1-2中,在"Main"方法的参数表中有个入口。
参数名是"args"。 在程序的后面部分就要引用该参数。"string[]"是参数"args"的类型。"string"类型用于存放字符。这些字符可以是一个单词,也可以是多个单词。方括号"[]"表示数组,"args"参数由命令行上的若干个单词构成。
3.在"Main"方法的语句中,多了一条"Console.WriteLine(...)"语句。
该语句中的参数表同以往的写法不同,其中有个格式字符串"{0}" 参数。 格式串中的第一个参数从数字0开始,第二个参数从数字1开始,依此类推。 "{0}" 参数意味着引号后面的参数值将会输出到该位置。现在让我们来看看引号后面的参数。
4."args[0]"参数,它指向"args"数组中的第一个字符串。
数组中的第一个元素是args[0], 第二个元素是args[1],依此类推。例如,如果我在命令行中写上"NamedWelcome Joe","args[0]"的值就为"Joe".
让我们回到在格式字符串中嵌入的"{0}" 参数吧,因为"args[0]"是格式串后面的第一个参数, 一旦执行该命令时,"args[0]"的值"Joe"就会替换掉格式串中的"{0}"。一旦执行命令:"NamedWelcome Joe",输出结果就会为:
>Hello, Joe!
>Welcome to the C# Station Tutorial!
通过控制台也可以把输入信息提供给程序。清单1-3演示了交互式处理用户输入的信息的方法。
3.清单1-3. 交互式处理输入信息的程序 InteractiveWelcome.cs
// Namespace Declaration
using System;
// Program start class
class NamedWelcome {
// Main begins program execution.
public static void Main() {
// Write to console/get input
Console.Write("What is your name?: ");
Console.Write("Hello, {0}! ", Console.ReadLine());
Console.WriteLine("Welcome to the C# Station Tutorial!");
}
}
说明
这一次,"Main"方法没有用到任何参数,但现在程序中有了三条语句,前面两条语句不同于第三条语句,它们是:"Console.Write(...)"而不是 "Console.WriteLine(...)"。区别是:"Console.Write(...)"语句把信息输出到控制台,之后光标停留在同一行,而"Console.WriteLine(...)"把信息输出,之后换行。
第一条语句仅仅输出"What is your name?: "到控制台。
第二条语句要等到其参数被适当地处理之后,才会输出信息。 格式串后面的第一个参数是:"Console.ReadLine()"。这就使得程序要等待用户在控制台输入信息,输入信息以回车或者换行结束。该方法的返回值替换了格式串中的"{0}"参数,并输出到控制台上。
最后一个语句也用来输出信息到控制台,这一点我们在前面已经介绍过。一旦运行了程序"InteractiveWelcome",其输出结果为:
>What is your Name?
>Hello, ! Welcome to the C# Station Tutorial!
小结
到现在为止,你已经了解了C#程序的基本结构,名称空间和类。你还了解到"Main"方法是C# 程序的入口,并学会了如何捕捉命令行的输入信息,以及如何进行交互式的I/O操作。
第二课:表达式,类型和变量
逻辑与 & 左
逻辑异或 ^ 左
逻辑或 | 左
条件与 && 左
条件或 || 左
条件 ?: 右
赋值等 = *= /= %= += -= <<= >>= &= ^= |= 右
左结合意味着运算符是从左到右进行运算的。右结合意味着所有的运算是从右到左进行的,如赋值运算符,要等到其右边的计算出来之后,才把结果放到左边的变量中。
2.清单 1-2. 单目运算符: Unary.cs
using System;
class Unary {
public static void Main() {
int unary = 0;
int preIncrement;
int preDecrement;
int postIncrement;
int postDecrement;
int positive;
int negative;
sbyte bitNot;
bool logNot;
preIncrement = ++unary;
Console.WriteLine("Pre-Increment: {0}", preIncrement);
preDecrement = --unary;
Console.WriteLine("Pre-Decrement: {0}", preDecrement);
postDecrement = unary--;
Console.WriteLine("Post-Decrement: {0}", postDecrement);
postIncrement = unary++;
Console.WriteLine("Post-Increment: {0}", postIncrement);
Console.WriteLine("Final Value of Unary: {0}", unary);
positive = -postIncrement;
Console.WriteLine("Positive: {0}", positive);
negative = +postIncrement;
Console.WriteLine("Negative: {0}", negative);
bitNot = 0;
bitNot = (sbyte)(~bitNot);
Console.WriteLine("Bitwise Not: {0}", bitNot);
logNot = false;
logNot = !logNot;
Console.WriteLine("Logical Not: {0}", logNot);
}
}
说明
1.当计算表达式的时候,在后置增一和后置减一运算符进行运算时,先返回其值,再进行增一或者减一运算。当使用前置加号和减号运算符进行运算时,是先进行增一或者减一的运算,然后再返回其结果值。
2.在清单1-2中, 变量unary初始化为0,进行++x 运算时,"unary"的值加1,再把其值1赋给"preIncrement"变量。在进行--x运算时,先把"unary"的值减到0, 再把值0赋给"preDecrement"变量。
3.进行x-运算时,先把"unary"的值0赋给"postDecrement" 变量,之后再把"unary"减到-1。进行x++运算时,先把"unary"的值-1赋给"postIncrement"变量,之后再对"unary"加1,使得"unary"变量现在的值为0。
4.变量"bitNot"初始值为0,进行按位取反运算,本例中,数0表示为二进制"00000000",按位取反之后变为-1,其二进制表示为"11111111"。
5.了解一下表达式"(sbyte)(~bitNot)", 任何对类型sbyte, byte, short 或者 ushort 类型数据的运算,返回结果都是整数。要把值赋给bitNot变量,我们必须使用cast (类型)运算符(强制类型转换),其中Type表示你希望转换成的类型(本例中为sbyte)。 Cast运算符把大范围类型的数据转换为小范围类型的数据时,须特别谨慎,因为此时有丢失数据的危险。一般来说,把小类型的数据赋给大类型变量,并没有问题, 因为大范围数据类型的变量具有足够的空间存放小类型数据。 注意在signed 和unsigned类型之间进行Cast运算时,也存在此类危险。 许多初级程序设计教程对变量的位表示作出了很好的讲解,同时也介绍了直接进行Cast运算的危险。
逻辑非(!)运算符可以处理布尔变量值。本例中,"logNot"变量从false 变为true。
上述程序的输出结果如下:
>Pre-Increment: 1
>Pre-Decrement 0
>Post-Decrement: 0
>Post-Increment -1
>Final Value of Unary: 0
>Positive: 1
>Netative: -1
>Bitwise Not: -1
>Logical Not: True
3.清单 1-3. 二元运算符 Binary.cs
using System;
class Binary {
public static void Main() {
int x, y, result;
float floatResult;
x = 7;
y = 5;
result = x+y;
Console.WriteLine("x+y: {0}", result);
result = x-y;
Console.WriteLine("x-y: {0}", result);
result = x*y;
Console.WriteLine("x*y: {0}", result);
result = x/y;
Console.WriteLine("x/y: {0}", result);
floatResult = (float)x/(float)y;
Console.WriteLine("x/y: {0}", floatResult);
result = x%y;
Console.WriteLine("x%y: {0}", result);
result += x;
Console.WriteLine("result+=x: {0}", result);
}
}
说明
清单1-3 演示了二元操作符的几个例子。加法(+),减法(-),乘法(*)和除法(/)的运算结果,就是我们通常进行的的四则运算的结果。
因为"floatResult"变量是浮点运算类型,所以整型变量"x"和"y" 被强制转换成浮点类型来计算FloatResult。
这里有个求余数的运算符,两个操作数相除,返回余数。
最后一条语句给出了另外一种赋值形式,这里用了(+=)运算符.无论什么时候你使用(+=)运算符,那么这个二进制运算符就应该在运算符左右两边都进行运算,然后把值赋给左边的参数。本语句相当于"result = result + x",并返回同样的值。
前面的课程中,你看到的使用次数较多的一种类型是"string" (字符串)类型。"string"类型是由包含在引号内的Unicode编码的字符构成。例如"This is a string."
另外一种数据类型是数组。数组可以看成是同种类型的元素构成的集合。当声明数组时,你要指定类型名,数组名,维数和数组大小。
4.清单 1-4. Array Operations: Array.cs
using System;
class Array {
public static void Main() {
int[] myInts = { 5, 10, 15 };
bool[][] myBools = new bool[2][];
myBools[0] = new bool[2];
myBools[1] = new bool[1];
double[,] myDoubles = new double[2, 2];
string[] myStrings = new string[3];
Console.WriteLine("myInts[0]: {0}, myInts[1]: {1}, myInts[2]: {2}", myInts[0], myInts[1], myInts[2]);
myBools[0][0] = true;
myBools[0][1] = false;
myBools[1][0] = true;
Console.WriteLine("myBools[0][0]: {0}, myBools[1][0]: {1}", myBools[0][0], myBools[1][0]);
myDoubles[0, 0] = 3.147;
myDoubles[0, 1] = 7.157;
myDoubles[1, 1] = 2.117;
myDoubles[1, 0] = 56.00138917;
Console.WriteLine("myDoubles[0, 0]: {0}, myDoubles[1, 0]: {1}", myDoubles[0, 0], myDoubles[1, 0]);
myStrings[0] = "Joe";
myStrings[1] = "Matt";
myStrings[2] = "Robert";
Console.WriteLine("myStrings[0]: {0}, myStrings[1]: {1}, myStrings[2]: {2}", myStrings[0], myStrings[1], myStrings[2]);
}
}
说明
清单 1-4 演示了数组的各种不同实现方法。第一个例子是"myInts"数组,它在声明的同时进行了初始化。
接着是个二维数组,可以把它理解为数组的数组。我们需要使用"new"运算符来实例化初始数组的大小,之后,再对每个子数组使用new运算符。
第三个例子是个二维数组。数组可以是多维的,每一维可以通过逗号隔开,也必须用"new"运算符进行实例化。
最后定义了一个一维的字符串数组。
每种情况下,对于数据元素的访问可以通过引用元素的位置(下标)来进行。数组的大小可以是任何整型值。其下标从0开始。
小结
到现在为止,你已经了解了C# 的变量,简单数据类型,数组和字符串。我们还学习了如何用C#的运算符构成表达式。
第三课:选择控制语句
本节课将介绍如何使用C#选择控制语句,第三课将达到如下几个目的:
1.学会"if"语句的用法。
2.学会"switch"语句的用法。
3.学会在"switch"语句中如何使用"break"语句。
4.理解"goto"语句的正确用法。
在前面几节课中,你所看到的程序都是顺序执行的。你无法控制输入语句,你所能做的就是跟着程序执行直到终止。本节课中,将介绍基于条件进行判断,从而选择进入相应的逻辑分支中去执行。
我们所介绍的第一个选择语句是"if"语句,它有三种基本形式:单条选择, 如果/否则,以及多情形选择。
1.清单3-1. IF语句的格式:IfSelection.cs
using System;
class IfSelect {
public static void Main() {
string myInput;
int myInt;
Console.Write("Please enter a number: ");
myInput = Console.ReadLine();
myInt = Int32.Parse(myInput);
// Single Decision and Action with brackets
if (myInt > 0) {
Console.WriteLine("Your number {0} is greater than zero.", myInt);
}
// Single Decision and Action without brackets
if (myInt < 0)
Console.WriteLine("Your number {0} is less than zero.", myInt);
// Either/Or Decision
if (myInt != 0) {
Console.WriteLine("Your number {0} is not equal to zero.", myInt);
}
else {
Console.WriteLine("Your number {0} is equal to zero.", myInt);
}
// Multiple Case Decision
if (myInt < 0 || myInt == 0) {
Console.WriteLine("Your number {0} is less than or equal to zero.", myInt);
}
else if (myInt > 0 && myInt <= 10) {
Console.WriteLine("Your number {0} is between 1 and 10.", myInt);
}
else if (myInt > 10 && myInt <= 20) {
Console.WriteLine("Your number {0} is between 11 and 20.", myInt);
}
else if (myInt > 20 && myInt <= 30) {
Console.WriteLine("Your number {0} is between 21 and 30.", myInt);
}
else {
Console.WriteLine("Your number {0} is greater than 30.", myInt);
}
}
}
说明
1.清单3-1中的IF语句的各种格式都使用了同一个输入变量"myInt"。
这是从用户获得交互内容的另一种方式。我们首先输出一行信息:"Please enter a number:"到控制台。"Console.ReadLine()"语句使得程序等待来自用户的输入,一旦用户输入一个数字,按回车键之后,该数字以字符串的形式返回到"myInput"变量中,由于我们需要的是一个整数,所以需要转换变量"myInput"成整型数据。用命令"Int32.Parse(myInput)"即可完成。 (Int32 等数据类型将在后面的课程中加以介绍。) 转换结果放到"myInt"变量中,这是个整数类型。
2.有了我们所需要的类型的数据,就可以用"if"语句来进行条件判断了。
对于第一种形式的IF语句,格式为: if (boolean expression) { statements }。该语句必须以关键字"if"开始。之后,括号中为布尔表达式。该布尔表达式必须计算出一个true或者false值。在本例中,我们检查用户的输入,看看输入值是否大于0,如果表达式运算结果为true,就执行大括号中的语句。(我们把大括号之间的语句部分称为"block"。) 块中有一个或者多个语句。如果布尔表达式的值为false,我们就忽略块中的语句,直接执行块后面的语句。
3.除了没有块之外,第二种"if"语句的格式非常类似于第一种。
因此,如果布尔表达式为true,将会执行布尔表达式之后的第一条语句。当布尔表达式的值为false,布尔表达式之后的第一条语句将被忽略掉,而直接执行其后的程序语句。如果你只有一条语句需要执行,就用该格式的"if"语句。如果你打算在布尔表达式的值为true时,执行两条或者两条以上的语句,就必须把它们放到块中。我个人的建议是:无论需要执行几条语句,要养成把if语句放到块中的习惯, 这就使得你避免犯如下错误:当添加了一条语句之后,忘记了添加一对括号。
4.大多数时候,你需要作出如下选择:当条件满足时做一件事,否则做另外一件事。
清单3-1中,程序演示了这种if语句格式的用法。 当布尔表达式为true时, 就立刻执行"if"后面的那条语句, 而当布尔表达式为false时,就执行"else"关键字后面的语句。
5.当要计算多个布尔表达式时,你可以使用if/else if/else 这种格式,上面的例子程序演示了这种形式,从关键字"if"开始, 一旦布尔表达式为真,就执行if后面的块。但是,这一次,在组合关键字"else if"后面还可以进行多个条件的判断。"else if"语句后面也有个布尔表达式,一旦该布尔表达式的值为true,就会执行紧接其后的块。这种情形可以一直持续下去,直到所有的情况都已经计算出来,但是整个"if/else if"序列必须以"else"结束。当所有的"if"或者"else if" 后面的布尔表达式的值都为false时,就执行关键字"else"后面的块。 对于if/else if/else格式的语句,每次仅仅执行一个其中部分的语句 。
6.上面的例子中,布尔表达式 (myInt < 0 || myInt == 0)包含了条件OR (||)运算符。
对于常规OR (|)运算符和条件OR (||)运算符来说,只要有运算符两边的子表达式之一为真,整个布尔表达式的值就为真。两个运算符的区别在于:正规OR 运算符(|)每次都对运算符(|)两边的表达式进行计算。而条件运算符OR (||)只有当第一个子表达式的值为false时,才计算第二个子表达式的值。
7.布尔表达式 (myInt > 0 && myInt <= 10)包含了条件运算符AND。
对于常规AND (&) 运算符和条件AND (&&)运算符来说,只有当运算符两边的子表达式的值都为真时,整个布尔表达式的值为真。两种运算符的区别在于:正规AND (&)运算符每次都计算运算符两边的子表达式的值,但是对于条件AND运算符来说,只有当第一个子表达式的值为真时,才计算第二个表达式的值。条件运算符(&& 和 ||) 通常称为运算优化的运算符,因为有时不需要计算整个表达式的值。这样就可以忽略掉不必要的逻辑表达式的计算,可以生成有效的代码。
同if/else if/else 格式的"if"语句类似,"switch"语句的用法如下:
2.清单3-2. 分支选择语句: SwitchSelection.cs
using System;
class SwitchSelect {
public static void Main() {
string myInput;
int myInt;
begin:
Console.Write("Please enter a number between 1 and 3: ");
myInput = Console.ReadLine();
myInt = Int32.Parse(myInput);
// switch with integer type
switch (myInt) {
case 1:
Console.WriteLine("Your number is {0}.", myInt);
break;
case 2:
Console.WriteLine("Your number is {0}.", myInt);
break;
case 3:
Console.WriteLine("Your number is {0}.", myInt);
break;
default:
Console.WriteLine("Your number {0} is not between 1 and 3.", myInt);
}
decide:
Console.Write("Type \"continue\" to go on or \"quit\" to stop: ");
myInput = Console.ReadLine();
// switch with string type
switch (myInput) {
case "continue":
goto begin;
case "quit":
Console.WriteLine("Bye.");
break;
default:
Console.WriteLine("Your input {0} is incorrect.", myInput);
goto decide;
}
}
}
说明
1.清单3-2 演示了多分支选择语句switch的用法。
关键字"switch"后面是switch 表达式。Switch表达式必须是以下类型中的一种: sbyte,byte,short,ushort, int,uint,long,ulong,char,string,或者enum类型。(enum类型将在后面的课程中加以介绍)。在清单3-2的第一个"switch"语句中, switch 表达式计算的是整数类型的数据。
2.在switch表达式后面是个switch 块, 当Switch表达式的值同某一个case后面的常量表达式的值相匹配时,就执行此case后面的语句,直到遇到"break"语句或者"goto"语句为止。每个分支以关键字"case"作为标号,其后为常量表达式,然后是分号(:)。本例子程序中,我们有"case 1:","case 2:"和"case 3:"。
3.你可以在所有的分支选择的后面加上一个"default"分支。
如果没有匹配的常量表达式,就进入default分支,并执行该分支的语句。虽然default标号是可选的,我建议你加上该分支。这将有助于处理一些意外事件 ,使得你的程序能够捕捉和处理不可预见的事件,从而使得程序更加可靠。
4.每个"case"标号必须以"break"语句作为结束。
"break"语句将使得程序退出switch语句,并从switch块后面的一条语句开始执行。对于"default"标号来说,"break"语句是可选的,因为有"break"语句和没有"break"语句,其运行结果是一样的。如果你在switch块中放置了一个"goto"语句,情况就不一样了。
5.清单3-2中的第二个"switch"语句演示了"goto"语句的用法。
"goto"语句可以让程序跳到关键字"goto"后面的标号中去执行。程序执行过程中,如果用户打入"continue", switch语句就匹配case "continue" 中的常量表达式,并执行"goto begin:"语句。程序就会离开"switch"语句,开始执行标号"begin:"后的第一条语句。这是个有效的循环,可以让你多次重复执行同样一段代码。一旦用户打入字符串"quit",循环将会结束。此时,将进入case "quit" 分支。该分支将输出信息"Bye."到控制台上,之后跳出switch语句,再结束程序。
一旦输入的字符串既不是"continue"也不是"quit",就会进入"default:"分支。于是,将会输出一个出错信息到控制台上,之后执行"goto decide:"命令。这就使得程序转跳到"decide:" 标号后面的第一条语句, 该语句执行后,将会询问用户是否愿意continue(继续)还是 quit(退出)。这是个有效的循环。
显然,"goto"语句功能强大,在受控的场合下,"goto"语句是很有用途的。但是,必须指出的是,"goto"如果出现被滥用的趋势,程序的调试和维护将会变得很困难。想象一下,如果程序到处可见goto语句,其流程将会变得难以阅读和理解。下节课中,将介绍在程序中创建循环语句的更好方法。
小结
现在,你已经了解了如何使用"if"语句的各种格式,也了解了如何使用"switch"语句。你也了解了使用"break"语句可以从"switch"语句中推出。最后,你也了解了如何使用"goto"语句跳到程序的另外一个部分。
第四课:循环控制语句
本节课将介绍如何使用C#控制语句中的循环语句,本课目的如下:
1.学会"while"循环的用法。
2.学会"do" 循环的用法。
3.学会"for" 循环的用法。
4.学会foreach循环的用法。
5.进一步了解"break"语句的用法。
6.如何使用"continue"语句。
在C#中,使用"goto"语句并不是一个最佳的建立循环的方法。本节课将介绍建立循环的常用方法。
第一个要介绍的语句是while循环语句
1.清单 4-1. While循环:While loop.cs
using System;
class Whileloop {
public static void Main() {
int myInt = 0;
while (myInt < 10) {
Console.Write("{0} ", myInt);
myInt++;
}
Console.WriteLine();
}
}
说明
1.清单 4-1演示了一个简单的while 循环。
以关键字"while"开始,后面是个布尔表达式。所有的控制语句都使用了布尔表达式。这就意味着表达式必须计算出true 或者false值。本例中,我们将检查myInt变量,看看它是否小于10。 因为myInt已经初始化为0,第一次计算时,该布尔表达式将返回true值。一旦布尔表达式的值为true,将执行该布尔表达式后面块中的语句。
2.在while块中,我们把数字和空格输出到控制台中,之后,对 myInt进行加1运算。一旦执行了while块中的语句之后,再次计算布尔表达式的值,这种情况将一直循环下去,直到该布尔表达式的值为false为止。 一旦布尔表达式的值为false, 程序将从while 块之后的第一条语句开始执行。在本例中,我们把数字0到9输出到控制台中,之后退出while块,再输出一个新行到控制台。
同 "while"循环类似的是"do" 循环语句。
2.清单 4-2. Do 循环: Do loop.cs
using System;
class Doloop {
public static void Main() {
string myChoice;
do {
// Print A Menu
Console.WriteLine("My Address Book\n");
Console.WriteLine("A - Add New Address");
Console.WriteLine("D - Delete Address");
Console.WriteLine("M - Modify Address");
Console.WriteLine("V - View Addresses");
Console.WriteLine("Q - Quit\n");
Console.WriteLine("Choice (A,D,M,V,or Q): ");
// Retrieve the user's choice
myChoice = Console.ReadLine();
// Make a decision based on the user's choice
switch(myChoice) {
case "A":
case "a":
Console.WriteLine("You wish to add an address.");
break;
case "D":
case "d":
Console.WriteLine("You wish to delete an address.");
break;
case "M":
case "m":
Console.WriteLine("You wish to modify an address.");
break;
case "V":
case "v":
Console.WriteLine("You wish to view the address list.");
break;
case "Q":
case "q":
Console.WriteLine("Bye.");
break;
default:
Console.WriteLine("{0} is not a valid choice", myChoice);
}
// Pause to allow the user to see the results
Console.Write("Press any key to continue...");
Console.ReadLine();
Console.WriteLine();
} while (myChoice != "Q" && myChoice != "q"); // Keep going until the user wants to quit
}
}
说明
1.清单 4-2 演示了"do"循环的例子。 "do" 循环的格式是: do { <语句> } while (<布尔表达式>);其中的语句可以是任何合法的C#语句,布尔表达式同以前的规定一样,其返回值要么为true,要么为false。
2.如果你打算输出清单4-2 中的信息或者菜单,之后再读取用户的输入,那么就使用"do" 格式的循环而不要使用"while" 格式的循环语句。因为在本例中,布尔表达式的值是在循环结束处进行计算的, "do" 循环可以保证:循环体中的语句至少执行一次。与此相对应的是:"while" 循环一开始时,计算布尔表达式的值,"while" 循环不能保证循环体中的语句能够至少执行一次。
3.让我们来回顾一下清单 4-2中的内容。
在Main()方法中,我们定义了变量 "myChoice"为字符串类型。之后,输出一些信息到控制台,即输出一个可让用户进行选择的菜单。我们必须要得到用户的输入,即Console.ReadLine()方法所返回的值,该值存放在myChoice变量中。我们必须先得到用户的输入,之后再处理。要完成这件事,一个有效的方法是就使用 "switch"语句。注意到:为了获得同样的功能,我们既匹配了小写字母,也匹配了大写字母。另外一点是:我们使用了 "default:" case,这是个良好的编程作风。
3.清单 4-3. For 循环: For loop.cs
using System;
class Forloop {
public static void Main() {
for (int i=0; i < 20; i++) {
if (i == 10)
break;
if (i % 2 == 0)
continue;
Console.Write("{0} ", i);
}
Console.WriteLine();
}
}
说明
1.清单 4-3 演示了"for" 循环的用法。
当你能够精确地知道循环的次数时, For 循环语句就派上了用场。本程序同清单 4-1中"while" 循环程序的运行结果一样。 "for" 循环中,括号中内容由三个由分号隔开的部分: "(<初始化表>; <布尔表达式>; <post-loop 动作表>)"组成
2.初始化表是个由逗号隔开的表达式,在"for" 循环的整个周期中,这些表达式仅仅计算一次。计算是在一开始进行的,并且在循环语句的执行之前进行。正如在清单 4-3所见,通常初始化表是先初始化一个作为计数器的整型变量。
3.一旦进行初始化后, "for" 循环就进入第二部分:布尔表达式的计算。 这里的布尔表达式可以写得很复杂,但是结果只能是true或者false。布尔表达式通常用来验证计数器变量的状态。
4.一旦布尔表达式的值为true时,就会执行"for" 循环大括号中的语句。通常情况下,这些语句从左大括号开始,不中断地一直执行到右大括号。但在清单4-3中,有几个例外,几个"if"语句改变了程序的流程。
第一个"if"语句检查"i"是否等于10,这里使用了"break"语句,可在此处跳出循环,转入 "for"块后面的第一条语句的执行。
第二条"if"语句使用了求余运算符,看看"i"是否能够被2整除,如果余数等于0,就执行 "continue"语句。控制将跳过循环体中余下的语句,转入下一轮新的循环。你需要在块中正确地组织好语句,一旦满足有关条件,就执行相应的语句。
5.一旦程序流程遇到continue语句,或者遇到块的结尾处的右括号, 就转入"for" 循环括号中的第三项:post-loop 动作表,该表由逗号隔开,在"for"块中的语句都执行完毕之后,就执行该动作表中的动作。 清单 4-3中的 post-loop 动作表中的动作很典型:计数器加1。一旦动作表执行完毕,流程就转到对布尔表达式值进行判别。循环将继续进行下去,直到布尔表达式的值为true。当布尔表达式的值为false,控制流程就转到 "for"块之后的第一条语句。
4.清单 4-4. The ForEach 循环: ForEachloop.cs
using System;
class ForEachloop {
public static void Main() {
string[] names = {"Cheryl", "Joe", "Matt", "Robert"};
foreach (string person in names) {
Console.WriteLine("{0} ", person);
}
}
}
说明
1."foreach" 循环列举出集合中所有的元素。
清单 4-4所使用的数组类型,就是这样的一个集合。(在"System.Collections"也可以由其他的数据类型作集合。). 在Main()方法中,我们所做的第一件事是:定义由四个字符串组成的数组names。
2."foreach"括号中的表达式是由关键字in隔开的两个项组成。in右边的项是集合名,in左边的项是变量名,用来存放该集合中的每个元素。
该循环的运行过程如下:每一次循环时,从集合中取出一个新的元素值,放到只读变量中去,括号中的整个表达式返回值为true, "foreach"块中的语句就能够执行。一旦集合中的元素都已经被访问到,整个表达式的值为false,控制流程就转入到 "foreach" 块后面的第一条可执行语句。
3.在清单 4-4的例子中,我们使用了字符串变量person用来存放names数组的每个元素,并且使用Console.WriteLine()方法输出person 变量的值。
小结
到现在为止,你已经了解了"while","do","for"和"foreach" 循环语句的用法。最后你也学习了如何设置有关条件,来改变循环体块中的语句的流程。
第五课:方法
本节课向你介绍C#的方法,其目的是:
1.了解方法的结构格式
2.了解静态和实例方法之间的区别
3.学会实例对象的使用
4.学会如何调用实例化的对象
5.学会方法的四种参数类型的使用
6.学会使用"this"引用
以往,对于每个程序来说,所有的工作都在Main()方法中实现。这对于功能简单的程序是合适的,因为仅仅用来学习一些概念。有个更好的方法来组织你的程序,那就是使用方法。方法是很有用的,因为方法可以让你在不同的单元中分开设计你的逻辑模块。
方法的结构格式如下:
属性 修饰符 返回值类型 方法名(参数) { 语句 }
我们将在后面的课程中,讨论属性和修饰符。方法的返回值可以是任何一种C#的数据类型,该返回值可以赋给变量,以便在程序的后面部分使用。方法名是唯一,可以被程序调用。为使得你的代码变得更容易理解和记忆,方法的取名可以同所要进行的操作联系起来。你可以传递数据给方法,也可以从方法中返回数据。它们由大括号包围起来。大括号中的语句实现了方法的功能。
1.清单5-1. 一个简单的方法: OneMethod.cs
using System;
class OneMethod {
public static void Main() {
string myChoice;
OneMethod om = new OneMethod();
do {
myChoice = om.getChoice();
// Make a decision based on the user's choice
switch(myChoice) {
case "A":
case "a":
Console.WriteLine("You wish to add an address.");
break;
case "D":
case "d":
Console.WriteLine("You wish to delete an address.");
break;
case "M":
case "m":
Console.WriteLine("You wish to modify an address.");
break;
case "V":
case "v":
Console.WriteLine("You wish to view the address list.");
break;
case "Q":
case "q":
Console.WriteLine("Bye.");
break;
default:
Console.WriteLine("{0} is not a valid choice", myChoice);
}
// Pause to allow the user to see the results
Console.Write("Press any key to continue...");
Console.ReadLine();
Console.WriteLine();
} while (myChoice != "Q" && myChoice != "q"); // Keep going until the user wants to quit
}
string getChoice() {
string myChoice;
// Print A Menu
Console.WriteLine("My Address Book\n");
Console.WriteLine("A - Add New Address");
Console.WriteLine("D - Delete Address");
Console.WriteLine("M - Modify Address");
Console.WriteLine("V - View Addresses");
Console.WriteLine("Q - Quit\n");
Console.WriteLine("Choice (A,D,M,V,or Q): ");
// Retrieve the user's choice
myChoice = Console.ReadLine();
return myChoice;
}
}
说明
1.清单5-1中的程序类似于第四课中的DoLoop程序。
区别在于:前一课中的程序打印出菜单内容,并在Main()方法中接受用户的输入,而本课中,该功能用一个名为getChoice()的方法实现,该方法的返回值类型是个字符串类型。在main方法中,在switch语句中用到了该串。方法"getChoice"实现了调用时所完成的工作。方法名后面的括号内是空的,因为调用getChoice()方法时,不需要传递任何数据。
2.在方法块中,我们首先定义了变量"myChoice"。
虽然它与 Main()方法中的"myChoice"变量同名同类型, 但它们是不同的两个变量,因为局部变量仅仅在其定义的块内可见。换句话说, getChoice()方法中的"myChoice" 同Main()方法中的"myChoice"变量没有丝毫联系。getChoice()方法打印出一个菜单到控制台,并读取用户的输入。"return" 语句把"myChoice"变量值返回给Main()方法中的调用者getChoice()。注意: "return"语句返回类型同该方法中定义的返回值类型相同,本例中,该返回值是个字符串。
3.在Main()方法中,在使用getChoice()之前,实例化了一个新的"OneMethod"对象。
这是因为:我们没有指明一个"静态"修饰符。(注意:Main()函数带有"静态"修饰符),getChoice()就成为一个实例的方法。 实例方法和静态方法的区别是:前者可以创建多个类的实例,每个实例有自己的单独的getChoice()方法。而一旦方法是静态的,就不存在方法的实例,你只能调用该静态方法的一个实现。
所以,正如前面所讲的,因为getChoice()并不是静态的,所以,我们必须实例化一个新对象来使用它。这是通过定义"OneMethod om = new OneMethod()"来进行的。在等号的左边,是对象引用"om",其类型是OneMethod。"om"是个对象的引用,这点很重要,"om"并不是对象自身,它是个引用OneMethod类型对象的变量。 在等号的右边,把新创建的OneMethod对象赋给引用"om"。 关键字"new"是个在堆上创建对象的新实例的C#运算符。此处完成的工作是: 在堆上创建一个新的OneMethod实例,并把它赋给om引用。一旦有了om引用的OneMethod对象实例,就可以通过om引用来对实例进行处理。
方法,域和其他类成员可以通过"." (点)运算符进行访问,标识和操纵。一旦需要调用方法getChoice(),就通过om引用,并使用点运算符"om.getChoice()"来进行。 getChoice() 块中的语句执行完毕之后即返回。为了捕捉到getChoice()的返回值,我们使用了赋值运算符"="。 返回串放到了Main()函数的局部变量 myChoice中,从那里,程序的其余部分按照前面课程中介绍的方式正常执行。
2.清单5-2. 方法参数:MethodParams.cs
using System;
class Address {
public string name;
public string address;
}
class MethodParams {
public static void Main() {
string myChoice;
MethodParams mp = new MethodParams();
do {
// show menu and get input from user
myChoice = mp.getChoice();
// Make a decision based on the user's choice
mp.makeDecision(myChoice);
// Pause to allow the user to see the results
Console.Write("Press any key to continue...");
Console.ReadLine();
Console.WriteLine();
} while (myChoice != "Q" && myChoice != "q"); // Keep going until the user wants to quit
}
// show menu and get user's choice
string getChoice() {
string myChoice;
// Print A Menu
Console.WriteLine("My Address Book\n");
Console.WriteLine("A - Add New Address");
Console.WriteLine("D - Delete Address");
Console.WriteLine("M - Modify Address");
Console.WriteLine("V - View Addresses");
Console.WriteLine("Q - Quit\n");
Console.WriteLine("Choice (A,D,M,V,or Q): ");
// Retrieve the user's choice
myChoice = Console.ReadLine();
return myChoice;
}
// make decision
void makeDecision(string myChoice) {
Address addr = new Address();
switch(myChoice) {
case "A":
case "a":
addr.name = "Joe";
addr.address = "C# Station";
this.addAddress(ref addr);
break;
case "D":
case "d":
addr.name = "Robert";
this.deleteAddress(addr.name);
break;
case "M":
case "m":
addr.name = "Matt";
this.modifyAddress(out addr);
Console.WriteLine("Name is now {0}.", addr.name);
break;
case "V":
case "v":
this.viewAddresses("Cheryl", "Joe", "Matt", "Robert");
break;
case "Q":
case "q":
Console.WriteLine("Bye.");
break;
default:
Console.WriteLine("{0} is not a valid choice", myChoice);
}
}
// insert an address
void addAddress(ref Address addr) {
Console.WriteLine("Name: {0}, Address: {1} added.", addr.name, addr.address);
}
// remove an address
void deleteAddress(string name) {
Console.WriteLine("You wish to delete {0}'s address.", name);
}
// change an address
void modifyAddress(out Address addr) {
//Console.WriteLine("Name: {0}.", addr.name); // causes error!
addr = new Address();
addr.name = "Joe";
addr.address = "C# Station";
}
// show addresses
void viewAddresses(params string[] names) {
foreach (string name in names) {
Console.WriteLine("Name: {0}", name);
}
}
}
说明
1.清单5-2是清单5-1的修改,主要是对程序进行了模块化,并添加了更多的实现,以便阐述参数传递的用法。
C#可以处理四种类型的参数:out(输出),ref(引用),params(数组)和value(值)。为了说明参数的用法,我们用两个字符串域创建地址类。
2.在Main()方法中,我们调用getChoice()来读取用户的输入,并且把字符串放到myChoice变量中。
之后,把myChoice变量作为makeDecision()函数的实在参数。在实现makeDecision()方法时,注意其形式参数为字符串myChoice。需要再次说明的是:这是个新的myChoice变量,不同于调用者的实在参数,仅仅是适用于本方法的局部变量。 因为makeDecision()方法的myChoice参数没有任何其他修饰符,故认为它是"值"参,即实在参数的值被拷贝到栈中,故作为值参的变量是局部的,任何对局部变量值的改变并不影响到调用者的实在参数的值。换句话说,值参仅仅是来自调用者的输入。
3.makeDecision()方法中的switch语句完成如下功能:
在每种情形下,都调用相应的方法。这些方法的调用不同于Main()方法。除了使用"mp" 引用,它们还使用了"this"关键字。"this"是对当前对象的引用。由于makeDecision()方法不是静态方法,当前对象已经被实例化,所以可以使用"this"引用来调用同一实例中的方法。
4.addAddress()方法用到了"ref"参数,即引用可作为参数来传递,即该引用被拷贝到栈中,其引用的对象同调用者的实参所引用的对象是同一个。
这意味着:任何对局部引用的对象的改变也就是对调用者所引用的对象的改变。你可以想象一下,这就相当于输入/输出参数。
5.modifyAddress()中有一个输出参数。
输出参数仅仅传递给被调用函数。一旦调用该方法时,在栈中的仅有的一个引用未被赋值,因为根据赋值的确定性原则,在该变量没有被赋值之前,就不能使用该变量。modifyAddress()方法的第一行作为注释,说明了这一点。你可以试着去掉注释符,编译一下程序,看看结果如何。一旦该变量被赋了值,在程序返回之后,输出参数就被拷贝到调用者的参数中。所以,在方法返回之前,必须给输出参数赋值。
小结
C# 语言的一个很有用途的参数类型是数组参数,它须是一维或多维的数组。在makeDecision()方法中,我们传递了用四个逗号隔开的字符串参数。参数的个数是变量。在viewAddresses()方法中,使用了foreach循环,逐一输出这些字符串。数组参数仅是一种输入性质的参数,任何对数组参数值的改变仅仅影响到局部的副本值。
概括地讲,你现在已经理解了方法的组织结构。你还了解了方法的实现可以采用的四种参数类型及其格式。 一旦你使用实例方法,就必须实例化该对象,静态方法则不同,后者可以在任何时候被调用。另外,你也了解了"this"引用是如何调用实例方法的。
第六课:名称空间
本节课将介绍C#的名称空间。其目的是:
1.了解什么是名称空间。
2.了解如何实现"using"指示符。
3.了解"alias" 指示符的用法。
4.了解名称空间的成员的内容。
在第一课中,你已经在简单的hello程序中看到了"using System;"指示符的使用。该指示符可以让你使用System名称空间中的成员。在第一课中,未及对此作出详细介绍,现在我们来解释一下名称空间的具体用法。一旦学完了本节课,你将了解"using"指示符及其相关内容。
作为C#的元素,名称空间可以用来帮助组织程序的结构,可以避免两套代码集中命名的冲突。在程序代码中,使用名称空间是个良好的编程习惯,因为这有助于重用你的程序代码。
1.清单6-1. The C# Station Namespace: NamespaceCSS.cs
// Namespace Declaration
using System;
// The C# Station Namespace
namespace csharp_station {
// Program start class
class NamespaceCSS {
// Main begins program execution.
public static void Main() {
// Write to console
Console.WriteLine("This is the new C# Station Namespace.");
}
}
}
说明
清单6-1演示了如何创建一个名称空间。把单词"namespace"放在"csharp_station"之前,就创建了一个名称空间。"csharp_station"名称空间内的大括号中包含了成员。
2.清单6-2 Nested Namespace 1: NestedNamespace1.cs
// Namespace Declaration
using System;
// The C# Station Tutorial Namespace
namespace csharp_station {
namespace tutorial {
// Program start class
class NamespaceCSS {
// Main begins program execution.
public static void Main() {
// Write to console
Console.WriteLine("This is the new C#
Station Tutorial Namespace.");
}
}
}
}
说明
名称空间可以建立一个代码的组织结构。一个良好的编程习惯是:用层次模式来组织你的名称空间。你可以把通用一些的名称放在最顶层,里层则放置一些专用一些的名称。这个层次系统可以用嵌套的名称空间表示。清单6-2演示了如何建立一个嵌套的名称空间。在不同的子名称空间内放置代码,从而组织好你的代码的结构。
3.清单6-3. Nested Namespace 2: NestedNamespace2.cs
// Namespace Declaration
using System;
// The C# Station Tutorial Namespace
namespace csharp_station.tutorial {
// Program start class
class NamespaceCSS {
// Main begins program execution.
public static void Main() {
// Write to console
Console.WriteLine("This is the new C# Station Tutorial Namespace.");
}
}
}
说明
清单6-3演示了另外一种编写嵌套的名称空间的方法。在"csharp_station"和"tutorial"之间置入点运算符,表明这是嵌套的名称空间。结果同清单6-2。 相比而言,清单6-3 更易书写。
4.清单6-4. Calling Namespace Members: NamespaceCall.cs
// Namespace Declaration
using System;
namespace csharp_station {
// nested namespace
namespace tutorial {
class myExample1 {
public static void myPrint1() {
Console.WriteLine("First Example of calling another namespace member.");
}
}
}
// Program start class
class NamespaceCalling {
// Main begins program execution.
public static void Main() {
// Write to console
tutorial.myExample1.myPrint1();
csharp_station.tutorial.myExample2.myPrint2();
}
}
}
// same namespace as nested namespace above
namespace csharp_station.tutorial {
class myExample2 {
public static void myPrint2() {
Console.WriteLine("Second Example of calling another namespace member.");
}
}
}
说明
1.清单6-4 的例子演示了用完整的名称指示,调用名称空间的成员。
一个完整的名称指示包括名称空间名,以及调用的方法名。程序的上半部分,在"csharp-station"名称空间内嵌套的名称空间"tutorial"中,定义了类"myExample1"及其方法"myPrint1"。 Main()方法中用完整的名称指示:"tutorial.myExample1.myPrint()" 来进行调用。 因为Main()方法和tutorial名称空间位于同一名称空间内,如果使用"csharp_station"的全称不是必需的。
2.清单6-4的下半部分,也是名称空间"csharp_station.tutorial"的一部分。
类"myExample1"和"myExample2"都属于该名称空间。另外,也可以把它们分别写入不同的文件,同时它们仍然属于同一名称空间。在Main()方法中,调用"myPrint2"方法时,采用了全称:"csharp_station.tutorial.myExample2.myPrint2()"。 在这里,必须使用全称中"csharp_station",因为"myExample2"定义在外部。
3.注意:这里对两个不同的类起了不同的名字:
"myExample1"和"myExample2"这是因为对于每个名称空间来说,其中的成员必须有唯一的名称。 记住:它们都处于同一名称空间中,不能取名相同。方法"myPrint1"和"myPrint2" 名称的不同仅仅是为了方便起见,即使同名也没有问题,因为它们属于不同的类。
5.清单6-5. The using Directive: UsingDirective.cs
// Namespace Declaration
using System;
using csharp_station.tutorial;
// Program start class
class
UsingDirective {
// Main begins program execution.
public static void Main() {
// Call namespace member
myExample.myPrint();
}
}
// C# Station Tutorial Namespace
namespace csharp_station.tutorial {
class myExample {
public static void myPrint() {
Console.WriteLine("Example of using a using directive.");
}
}
说明
调用方法时,如果你不想打入全称,可使用"using"指示符。在清单6-5中,有两个"using"指示符。第一个指示符是"using System",同本教程其它地方出现的"using"指示符相同。你不需要每次都打上"System",只需要打入该名称空间的成员方法名即可。在myPrint()中,"Console"是个"System"名称空间中的成员类,该类有个"WriteLine"的方法。该方法的全称是: "System.Console.WriteLine(...)"。
类似地,using指示符"using csharp_station.tutorial"可以让我们在使用 "csharp_station.tutorial" 名称空间的成员时,无需打入全称。所以,我们可以打入"myExample.myPrint()"。如果不使用"using"指示符,每次实现该方法时,我们就得打入"csharp_station.tutorial.myExample.myPrint()" 。
6.清单6-6. The Alias Directive: AliasDirective.cs
// Namespace Declaration
using System;
using csTut = csharp_station.tutorial.myExample; // alias
// Program start class
class AliasDirective {
// Main begins program execution.
public static void Main() {
// Call namespace member
csTut.myPrint();
myPrint();
}
// Potentially ambiguous method.
static void myPrint() {
Console.WriteLine("Not a member of
csharp_station.tutorial.myExample.");
}
}
// C# Station Tutorial Namespace
namespace csharp_station.tutorial {
class myExample {
public static void myPrint() {
Console.WriteLine("This is a member of csharp_station.tutorial.myExample.");
}
}
}
说明
1.有时,往往遇到取名较长的名称空间,而你可以把该名称变短些。
这样就增强了可读性,还避免了同名的冲突。清单6-6 演示了如何使用别名指示符,创建别名的格式例子是:"using csTut = csharp_station.tutorial.myExample"。表达式"csTut"可以取代"csharp_station.tutorial.myExample",用在本文件的任何地方。在Main()方法中就使用了"csTut"。
2.在Main()方法中,调用了"AliasDirective" 类中"myPrint" 方法。
这与"myExample" 类的"myPrint"方法同名。 虽然同名,这两个方法都各自正确地进行了调用,原因是:"myExample"类的"myPrint"方法用别名"csTut"表示。编译器能够准确地了解所要执行的是哪个方法。一旦漏掉了"csTut",编译器将两次调用"AliasDirective"类的"myPrint"方法。
3.另外一方面,如果我们没有创建别名指示符,而是添加了"using csharp_station.tutorial.myExample"之后,再调用myPrint(),编译器就会生成出错信息,因为它不知道究竟是调用. "csharp_station.tutorial.myExample.myPrint()"方法呢?还是去调用"AliasDirective.myPrint()"方法。所以使用名称空间是良好的编程习惯,可避免代码中的冲突现象。
小结
到现在为止,我们已经了解在名称空间中可以使用类,实际上,名称空间可以使用如下类型的数据:
类;结构;接口;枚举;代理
在后面的课程中我们将详细介绍这些数据类型。
概括来讲,你已经了解了什么是名称空间,如何定义自己的名称空间。如果你不想打入全称,可以使用"using"指示符。一旦你想缩短名称空间的长名,可以使用别名指示符。另外,除了类之外,你也了解了名称空间可以使用的其他一些数据类型。
第七课:类的入门
本节课将介绍C#中的类,其目的如下:
1.了解构造函数的实现
2.了解实例和静态成员的区别
3.了解析构函数的使用
4.熟悉类的成员类型
在本教程的一开始,你就已经接触到类的用法了。现在,既然已经对类有了感性认识,并知道如何定义一个类,本节课将
定义类的格式是:关键字"Class"后面跟着类名,类名之后的大括号中包含的是类的成员。构造函数没有返回值,且与类同名。清单7-1是一个关于类的例子。
清单 7-1. Example C# Classes: Classes.cs
// Namespace Declaration
using System;
// helper class
class OutputClass {
string myString;
// Constructor
public OutputClass(string inputString) {
myString = inputString;
}
// Instance Method
public void printString() {
Console.WriteLine("{0}", myString);
}
// Destructor
~OutputClass() {
// Some resource cleanup routines
}
}
// Program start class
class ExampleClass {
// Main begins program execution.
public static void Main() {
// Instance of OutputClass
OutputClass outCl = new OutputClass("This is printed by the output class.") { }
// Call Output class' method
outCl.printString();
}
}
说明
1.清单7-1 演示了两个类。
第一个类"OutputClass"包括:一个构造函数,一个实例方法,以及一个析构函数,还包括一个域名"myString"。构造函数是用来初始化类的数据成员的。本例中,OutputClass类的构造函数接收一个字符串参数,并把它拷贝到该类的myString域中。
2.构造函数并不是一定要用户自己定义的,例如ExampleClass类就用到了缺省的构造函数。
缺省的构造函数很简单,且不带参数。但这样的构造函数并没有多大用处。为了使得缺省的构造函数能够有些用途,用户自己可以对它初始化。例如:
public OutputClass() : this("Default Constructor String") { }
这种形式的构造函数可以包含在清单7-1中的OutputClass类中。该缺省的构造函数后跟着初始化符。分号":"标志着初始化开始,后面的"this"是关键字,表示OutputClass这个类所对应的对象。this可以有效地调用该对象中的构造函数。"this"关键字后面的参数为字符串类型。上面的初始化工作调用了OutputClass构造函数,该构造函数接收一个字符串类型的数据作为参数。初始化工作确保在类的域能够在类的实例化过程中被初始化。
上面的例子表明:一个类可以有多种格式的构造函数。具体调用哪一种构造函数取决于参数个数及参数类型。
类成员的类型
在C#中,有两种类型的类成员,实例和静态的。每次定义类的对象时,就创建了该类的一个实例。ExampleClass中的 Main() 方法创建了OutputClass类的一个名为"outCl"的实例。你可以用不同的名字创建OutputClass类的多个实例。这些实例都是各自独立的。例如,OutputClass类的两个实例创建如下:
OutputClass oc1 = new OutputClass("OutputClass1");
OutputClass oc2 = new OutputClass("OutputClass2");
于是,创建了OutputClass类的两个单独的实例,且各自带有单独的"myString"域和"printString()"方法。上例中,两个实例名为"oc1" 和"oc2"。 另外一方面,如果类成员是静态的,可以通过如下格式来访问: <classname>.<static class member>。
一旦OutputClass类有如下的静态方法:
static void staticPrinter() {
Console.WriteLine("There is only one of me.");
}
你就可以用下面的方式,从Main()中调用该函数:
OutputClass.staticPrinter();
注意:
调用类的静态成员必须通过类名而不是实例名。类的静态成员的副本仅有一个。
另外一种类型的构造函数是静态构造函数。 通过在构造函数名称的前面使用关键字"static",就可以定义一个静态的构造函数。 调用静态的构造函数的发生时间是:在创建类的实例之前 ,在调用类的静态成员之前,在调用派生类的静态构造函数之前。(在后续课程中将介绍),且仅被调用一次。
OutputClass也有一个析构函数,除了前面加上了"~"符号,就跟构造函数的格式一样。析构函数用于释放类所占用的资源。当C#垃圾搜集器决定把对象从内存中清除出去时,就会调用析构函数。
小结
现在,你已经了解了类的如下成员:域,方法,构造函数,析构函数。下面是类的完整的成员类型:
构造函数;析构函数;域;方法;属性;索引;代理 ;事件;嵌套类
上面没有介绍过的类型将在后续课程中讲解。
概括地讲,你现在已经学会了如何定义常规的和静态的构造函数,也了解了如何初始化类的域。如果没有必要实例化一个对象,可以创建静态的类成员。你也了解了用来释放资源的析构函数的用法。
第八课:类的继承
本节课将介绍C#中的继承,其目的如下:
1.基类的实现
2.类的继承
3.在派生类中初始化基类
4.如何调用基类成员
5.如何覆盖基类成员
继承是面向对象程序设计的主要特征之一,它可以让你重用代码,可以节省程序设计的时间。
1.清单8-1 继承: BaseClass.cs
using System;
public class ParentClass
{
public ParentClass()
{
Console.WriteLine("Parent Constructor.");
}
public void print()
{
Console.WriteLine("I'm a Parent Class.");
}
}
public class ChildClass : ParentClass
{
public ChildClass()
{
Console.WriteLine("Child Constructor.");
}
public static void Main()
{
ChildClass child = new ChildClass();
child.print();
}
}
Output:
Parent Constructor.
Child Constructor.
I'm a Parent Class.
说明
清单8-1演示了两个类的用法。上面的一个类名为ParentClass, main函数中用到的类名为ChildClass。要做的是创建一个使用父类ParentClass现有代码的子类ChildClass。
1.首先必须说明ParentClass是ChildClass的基类。
这是通过在ChildClass类中作出如下说明来完成的:"public class ChildClass : ParentClass"。在派生类标识符后面,用分号":" 来表明后面的标识符是基类。C#仅支持单一继承。因此,你只能指定一个基类。
2.ChildClass的功能几乎等同于ParentClass。
因此,也可以说ChildClass "就是" ParentClass。在ChildClass 的Main()方法中,调用print() 方法的结果,就验证这一点。该子类并没有自己的print()方法,它使用了ParentClass中的 print()方法。在输出结果中的第三行可以得到验证。
3.基类在派生类初始化之前自动进行初始化。
注意到清单8-1的输出结果。ParentClass 类的构造函数在ChildClass的构造函数之前执行。
2.清单 8-2. 派生类同基类进行通信: BaseTalk.cs
using System;
public class Parent
{
string parentString;
public Parent()
{
Console.WriteLine("Parent Constructor.");
}
public Parent(string myString)
{
parentString = myString;
Console.WriteLine(parentString);
}
public void print()
{
Console.WriteLine("I'm a Parent Class.");
}
}
public class Child : Parent
{
public Child() : base("From Derived")
{
Console.WriteLine("Child Constructor.");
}
public void print()
{
base.print();
Console.WriteLine("I'm a Child Class.");
}
public static void Main()
{
Child child = new Child();
child.print();
((Parent)child).print();
}
}
Output:
From Derived
Child Constructor.
I'm a Parent Class.
I'm a Child Class.
I'm a Parent Class.
说明
1.派生类在初始化的过程中可以同基类进行通信。
清单8-2演示了在子类的构造函数定义中是如何实现同基类通信的。分号":"和关键字base用来调用带有相应参数的基类的构造函数。输出结果中,第一行表明:基类的构造函数最先被调用,其实在参数是字符串"From Derived"。
2.有时,对于基类已有定义的方法,打算重新定义自己的实现。
Child类可以自己重新定义print()方法的实现。Child的print()方法覆盖了Parent中的 print 方法。结果是:除非经过特别指明,Parent类中的print方法不会被调用。
3.在Child类的 print() 方法中,我们特别指明:调用的是Parent类中的 print() 方法。
方法名前面为"base",一旦使用"base"关键字之后,你就可以访问基类的具有公有或者保护权限的成员。 Child类中的print()方法的执行结果出现上面的第三行和第四行。
4.访问基类成员的另外一种方法是:通过显式类型转换。
在Child类的Main()方法中的最后一条语句就是这么做的。记住:派生类是其基类的特例。这个事实告诉我们:可以在派生类中进行数据类型的转换,使其成为基类的一个实例。清单8-2的最后一行实际上执行了Parent类中的 print()方法。
小结
你已经了解了如何创建一个派生类及其基类。你可以对基类成员进行初始化,以及如何对方法进行隐式或者显式的调用。你也了解了派生类是其基类的一个特例。
第九课:多态性
本节课将介绍C#的多态性,其目的包括:
1.了解什么是多态性
2.如何定义一个虚方法
3.如何重载一个虚方法
4.如何在程序中运用多态性
面向对象程序设计中的另外一个重要概念是多态性。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。 可以把一组对象放到一个数组中,然后调用它们的方法,在这种场合下,多态性作用就体现出来了,这些对象不必是相同类型的对象。当然,如果它们都继承自某个类,你可以把这些派生类,都放到一个数组中。 如果这些对象都有同名方法,就可以调用每个对象的同名方法。本节课将向你介绍如何完成这些事情。
1.清单9-1. 带有虚方法的基类:DrawingObject.cs
using System;
public class DrawingObject
{
public virtual void Draw()
{
Console.WriteLine("I'm just a generic drawing object.");
}
}
说明
清单9-1 定义了DrawingObject类。这是个可以让其他对象继承的基类。该类有一个名为Draw()的方法。Draw()方法带有一个virtual修饰符,该修饰符表明:该基类的派生类可以重载该方法。DrawingObject类的 Draw()方法完成如下事情:输出语句"I'm just a generic drawing object."到控制台。
2.清单9-2. 带有重载方法的派生类:Line.cs, Circle.cs, and Square.cs
using System;
public class Line : DrawingObject
{
public override void Draw()
{
Console.WriteLine("I'm a Line.");
}
}
public class Circle : DrawingObject
{
public override void Draw()
{
Console.WriteLine("I'm a Circle.");
}
}
public class Square : DrawingObject
{
public override void Draw()
{
Console.WriteLine("I'm a Square.");
}
}
说明
清单9-2定义了三个类。这三个类都派生自DrawingObject类。每个类都有一个同名Draw()方法,这些Draw()方法中的每一个都有一个重载修饰符。重载修饰符可让该方法在运行时重载其基类的虚方法,实现这个功能的条件是:通过基类类型的指针变量来引用该类。
3.清单9-3. 实现多态性的程序:DrawDemo.cs
using System;
public class DrawDemo
{
public static int Main(string[] args)
{
DrawingObject[] dObj = new DrawingObject[4];
dObj[0] = new Line();
dObj[1] = new Circle();
dObj[2] = new Square();
dObj[3] = new DrawingObject();
foreach (DrawingObject drawObj in dObj)
{
drawObj.Draw();
}
return 0;
}
}
说明
清单9-3演示了多态性的实现,该程序使用了在清单 9-1 和清单9-2中定义的类。在DrawDemo类中的Main()方法中,创建了一个数组, 数组元素是DrawingObject 类的对象。该数组名为dObj,是由四个DrawingObject类型的对象组成。
接下来, 初始化dObj数组, 由于Line, Circle和Square类都是DrawingObject类的派生类,所以这些类可以作为dObj数组元素的类型。 如果C#没有这种功能,你得为每个类创建一个数组。继承的性质可以让派生对象当作基类成员一样用,这样就节省了编程工作量。
一旦数组初始化之后,接着是执行foreach循环,寻找数组中的每个元素。在每次循环中, dObj 数组的每个元素(对象)调用其Draw()方法。多态性体现在:在运行时,各自调用每个对象的Draw()方法。尽管dObj 数组中的引用对象类型是DrawingObject,这并不影响派生类重载DrawingObject 类的虚方法Draw()。 在dObj 数组中,通过指向DrawingObject 基类的指针来调用派生类中的重载的Draw()方法。
输出结果是:
I'm a Line.
I'm a Circle.
I'm a Square.
I'm just a generic drawing object.
在DrawDemo 程序中,调用了每个派生类的重载的Draw()方法。 最后一行中,执行的是DrawingObject类的虚方法Draw()。这是因为运行到最后,数组的第四个元素是DrawingObject类的对象。
小结
现在对多态性有所了解之后,你可以在派生类中,实现一个重载基类虚方法的方法。虚方法和重载的派生类方法之间的关系就体现出C#的多态性。
第十课:属性
本节课将介绍C#的属性,其目的包括:
1.理解什么是属性
2.如何实现属性
3.创建一个只读属性
4.创建一个只写属性
属性是C#中独具特色的新功能。通过属性来读写类中的域,这具有一定的保护功能。在其它语言中,这是通过实现特定的getter和setter方法来实现的。C#的属性具有保护功能,可以让你就象访问域一样访问属性。要了解属性的用法,我们先来看看如何用传统的方法对域进行封装。
1.清单 10-1. 传统的访问类的域的例子:Accessors.cs
using System;
public class PropertyHolder
{
private int someProperty = 0;
public int getSomeProperty()
{
return someProperty;
}
public void setSomeProperty(int propValue)
{
someProperty = propValue;
}
}
public class PropertyTester
{
public static int Main(string[] args)
{
PropertyHolder propHold = new PropertyHolder();
propHold.setSomeProperty(5);
Console.WriteLine("Property Value: {0}", propHold.getSomeProperty());
return 0;
}
}
说明
1.清单 10-1 演示了用传统方法访问类的域的例子。
PropertyHolder类有个我们感兴趣的域someProperty, PropertyHolder类带有两个方法:getSomeProperty和setSomeProperty。getSomeProperty方法返回someProperty域的值。SetSomeProperty方法设置域someProperty的值。
2.类PropertyTester使用类PropertyHolder中的方法来获取someProperty域的值。
Main方法中新创建了一个PropertyHolder对象,之后通过使用setSomeProperty方法,调用propHold对象的setSomeProperty方法,设置其值为5。之后,调用Console.WriteLine方法输出属性值。对propHold对象的getSomeProperty的调用,是用来获取属性值的。它输出"Property Value: 5"到控制台。
3.这种传统的访问域的信息的方法是很好的,因为它支持面向对象的封装的概念。
如果在对域someProperty的实现中,域的类型从int 类型变为byte类型,上述的方法仍然适用。现在,如果采用属性的话,其实现会做得更为平滑。
2.清单 10-2. 使用属性访问类的域:Properties.cs
using System;
public class PropertyHolder
{
private int someProperty = 0;
public int SomeProperty
{
get
{
return someProperty;
}
set
{
someProperty = value;
}
}
}
public class PropertyTester
{
public static int Main(string[] args)
{
PropertyHolder propHold = new PropertyHolder();
propHold.SomeProperty = 5;
Console.WriteLine("Property Value: {0}", propHold.SomeProperty);
return 0;
}
}
说明
1.清单 10-2 演示了如何创建和使用属性。
PropertyHolder类中有个"SomeProperty" 属性的实现。注意:属性名的首字母必须大写,这是属性名"SomeProperty"和域名"someProperty"的唯一区别。属性有两种访问操作:get和set。Get访问操作返回的是someProperty域的值。Set访问操作是设置someProperty域的值,其值为"value"的内容。Set访问符号后面的"value"是C#中的保留字。通常,在其他场合下使用"value"关键字会出错。
2.PropertyTester 类使用PropertyHolder类中的SomeProperty属性。
在Main方法的第一行中, 创建了PropertyHolder对象propHold。之后,把propHold对象的 someProperty 域的值设置为5,很简单,就象对域赋值一样,给属性赋值。
3.Console.WriteLine方法输出 propHold对象的someProperty域的值。
这是通过使用propHold对象的SomeProperty属性来完成的。很简单,就象对域赋值一样,赋值给属性。属性可以设置为只读的,这可以通过在属性的实现中只设一个get访问符号来实现。
3.清单 10-3. 只读属性: ReadOnlyProperty.cs
using System;
public class PropertyHolder
{
private int someProperty = 0;
public PropertyHolder(int propVal)
{
someProperty = propVal;
}
public int SomeProperty
{
get
{
return someProperty;
}
}
}
public class PropertyTester
{
public static int Main(string[] args)
{
PropertyHolder propHold = new PropertyHolder(5);
Console.WriteLine("Property Value: {0}", propHold.SomeProperty);
return 0;
}
}
说明
1.清单10-3 演示了如何实现只读属性。
PropertyHolder类中,SomeProperty 属性只有一个get访问操作,没有用到set访问操作。PropertyHolder类中还有个接受整型参数的构造函数。
2.在PropertyTester类的Main方法中,创建了新名为propHold的PropertyHolder类的对象。
propHold对象在实例化时,调用了带参数的PropertyHolder构造函数。在本例中,参数值为5,这对propHold 对象的someProperty域的值进行了初始化。
3.因为PropertyHolder 类的SomeProperty属性是只读的,所以没有其他的方法来设置someProperty域的值。
如果你插入了"propHold.SomeProperty = 7"语句到程序清单中,该程序编译将不会通过,因为SomeProperty是只读属性。在Console.WriteLine 方法中使用SomeProperty属性时,程序执行正常。这是因为该方法调用了SomeProperty属性的get访问操作,这是个只读操作。
4.清单 10-4. 只写属性: WriteOnlyProperty.cs
using System;
public class PropertyHolder
{
private int someProperty = 0;
public int SomeProperty
{
set
{
someProperty = value;
Console.WriteLine("someProperty is equal to {0}", someProperty);
}
}
}
public class PropertyTester
{
public static int Main(string[] args)
{
PropertyHolder propHold = new PropertyHolder();
propHold.SomeProperty = 5;
return 0;
}
}
说明
1.清单 10-4 演示了如何创建和使用只写属性。
这一次,在PropertyHolder类中的SomeProperty属性中,去掉了get访问操作,而加上了set访问操作。其功能是输出someProperty域的值。
2.在PropertyTester 类中的Main方法中,用缺省的构造函数对PropertyTester类进行初始化。
之后,使用propHold 对象的SomeProperty属性,设置该域的值为5。这就调用了propHold 对象的set访问操作, 把someProperty 域的值设置为5,最后,把"someProperty is equal to 5"的信息输出到控制台。
小结
现在,你已经了解了什么是属性,以及属性的使用方法,你也了解了使用属性和使用传统的类的方法之间的区别。属性可以是只读的,也可以是只写的,每种场合下的使用方法,你都有所了解。
第十一课:索引指示器
本节课将介绍C#的索引指示器,其目的包括:
1.了解什么是索引指示器
2.如何实现索引指示器
3.重载索引指示器
4.了解如何实现多参数的索引指示器
索引指示器并不难使用。它们的用法跟数组相同。在一个类内部,你可以按照你的意愿来管理一组数据的集合。这些对象可以是类成员的有限集合,也可以是另外一个数组,或者是一些复杂的数据结构。不考虑类的内部实现,其数据可以通过使用索引指示器来获得。如下是一个例子:
1.清单 11-1. 索引指示器的例子:IntIndexer.cs
using System;
///
/// A simple indexer example.
///
class IntIndexer
{
private string[] myData;
public IntIndexer(int size)
{
myData = new string[size];
for (int i=0; i < size; i++)
{
myData = "empty";
}
}
public string this[int pos]
{
get
{
return myData[pos];
}
set
{
myData[pos] = value;
}
}
static void Main(string[] args)
{
int size = 10;
IntIndexer myInd = new IntIndexer(size);
myInd[9] = "Some Value";
myInd[3] = "Another Value";
myInd[5] = "Any Value";
Console.WriteLine("\nIndexer Output\n");
for (int i=0; i < size; i++)
{
Console.WriteLine("myInd[{0}]: {1}", i, myInd);
}
}
}
说明
1.清单 11-1演示了如何实现一个索引指示器, IntIndexer类有个名为myData的字符串数组,该数组是私有成员,因而其外部成员是看不见的。该数组是在构造函数中进行初始化的,该构造函数带有一个整型size参数,用来初始化myData数组,初始化时 把单词"empty"作为每个数组元素的值。
2.IntIndexer类的下一成员是索引指示器(Indexer),由关键字this和方括号[int pos]标识出来。该成员带有一个位置参数pos。正如你已经猜测到,Indexer的实现同属性一样。Indexer有get 和set访问操作,就同属性中的用法一样。索引指示器(indexer)返回一个字符串,在定义索引指示器时,string这个类型名标志着其返回类型为字符串类型。
3.Main()方法完成如下事情:初始化一个新的IntIndexer对象,添加一些值,并且打印出结果。其输出结果如下:
Indexer Output
myInd[0]: empty
myInd[1]: empty
myInd[2]: empty
myInd[3]: Another Value
myInd[4]: empty
myInd[5]: Any Value
myInd[6]: empty
myInd[7]: empty
myInd[8]: empty
myInd[9]: Some Value
4.在不少程序语言中,通常都是使用整数作为下标来访问作为数组元素的,但C#的索引指示器不仅能够做到这一点,而且还能够更进一步。 定义索引指示器时,可以带有多个参数,每个参数的类型可以不同。添加的参数由逗号隔开,同方法中的的参数表一样。索引指示器的合法的参数类型包括:整型,枚举类型和字符串。另外,索引指示器也可以被重载。在清单 11-2中,我们修改了前面的程序,以便用来重载索引指示器 ,从而可以接受不同类型的参数。
2.清单 11-2. 重载的索引指示器: OvrIndexer.cs
using System;
///
/// Implements overloaded indexers.
///
class OvrIndexer
{
private string[] myData;
private int arrSize;
public OvrIndexer(int size)
{
arrSize = size;
myData = new string[size];
for (int i=0; i < size; i++)
{
myData = "empty";
}
}
public string this[int pos]
{
get
{
return myData[pos];
}
set
{
myData[pos] = value;
}
}
public string this[string data]
{
get
{
int count = 0;
for (int i=0; i < arrSize; i++)
{
if (myData == data)
{
count++;
}
}
return count.ToString();
}
set
{
for (int i=0; i < arrSize; i++)
{
if (myData == data)
{
myData = value;
}
}
}
}
static void Main(string[] args)
{
int size = 10;
OvrIndexer myInd = new OvrIndexer(size);
myInd[9] = "Some Value";
myInd[3] = "Another Value";
myInd[5] = "Any Value";
myInd["empty"] = "no value";
Console.WriteLine("\nIndexer Output\n");
for (int i=0; i < size; i++)
{
Console.WriteLine("myInd[{0}]: {1}", i, myInd);
}
Console.WriteLine("\nNumber of \"no value\" entries: {0}", myInd["no value"]);
}
}
说明
1.清单 11-2 演示了如何重载索引指示器。
带有整型参数pos的第一个索引指示器同清单11-1中的一样,但是,该程序中有个带有字符串参数的新的索引指示器。对于这个新的索引指示器来说,其get操作返回的是同参数值data相匹配的成员的个数。 Set操作把数组中同参数值匹配的元素值该变为value值。
2.在清单11-2的Main()方法中,演示了重载的索引指示器,它接受字符串参数。
该重载的索引指示器调用了set操作,通过使用下列命令: myInd["empty"] = "no value"; set操作把"no value"值赋给myInd 类中所有的值为"empty"的成员。 myInd类的每个成员都已经输出之后,就把最后一个数据输出到控制台,该数据统计数组成员值为"no value"的个数。 使用如下命令:myInd["no value"],就可调用get操作。输出结果如下:
Indexer Output
myInd[0]: no value
myInd[1]: no value
myInd[2]: no value
myInd[3]: Another Value
myInd[4]: no value
myInd[5]: Any Value
myInd[6]: no value
myInd[7]: no value
myInd[8]: no value
myInd[9]: Some Value
Number of "no value" entries: 7
3.在清单 11-2中,两个索引指示器共处在同一个类中, 这是可以的,因为它们有不同的特征。
一个索引指示器的特征是通过索引指示器参数表中的参数个数和类型表现出来的。类能够辨别出其特征,并调用相应的索引指示器。带有多个参数的索引指示器可以用如下格式来实现:
public object this[int param1, ..., int paramN]
{
get
{
// process and return some class data
}
set
{
// process and assign some class data
}
}
小结
现在你已经了解了索引指示器是用来做什么的,以及其用法。如同数组的用法一样,你可以创建索引指示器来访问类的成员。本文也提到了索引指示器的重载和多参数索引指示器。
文章来源:http://www.codeguru.cn/ShowThread.aspx?PostID=1085