第2章 C#语言基础
C#是一门面向对象的高级编程语言,它是构成.NET应用程序的重要基础。在了解具体的.NET应用程序设计之前,读者有必要先学习一下C#的语言基础。本章将重点介绍该语言的基础部分。
2.1 类型
C#类型是C#语言的重要基础,所有的C#类型都从基本类型继承而来。C#中有两种基本类型:值类型和引用类型。
说明:值类型与引用类型的转换可以通过装箱和拆箱来完成。
2.1.1 基类型
在C#中,一个类型可以从另外一个类型继承。而基类型就是这些类型的最顶级继承的类型。虽然结构与类不一样,可是他们的基类型都是对象(Object),C#中所有类型的基类型都是Object,即所有类型都是从Object继承下来的。
2.1.2 使用值类型和引用类型
值类型的变量直接包含它们的数据,而引用类型的变量存储对数据的引用,后者称为对象。对于引用类型,两个变量可能引用同一个对象,因此对一个变量的操作可能影响另一个变量所引用的对象。对于值类型,每个变量都有它们自己的数据副本,因此对一个变量的操作不可能影响另一个变量。
值类型主要包括简单类型、整型、浮点型、decimal类型、bool类型和枚举类型。下面分别描述:
● 简单类型:C# 提供称为简单类型的预定义结构类型集。简单类型通过保留字标识,而这些保留字只是System命名空间中预定义结构类型的别名,具体的简单类型如表2-1所示。
表2-1 C#简单类型
● 整型:C#支持9种整型:sbyte、byte、short、ushort、int、uint、long、ulong和char。整型的大小和取值范围如表2-2所示。
表2-2 C#整型
● 浮点型:C#支持两种浮点型——float和double。float和double类型用32位单精度和64位双精度格式来表示,float类型可表示精度为7位、大约在1.5 ×10-45~3.4 × 1038的范围内的值。double类型可表示精度为15位或16位、大约在5.0 ×10-324~1.7×10308的范围内的值。
● decimal类型:decimal类型是128位的数据类型,适合用于财务计算和货币计算。decimal类型可以表示具有28或29个有效数字、大约在1.0 ×10-28~7.9 ×1028范围内的值。
● bool类型:bool类型表示布尔逻辑量。bool类型的可能值为true和false。
● 枚举类型:枚举类型是具有命名常量的独特的类型。每个枚举类型都有一个基础类型,该基础类型必须为byte、sbyte、short、ushort、int、uint、long或ulong。
引用类型包括类类型、对象类型、string类型、接口类型、数组类型和委托类型。下面分别介绍这几种引用类型。
● 类类型:类类型是面向对象中的基础类型,它用来表示一个对象类。类类型包括方法、属性、事件、索引器、运算符、实例构造函数、析构函数和静态构造函数。
● 对象类型:对象类型(Object类型)是所有其他类型的基类。C#中的每种类型都是直接或间接从对象类型派生的。
● string类型:string类型是直接从object继承的密封类类型。string类的实例表示Unicode字符串。string类型的值可以写为字符串。
● 接口类型:接口类型用于类的继承。继承接口的类,必须实现接口中定义的相关内容。一个接口可以从多个基接口继承,而一个类或结构可以实现多个接口。
● 数组类型:数组是存放数据的容器,可以在数组中指定该数组大小,同时指定该数组对应的数据类型。数组中包含的变量具有相同的类型,该类型称为数组的元素类型。
● 委托类型:委托是一种数据结构,它引用一个或多个方法,对于实例方法,还引用这些方法所对应的对象实例。
2.1.3 装箱与拆箱
装箱就是把简单类型转换为引用类型,而拆箱是一个反向过程,即将引用类型转换回值类型。装箱与拆箱使得值类型与引用类型之间的转换变成了可能。
装箱转换实际上是允许将值类型隐式转换为引用类型,具体的转换语法如下。
int i = 123; object o = (object) i;
拆箱与装箱的概念正好相反,拆箱转换允许将引用类型显式转换为值类型,具体的转换语法如下。
object o = 123; i = (int) o;
说明:装箱与拆箱的过程只是为了说明两种类型之间在.NET框架下的一种转换原理,在真正的程序开发中,读者可以根据实际的业务场景选择使用不同方法来实现类型的转换。
2.2 语句和运算符
语句和运算符组成了编程语言中的程序逻辑,C#也不例外,本节介绍C#中经常使用的几种语句,以及在程序中对运算符的使用方法。
2.2.1 选择语句
选择语句指的是switch语句。它由switch、case、default、break等关键词组成。switch语句选择一个要执行的语句列表,此列表具有一个相关联的switch标签,它对应于switch表达式的值。
switch语句的类型由switch表达式确定。这种类型可以为sbyte、byte、short、ushort、int、uint、long、ulong、char、string或enum,每个case标签常量表达式的值必须属于上述类型之一。
技巧:选择语句常用于在多种选择条件并列的时候,只选择一种后续操作的情况。
switch语句按下列规则执行:计算switch表达式并将其转换为主导类型。如果在该switch语句的case标签中,有一个指定的常量恰好等于switch表达式的值,控制将转到匹配的case标签后的语句列表。如果在该switch语句的case标签中,指定的常量都不等于switch表达式的值,且如果存在一个default标签,则控制将转到default标签后的语句列表。如果在该switch语句的case标签中,指定的常量都不等于switch表达式的值,且不存在default标签,则控制将转到switch语句的结束点。下面通过一个实例介绍如何使用switch选择语句。
【本节示例参考:\Source Code\C02\SwitchClause】
选择语句代码示例:Program.cs
static void Main(string[] args) { //输出颜色选择提示信息 Console.WriteLine("请选择颜色代码:"); Console.WriteLine("1-绿色"); Console.WriteLine("2-红色"); Console.WriteLine("3-蓝色"); //读取用户输入的颜色代码 string input = ""; while (input != "0") { input = Console.ReadLine(); //通过switch语句判断颜色的选择代码 switch (input) { //用户输入1 case "1": Console.WriteLine("你选择了绿色"); break; //用户输入2 case "2": Console.WriteLine("你选择了红色"); break; //用户输入3 case "3": Console.WriteLine("你选择了蓝色"); break; //用户输入其他值 default: Console.WriteLine("选择的颜色不正确"); break; } } Console.ReadLine(); }
实例中,程序根据用户输入string类型的颜色代码,进行switch判断,然后根据输入数字的不同,进入不同的case语句进行处理,将不同数字对应的颜色显示到控制台中。如果输入的数字代码不在case对应的选择内容中,则进入default语句,提示用户选择颜色不正确。实例的运行结果如图2-1所示。
图2-1 选择语句实例运行结果
2.2.2 循环语句
循环语句是运行在某种条件之下的,循环执行某段程序的语句。除非不满足循环条件或者使用了循环跳出的语句,否则循环语句将不停地运行下去。C#中包括以下几种常用的循环语句:for语句、while语句、foreach语句等。在这些循环语句的循环体内,也可以使用break语句或者continue语句来停止循环。break语句表示跳出循环语句,continue语句表示跳出本次循环,进入下一个循环语句。
技巧:循环语句多用在为了实现循环执行某操作的场景,比如对集合元素的遍历、计数器等操作。
1.for语句
for语句执行的条件是满足for循环的条件表达式。for循环表达式包括3个部分,分别是循环的初始值、循环的条件、循环变量的自增设定。3个条件不是都需要设定,可以根据程序的实际情况省略。比如下面代码:
for(int n=0;n<100;n++) { //循环体代码 }
这个循环表示在n小于100的情况下,循环体中的代码将被重复执行。
2.while语句
while语句按照条件语句的结果执行一个循环语句零次或多次。while语句首先计算布尔表达式,如果布尔表达式返回的结果为true,控制将转到循环语句中。当代码运行到循环语句的结束点时,将重新转到while语句的开头。如果布尔表达式的结果为false,代码将转到while语句的结束点。比如下面代码:
int n=0; while(n<100) { //循环体代码 n++ }
代码中,while语句将被不断执行,直至n等于100的时候,才结束while循环。
3.foreach语句
foreach语句用于枚举一个集合的元素,并对该集合中的每个元素执行一次相关的循环语句。foreach语句的in之后的参数类型必须是集合类型,且必须有一个从该集合的元素类型到迭代变量类型的显式转换。比如下面代码:
string ] args = new string ["1","2","3","4","5"] foreach (string s in args) { Console.WriteLine(s); }
这段代码表示,foreach循环会自动遍历定义的字符串数组中的各个元素。
下面通过一个实例介绍如何使用for循环语句。
【本节示例参考:\Source Code\C02\ ForClause】
for循环语句代码示例:Program.cs
static void Main(string[] args) static void Main(string[] args) { //使用for语句,构造5次循环 for (int i = 1; i <= 5; i++) { //将每次循环的计数变量输出到控制台 Console.WriteLine(i); } Console.ReadLine(); }
实例中,使用for语句将每次循环的整数值i输出到控制台中。实例的运行结果如图2-2所示。
图2-2 for循环语句实例运行结果
2.2.3 使用运算符
运算符是组成表达式的基础,由运算符和操作数可以完整地表示一个表达式。运算符包括算术运算符、赋值运算符和逻辑运算符。算术运算符主要用于数学中的计算,如加、减、乘、除、余等。
技巧:运算符除了完成数学逻辑运算之外,还具有流程控制的功能,比如在循环语句中的自增或者自减操作。
● 乘法运算符:乘法运算符用来计算两个值的积。乘法运算符的示例如下所示。
int a=200; int b=10; int c=a*b;
● 除法运算符:除法运算符用来计算两个值的商。除法运算符的示例如下所示。
int a=200; int b=10; int c=a/b;
● 余数运算符:余数运算符用来计算两个数的余数。余数运算符的示例如下所示。
int a=200; int b=10; int c=a%b;
● 加法运算符:加法运算符对于数值和枚举类型,计算两个操作数的和。当一个或两个操作数为string类型时,加法运算符把两个操作数的字符串表示形式串联起来。加法运算符的示例如下所示。
int a=200; int b=10; int c=a+b; string a="hello"; string b="C#"; string c=a+b;
● 减法运算符:减法运算符用来计算两个数相减之后的差值。减法运算符的示例如下所示。
int a=200; int b=10; int c=a-b;
赋值运算符主要用于赋值,赋值运算符的左操作数必须是属于变量、属性访问、索引器访问或事件访问类别的表达式,它将右操作数的值赋予左操作数给定的变量、属性或索引器元素。
“&”、“^”和“|”运算符称为逻辑运算符。“&”运算符计算两个操作数的按位逻辑与;“|”运算符计算两个操作数的按位逻辑或;“^”运算符计算两个操作数的按位逻辑异或。
2.2.4 重载运算符
重载运算符是指对已有的运算符的实现功能进行重新定义,使之在特定的对象处理中实现定义的逻辑功能。重载运算符允许使用“+”、“-”、“=”和“!=”等运算符合并和比较类型。通过在类型中添加重载运算符,开发人员可以像使用基本类型一样使用该类型。下面通过一个实例介绍,如何在程序中实现运算符的重载。
技巧:重载运算符主要是为了实现在不同业务场景中运算符的业务涵义。比如对于字符串对象,加号运算符就是指将两个字符串连接,并不是对字符串进行加法操作。
【本节示例参考:\Source Code\C02\ OperatorOverride】
重载运算符代码示例:Program.cs
class Program { static void Main(string[] args) { int tempCount = 10; Number num = new Number(); Number sum = new Number(); Number sub = new Number(); Console.WriteLine("原始数字为:--> {0}", num.count); //使用加法操作符重载 sum = sum + tempCount; //使用减法操作符重载 sub = sub - tempCount; Console.WriteLine("加上10之后的数字为-->{0}", sum.count); Console.WriteLine("减去10之后的数字为-->{0}", sub.count); Console.ReadLine(); } } class Number { public int count; public Number() { count = 20; } //定义加法的操作符重载 public static Number operator +(Number n, int num) { int tempCount = n.count + num; Number num1 = new Number(); num1.count = tempCount; return num1; } //定义减法的操作符重载 public static Number operator -(Number n, int num) { int tempCount = n.count - num; Number num1 = new Number(); num1.count = tempCount; return num1; } }
实例的运行结果如图2-3所示。
图2-3 操作符重载实例运行结果
程序代码说明如下:
(1)代码中定义了两个用于操作符重载的方法,分别是“+”“-”运算符重载方法,该重载方法表示在该类的实例中如果对实例采用了这两种符号操作,则按代码中定义的过程进行处理。
(2)在类定义中,声明了一个公有的计数器成员count,重载中的运算是基于该公共成员进行加减操作的。
该程序说明,操作符重载之后,表示该符号已经失去了原有的意义,取而代之的是类中定义的操作符进行的运算。
2.2.5 运算符转换
运算符转换是指把指定类型的值通过已经定义好的运算符转换方法,进行类型的强制转换。运算符转换分为使用关键字implicit的隐式转换、以及使用关键字explicit的显式转换。
下面通过一个实例介绍如何在程序中实现运算符转换。
【本节示例参考:\Source Code\C02\ ExplicitConvert】
运算符转换代码示例:Program.cs
class Program { static void Main(string[] args) { try { byte b = 3; ConvertObject d = (ConvertObject)b; // 显式转换 } catch (System.Exception e) { System.Console.WriteLine("{0} 程序异常.", e); } Console.ReadLine(); } } struct ConvertObject { byte value; public ConvertObject(byte value) //构造函数 { if (value > 9) { throw new System.ArgumentException(); } this.value = value; } public static explicit operator ConvertObject(byte b) // 定义显式操作 符转换 { ConvertObject d = new ConvertObject(b); // 显式转换 System.Console.WriteLine("已经转换"); return d; } }
程序说明:
(1)在结构体ConvertObject中,使用explicit关键字定义了ConvertObject方法,该方法可以实现在对象转换时进行的代码逻辑处理。而ConvertObject构造函数则实现了真正的转换操作。
(2)运算符转换同一般的类型转换一样,可以直接通过(ConvertObject)b的语法进行类型的显式转换。
2.3 字符串处理
字符串是程序设计中的一个重要的变量类型,通过字符串的处理,很多重要的逻辑可以基于字符串来转换处理,使程序逻辑变得可读性比较强。本节介绍如何在程序中使用字符串的相关处理。
2.3.1 使用string和StringBuilder
string字符串是Unicode字符的有序集合,用于表示文本。string对象是System.Char对象的有序集合,用于表示字符串。StringBuilder表示值为可变字符序列的类似字符串的对象。之所以说值是可变的,是因为通过追加、移除、替换或插入字符创建它之后,还可以对其进行修改。
注意:鉴于string的缺陷,在操作长度比较大的字符串的时候,使用StringBuilder会比使用string处理性能提高很多。
下面通过一个实例介绍,如何在程序中使用string和StringBuilder。
【本节示例参考:\Source Code\C02\ StringTypeCommon】
String和StringBuilder代码示例:Program.cs
static void Main(string[] args) { string str = " Hello world "; Console.WriteLine("string对象处理的相关方法"); Console.WriteLine("显示整个字符串:{0}", str); //获取字符串的指定位置字符: Console.WriteLine("显示字符串第一个字符:{0}", str[0]); Console.WriteLine("显示字符串第二个字符:{0}", str[1]); //将指定字符串插入到指定位置: Console.WriteLine("在指定位置插入字符串:{0}", str.Insert(6," C#")); //获取字符串长度 Console.WriteLine("获取字符串长度:{0}", str.Length); //删除字符串空格 Console.WriteLine("删除字符串空格:{0}", str.Trim()); //字符串复制 Console.WriteLine("字符串复制:{0}", str.Clone().ToString()); //将整个字符串全部大写 Console.WriteLine("大写整个字符串:{0}", str.ToUpper()); //将整个字符串全部小写 Console.WriteLine("小写整个字符串:{0}", str.ToLower()); //截取指定长度的部分字符串 Console.WriteLine("截取部分字符串:{0}", str.Substring(0,7)); Console.WriteLine("StringBuilder对象处理的相关方法"); StringBuilder sb = new StringBuilder("Hello world "); Console.WriteLine("显示整个StringBuilder内容:{0}", sb.ToString()); //在StringBuilder末端追加文本 Console.WriteLine("在StringBuilder末端追加文本:{0}", sb.Append("!!!")); //在StringBuilder指定位置追加文本 Console.WriteLine("在StringBuilder指定位置追加文本:{0}", sb.Insert(6, "dear ")); //获取StringBuilder最大文本数量 Console.WriteLine("获取StringBuilder最大文本数量:{0}", sb.MaxCapacity); Console.ReadLine(); }
实例的运行结果如图2-4所示。
图2-4 String和StringBuilder操作实例运行结果
程序说明:
(1)在主函数(即前文介绍过的Main函数)中,首先定义了一个待操作的字符串变量,然后通过字符串的属性或者相关方法来处理该字符串的内容。
(2)主函数的后续部分定义了一个StringBuilder对象,也同样调用了该实例的相关属性和方法来实现对字符串对象的处理。
2.3.2 字符串的格式化
字符串的格式化是指通过指定的格式将指定的值替换到字符串中。字符串的格式化包括字符串类型格式化、数字类型格式化、日期类型格式化等。字符串类型本身的格式化是通过string.format方法来实现的。如下代码所示。
string.format("{0} {1}","hello","world")
技巧:字符串的连接,是指可以通过对多个字符串进行连接的处理,但程序的可读性以及性能都不理想,但使用了格式化字符串的方式之后,可以直接在待格式化的字符串中完整地看到字符串的内容,需要替换的内容也会清晰可见。
代码中,使用了string的format方法,该方法具有多个重载方法。本方法中的第一个参数表示带有参数化文本的字符串,其中的“{0}”表示文本的该位置将会由后续参数的内容所替换。同理“{1}”也表示将内容world替换到该位置。该段代码执行格式化之后的内容为“hello world”。
数字类型的格式化包括标准的数字格式字符串和自定义的格式字符串。标准的数字格式化是指按照区域特性定义的对数字格式的显示。通常情况下,它会通过已经定义好的特殊标记进行格式的说明。如下实例代码。
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-us"); //定义当前进程为美国的区域 double MyDouble = 123456789; Console.WriteLine(MyDouble.ToString("C")); Console.WriteLine(MyDouble.ToString("E")); Console.WriteLine(MyDouble.ToString("P")); Console.WriteLine(MyDouble.ToString("N")); Console.WriteLine(MyDouble.ToString("F"));
运行之后的格式化结果如下:
$123,456,789.00 1.234568E+008 12,345,678,900.00% 123,456,789.00 123456789.00
其中的“C”、“E”等代码就是标准的格式代码,所有的数字格式代码及其含义如表2-3所示。
表2-3 标准数字格式代码含义
除了进行标准的数字格式化之外,C#还支持自定义的格式。如下代码所示。
Double myDouble = 1234567890; String myString = myDouble.ToString( "(###) ### - ####" );
输出的字符串结果为
"(123) 456-7890"
可以看出,通过自定义的格式,数字被有效地转换成一个电话号码的字符串。
与数字类型的格式化相似,日期类型的格式化也包括标准格式化和自定义格式化两部分。由于基本使用方式与数字非常相似,日期格式化部分就不做过多介绍,下面只列出参考代码实例以及日期标准格式对应的格式代码表说明。
DateTime dt = DateTime.Now; //标准日期格式 Console.WriteLine( dt.ToString("d") ); Console.WriteLine( dt.ToString("m") ); //自定义格式 Console.WriteLine( dt.ToString("yyyy-MM-dd") );
日期格式代码及其含义如表2-4所示。
表2-4 标准日期格式代码含义
2.3.3 对字符串进行编码
由于各个区域文化与字符的差异很大,所以在C#中提供了对字符串进行编码的操作,其目的就是为了满足对字符串随着不同地域进行不同的结果处理。编码是一个将一组Unicode字符转换为一个字节序列的过程。解码是一个反向操作过程,即将一个编码字节序列转换为一组Unicode字符。
Unicode标准为所有支持脚本中的每个字符分配一个码位。Unicode转换格式(UTF)是一种码位编码方式。
● UTF-8:它将每个码位表示为一个由1~4个字节组成的序列。
● UTF-16:它将每个码位表示为一个由1~2个16位整数组成的序列。
● UTF-32:它将每个码位表示为一个32位整数。
下面以UTF-8为例,介绍如何实现字符串的编码过程。
【本节示例参考:\Source Code\C02\ UTF8Encoding】
字符串编码代码示例:Program.cs
static void Main(string[] args) static void Main(string[] args) { // 创建UTF-8编码 UTF8Encoding utf8 = new UTF8Encoding(); // 定义包含编码的字符串 String unicodeString = "这个字符串包含了两个8位的编码 " + "Pi (\u03a0) 、 Sigma (\u03a3)."; Console.WriteLine("原始字符串:"); Console.WriteLine(unicodeString); // 编码 Byte[] encodedBytes = utf8.GetBytes(unicodeString); Console.WriteLine(); Console.WriteLine("编码:"); foreach (Byte b in encodedBytes) { Console.Write("[{0}]", b); } Console.WriteLine(); // 解码 String decodedString = utf8.GetString(encodedBytes); Console.WriteLine(); Console.WriteLine("解码:"); Console.WriteLine(decodedString); Console.ReadLine(); }
实例的运行结果如图2-5所示。
图2-5 字符串编码实例运行结果
程序说明:
(1)程序的主函数中,首先定义了一个UTF8Encoding的对象,然后定义了一个包含UTF8Encoding格式的字符串,该字符串中包含了特殊的编码字符串。
(2)调用UTF8Encoding对象的GetBytes方法实现字符串编码操作,然后将编码转换的字节信息显示到控制台。
(3)调用UTF8Encoding对象的GetString方法实现字节的解码操作。最终将界面的字符串信息显示到控制台。
编码实际是将字符串转变成字节信息,而解码是将字节信息恢复成字符串。
2.4 类和结构
类和结构是C#面向语言的重要对象形式,编程中可以通过类和结构定义对象,从而实现对象的操作以及对象间的引用。
2.4.1 定义类和结构
类是一种数据结构,它可以包含数据成员、函数成员(方法、属性、事件、索引器、运算符、实例构造函数、静态构造函数和析构函数)以及嵌套类型。结构与类很相似,都表示可以包含数据成员和函数成员的数据结构。但与类不同,结构是一种值类型,并且不需要堆分配。
类通过访问级别来定义该类被访问的限制,访问级别包括如下关键字:public、protected、internal、protected internal、private、abstract、sealed。各个访问级别的说明如下表2.5所示。
表2-5 类的访问级别说明
下面的代码中定义了一个Person类。
public class Person { // 字段 public string name; // 构造函数 public Person() { name = "nill"; } // 方法 public void SetName(string inputName) { name = inputName; } }
结构声明的修饰符与类声明的修饰符具有相同的意义,结构声明包括以下几种修饰符:new、public、protected、internal、private。
下面的代码定义了一个Point的结构体。
struct Point { public int x, y; public Point(int x, int y) { this.x = x; this.y = y; } }
2.4.2 定义属性
属性是类中字段和方法的结合体,通过定义属性,调用该类的时候,可以直接对该类的属性进行读写操作。
说明:属性的定义通过get和set关键字来实现。get关键字用来定义读取该属性时的操作,而set关键字用来定义设置该关键字的操作。如果一个属性同时具备了get和set操作,则该属性为读写性质的属性;如果只有set操作,则该属性为只写属性;同理如果只有get操作,则该属性为只读属性。
下面的代码定义了一个类的ID属性,该属性为读写属性,外部可以直接对其属性进行操作。
class Test { private string id;//私有成员 public string ID { //公有读属性 get { return id} } public string ID{//公有写属性 set { id= value; } } }
2.4.3 定义索引器
索引器是为了对类、接口和结构进行类似数组的索引,比如对类中定义某一位置的值进行赋值或者读取的操作,都可以通过索引器的定义来实现。在类与接口的声明索引器需要使用this关键字。下面通过一个实例来介绍如何通过程序实现索引器的定义以及使用。
【本节示例参考:\Source Code\C02\ IndexDemo】
索引器的定义与使用代码示例:Program.cs
class Program { static void Main(string[] args) { Indexer test = new Indexer(); // 调用赋值索引器,对数组的0和9位置的元素进行赋值 test[0] = 1111; test[9] = 2222; for (int i = 0; i <= 10; i++) { System.Console.WriteLine("元素 #{0} = {1}", i, test[i]); } Console.ReadLine(); } } class Indexer { private int[] arr = new int[100]; public int this[int index] // 索引器声明 { get { // 检查索引器的索引值. if (index < 0 || index >= 100) { return 0; } else { return arr[index]; } } set { if (!(index < 0 || index >= 100)) { arr[index] = value; } } } }
实例的运行结果如图2-6所示。
图2-6 索引器的定义与使用实例运行结果
程序说明:
(1)程序中首先定义了一个类Indexer,该类中定义了一个私有的数组成员,然后通过public int this[int index]声明了索引器,再通过get和set设置关键词,获取或者设置索引器对
应位置的值。
(2)在主函数中,首先定义了一个Indexer的实例对象,然后通过索引器对该对象的两个索引内容进行赋值。
2.4.4 重载方法
方法的重载是指定义多个相同名称的方法,根据各个方法的参数不同来,在调用的时候由编译译器进行区分。编译器会根据调用者输入的参数来自动识别需要调用的方法。
技巧:方法的重载可以根据参数的不同,而由程序判断具体的调用,各个重载方法之间可以自行调用,从而减少了开发的时间,由于参数不同而增加的同一逻辑的代码量。
下面通过一个实例来介绍如何实现方法的重载。
【本节示例参考:\Source Code\C02\ MultiMethod】
重载方法定义代码示例:Program.cs
class Program { static void Main(string[] args) { //构造类的实例 TestClass obj = new TestClass(); Object inputObj = "11"; //调用string重载方法 Console.WriteLine("调用string重载方法{0}",obj.ConvertObject(inputObj, "0")); //调用int重载方法 Console.WriteLine("调用int重载方法{0}", obj.ConvertObject(inputObj, 0)); //调用DateTime重载方法 Console.WriteLine("调用DateTime重载方法{0}", obj.ConvertObject(inputObj, DateTime.MinValue)); //调用decimal重载方法 Console.WriteLine("调用decimal重载方法{0}", obj.ConvertObject(inputObj, decimal.Zero)); Console.ReadLine(); } } /// <summary> /// 定义包含重载方法的类 /// </summary> public class TestClass { /// <summary> /// 重载方法,输入参数为string类型 /// </summary> /// <param name="obj">待转换值</param> /// <param name="defaultValue">默认值</param> /// <returns>返回结果</returns> public string ConvertObject(object obj, string defaultValue) { string ret = defaultValue; try { ret = System.Convert.ToString(obj); } catch {} return ret; } /// <summary> /// 重载方法,输入参数为int类型 /// </summary> /// <param name="obj">待转换值</param> /// <param name="defaultValue">默认值</param> /// <returns>返回结果</returns> public int ConvertObject(object obj, int defaultValue) { int ret = defaultValue; try { ret = System.Convert.ToInt32(obj); } catch { } return ret; } /// <summary> /// 重载方法,输入参数为DateTime类型 /// </summary> /// <param name="obj">待转换值</param> /// <param name="defaultValue">默认值</param> /// <returns>返回结果</returns> public DateTime ConvertObject(object obj, DateTime defaultValue) { DateTime ret = defaultValue; try { ret = System.Convert.ToDateTime(obj); } catch { } return ret; } /// <summary> /// 重载方法,输入参数为decimal类型 /// </summary> /// <param name="obj">待转换值</param> /// <param name="defaultValue">默认值</param> /// <returns>返回结果</returns> public decimal ConvertObject(object obj, decimal defaultValue) { decimal ret = defaultValue; try { ret = System.Convert.ToDecimal(obj); } catch { } return ret; } }
实例的运行结果如图2-7所示。
图2-7 方法重载实例运行结果
程序说明:
(1)程序中一共定义了4种同名的重载方法,每种方法都采用几乎相同的逻辑,将输入对象转换成该方法中输入给定类型的值。
(2)各种重载方法通过第二个参数的类型来判断需要转换的结果类型。
这样,在程序使用的时候,就可以直接调用同名方法,实现对不同对象的类型转换处理。
2.4.5 使用Ref和Out类型参数
Ref和Out关键字都是用来修饰方法中参数类型的修饰符。Ref关键字使参数按引用传递。其效果是,当控制权传递回调用方法时,在方法中对参数所做的任何更改都将反映在该变量中。若要使用Ref参数,则方法定义和调用方法都必须显式使用Ref关键字。Out关键字会导致参数通过引用来传递。这与Ref关键字类似,不同之处在于Ref要求变量必须在传递之前进行初始化。
注意:若要使用Out参数,方法定义和调用方法都必须显式使用Out关键字。
下面通过一个实例介绍如何在方法中定义和使用Ref和Out参数。
【本节示例参考:\Source Code\C02\ MethodParam】
Ref和Out参数代码示例:Program.cs
class Program{
static void Main(string[] args) { int inputInt = 0; TestClass obj = new TestClass(); //调用使用值类型参数方法 int result = obj.UseValueParam(inputInt); Console.WriteLine("使用值类型参数方法:{0}", result); //调用使用引用类型参数方法 obj.UseRefParam(ref inputInt); Console.WriteLine("使用引用类型参数方法:{0}", inputInt); int outParam; //调用使用引用输出类型参数方法 obj.UseOutParam(out outParam); Console.WriteLine("使用引用输出类型参数方法:{0}", outParam); Console.ReadLine(); } } /// <summary> /// 类声明 /// </summary> public class TestClass { /// <summary> /// 普通参数 /// </summary> /// <param name="inputValue"></param> /// <returns></returns> public int UseValueParam(int inputValue) { return inputValue + 1; } /// <summary> /// 使用引用参数 /// </summary> /// <param name="inputRef"></param> /// <returns></returns> public int UseRefParam(ref int inputRef) { inputRef += 1; return inputRef; } /// <summary> /// 使用输出参赛 /// </summary> /// <param name="inputOut"></param> /// <returns></returns> public int UseOutParam(out int inputOut) { inputOut = 1; return inputOut; } }
实例的运行结果如图2-8所示。
图2-8 Ref和Out参数实例运行结果
程序说明:
(1)TestClass类中,分别定义了三种方法。三种方面采用的参数分别为普通类型、Out类型以及Ref类型。三种方法都是对输入的整型变量进行值操作。
(2)在主函数中,定义了整型输入变量之后,比较了三种参数类型在调用时的区别。
2.4.6 定义接口和抽象类
接口可以包含方法、属性、事件和索引器。接口本身不提供它所定义的成员的实现。接口只指定实现该接口的类或结构必须提供的成员。一个类可以继承于多个接口,但这些继承的子类必须实现每个接口中定义的方法。
接口定义的实例代码如下所示。
interface IObject1 { object Method1(); } interface IObject2 { int Method2(); } class Test: IObject1, IObject2 { public object Method1 () {...} public int Method2() {...} }
注意:抽象类不能实例化,抽象类的用途是提供给继承它的子类以共享其中方法和属性等的定义。定义抽象类需要使用abstract关键字。
下面通过一个实例介绍如何实现抽象类的定义,以及抽象类的继承和使用。
【本节示例参考:\Source Code\C02\ UseAbstractClass】
抽象类的使用代码示例:Program.cs
class Program { static void Main(string[] args) { //定义Employee子类的实例 Person employee = new Employee("Black", "Smith", 25); //定义Manager子类的实例 Person manager = new Manager("Bill", "Gatz", "Microsoft"); //调用子类的抽象方法的实现方法 employee.GetFullName(); manager.GetFullName(); Console.ReadLine(); } } /// <summary>
/// 定义抽象类
/// </summary>
abstract class Person{
///定义抽象类的包含成员protected string _firstName;protected string _lastName;/// <summary>
/// 抽象类构造函数
/// </summary>public Person(){ }
/// <summary>
/// 抽象类构造函数
/// </summary>
public Person(string firstName, string lastName){_firstName = firstName;_lastName = lastName;}
/// <summary>
/// 定义抽象方法
/// </summary>
public abstract void GetFullName();}
/// <summary>
/// 定义继承于抽象类的Employee子类
/// </summary>
class Employee:Person{public int _age;/// <summary>
/// 子类构造函数
/// </summary>
public Employee():base(){ }
/// <summary>/// 子类构造函数/// </summary>
public Employee(string firstName, string lastName,int age):base(firstName, lastName) { _age = age; } /// <summary> /// 重写父抽象类的方法 /// </summary> public override void GetFullName() { Console.WriteLine("Employee:{0} {1}", _firstName, _lastName); } } /// <summary> /// 定义继承于抽象类的Manager子类 /// </summary> class Manager : Person { public string _company; /// <summary> /// 子类构造函数 /// </summary> public Manager(): base() { } /// <summary> /// 子类构造函数 /// </summary> public Manager(string firstName, string lastName, string company) : base(firstName, lastName) { _company = company; } /// <summary> /// 重写父抽象类的方法 /// </summary> public override void GetFullName() { Console.WriteLine("Manager:{0} {1}", _firstName, _lastName); } }
实例的运行结果如图2-9所示。
图2-9 抽象类定义和使用实例运行结果
程序说明:
(1)程序中首先定义了抽象类Person,然后在该类定义了类的构造函数以及GetFullName抽象方法。
(2)随后定义了Employee和Manager两个类,继承Person类,并且在各自的类定义中重写了GetFullName抽象方法。
抽象类本身不能被实例化,而且抽象类中定义的抽象方法不需要实现具体逻辑。但继承该抽象类的子类必须实现抽象方法。
2.5 使用集合编程
集合是C#编程中一个重要的数据组成形式,通过集合可以将数据存储于其中,并通过各个集合提供的特性,对数据进行索引、取值、排序等操作。C#集合包括枚举、数组、ArrayList、哈希表、字典、堆栈和队列,下面逐一进行介绍。
说明:C#中提供了多种集合对象,各个集合对象有其自身的特性。在具体的开发中,读者可以根据集合的特性来分别使用。比如堆栈实现先进后出的集合特性,而队列实现先进先出的集合特性。
2.5.1 使用枚举
枚举是值类型的一种特殊形式,它从System.Enum继承而来,并为基础基元类型的值提供替代名称。枚举类型由名称、基础类型和一组字段组成。
说明:枚举是一种事先定义好的集合类型,可以通过枚举来实现业务常用变量与值的一种映射关系。
下面通过一个实例介绍如何使用枚举编程。
【本节示例参考:\Source Code\C02\ EnumTypeFormat】
使用枚举代码示例:Program.cs
class Program { //定义星期枚举 enum Days { Sat = 1, Sun, Mon, Tue, Wed, Thu, Fri }; //定义范围枚举,类型为long enum Range : long { Max = 2147483648L, Min = 255L }; static void Main() { //将枚举强制转换为int型 int x = (int)Days.Sun; int y = (int)Days.Fri; //输出枚举对应的整型值 Console.WriteLine("Sun = {0}", x); Console.WriteLine("Fri = {0}", y); //将枚举强制转换为long型 long xx = (long)Range.Max; long yy = (long)Range.Min; //输出枚举对应的long值 Console.WriteLine("Max = {0}", xx); Console.WriteLine("Min = {0}", yy); Console.ReadLine(); } }
实例的运行结果如图2-10所示。
图2-10 使用枚举实例运行结果
程序说明:
(1)程序中定义了Days和Range两个枚举,一个是默认的枚举类型,即使用整型int代表枚举值,另外一个采用的是long作为枚举值。
(2)在主函数中,分别使用int强制类型转换和long强制类型转换,将各个枚举代表的值输出到控制台。
2.5.2 使用数组
数组是一种指定类型的数据集合,每个数组有固定的大小,并且其中数组元素的类型必须保持一致。数组的定义语法为:数组类型[] 数组名称。如下面代码定义了一个长度为10的整型数组。
int[] arr = new int[10];
说明:数组是一种固定长度的集合类型,可以根据其类型来存储一维或者多维的数据。
下面通过一个实例介绍如何使用数组。
【本节示例参考:\Source Code\C02\ InitArray】
使用数组代码示例:Program.cs
class Program { public static void Main() { int[] myIntArray = new int[5] { 1, 2, 3, 4, 5 };//定义整型数组 Object[] myObjArray = new Object[5] { 26, 27, 28, 29, 30 };//定义对象数组 Console.WriteLine("初始化,"); Console.Write("整型集合:"); PrintValues(myIntArray); Console.Write("对象集合: "); PrintValues(myObjArray); Array.Copy(myIntArray, myObjArray, 2); Console.WriteLine("\n将整型集合复制到对象集合"); Console.Write("整型集合内容:"); PrintValues(myIntArray); Console.Write("对象集合内容: "); PrintValues(myObjArray); Array.Copy(myObjArray, myObjArray.GetUpperBound(0) -1, myIntArray, myIntArray.GetUpperBound(0) -1, 2); Console.WriteLine("\n将对象集合复制到整型集合,"); Console.Write("整型集合内容:"); PrintValues(myIntArray); Console.Write("对象集合内容: "); PrintValues(myObjArray); Console.ReadLine(); } /// <summary> /// 显示对象集合内容 /// </summary> public static void PrintValues(Object[] myArr) { foreach (Object i in myArr) { Console.Write("\t{0}", i); } Console.WriteLine(); } /// <summary> /// 显示整型集合内容 /// </summary> public static void PrintValues(int[] myArr) { foreach (int i in myArr) { Console.Write("\t{0}", i); } Console.WriteLine(); } }
实例的运行结果如图2-11所示。
图2-11 使用数组实例运行结果
程序说明:
(1)程序中,首先定义了两个数组变量,类型分别为整型和对象类型。通过定义了PrintValues重载方法,用来实现将数组的内容显示到控制台中。
(2)Copy方法用来实现数组的复制功能。将整型集合数据复制到对象集合中,然后再将对象集合复制到整型集合。
2.5.3 使用ArrayList
ArrayList可以理解为一种特殊的数组,但由于数组本身需要固定长度,所以数组往往不是很灵活。ArrayList不同,它可以动态增加或者减少内部集合所存储的数据对象个数。ArrayList的默认初始容量为0,容量会根据需要通过重新分配自动增加。使用整数索引可以访问此ArrayList集合中的元素,集合中的索引从0开始。
说明:ArrayList与数组最大的不同在于,声明ArrayList对象的时候,无须指定集合的长度,而在后续使用中动态地进行添加。
下面通过一个实例介绍如何使用ArrayList。
【本节示例参考:\Source Code\C02\ InitArrayList】
使用ArrayList代码示例:Program.cs
class Program { public static void Main() { ArrayList myAL = new ArrayList();//定义集合 myAL.Add("Hello");//添加集合内容 myAL.Add("World"); myAL.Add("!"); Console.WriteLine("ArrayList对象属性"); Console.WriteLine(" 数量: {0}", myAL.Count); Console.WriteLine(" 可包含元素数: {0}", myAL.Capacity); Console.Write(" 值:"); PrintValues(myAL); Console.ReadLine(); } public static void PrintValues(IEnumerable myList) { //遍历集合中的元素 foreach (Object obj in myList) Console.Write(" {0}", obj); Console.WriteLine(); } }
实例的运行结果如图2-12所示。
图2-12 使用ArrayList实例运行结果
程序说明:
(1)程序中,首先声明了一个ArrayList对象实例,由于该对象不需要指定类型和长度,所以可以通过该实例的Add方法,在实例中添加内容。
(2)通过PrintValues方法,实现对ArrayList对象内容的遍历,然后将该对象的所有内容值显示到控制台中。
2.5.4 使用哈希表
哈希表是表示键-值对的数据集合,这些键-值对根据键的哈希代码进行组合。哈希表中的每个元素都是存储在DictionaryEntry对象之中。
下面通过一个实例介绍如何使用哈希表。
【本节示例参考:\Source Code\C02\ InitHashtable】
使用哈希表代码示例:Program.cs
class Program { public static void Main() { Hashtable myHT = new Hashtable();//定义集合对象 myHT.Add("First", "Hello");//添加集合元素 myHT.Add("Second", "World"); myHT.Add("Third", "!"); Console.WriteLine("Hashtable属性"); Console.WriteLine(" 数量: {0}", myHT.Count); Console.WriteLine(" 键值显示:"); PrintKeysAndValues(myHT); Console.ReadLine(); } public static void PrintKeysAndValues(Hashtable myHT) { Console.WriteLine("\t-键-\t-值-"); //遍历集合中的元素 foreach (DictionaryEntry de in myHT) Console.WriteLine("\t{0}:\t{1}", de.Key, de.Value); Console.WriteLine(); } }
实例的运行结果如图2-13所示。
图2-13 使用哈希表实例运行结果
程序说明:
(1)程序中,首先定义了Hashtable的对象实例myHT,然后通过Add方法,添加集合中的元素。
(2)在PrintKeysAndValues方法中,通过foreach循环语句,遍历集合中的各个DictionaryEntry对象,然后将每个对象的键值属性显示到控制台中。
2.5.5 使用字典
字典表示的也是键-值对的数据集合,可以通过字典的泛型类来定义一个复杂的集合对象,从而具体指定字典中存储的数据键类型以及值类型。
下面通过一个实例介绍如何使用字典。
【本节示例参考:\Source Code\C02\ OperateDictionary】
使用字典代码示例:Program.cs
public static void Main() { Dictionary<string, string> openWith = new Dictionary<string, string>();//定义字典泛型 openWith.Add("txt", "notepad.exe");//添加字典元素 openWith.Add("bmp", "paint.exe"); openWith.Add("dib", "paint.exe"); openWith.Add("rtf", "wordpad.exe"); try { openWith.Add("txt", "winword.exe"); } catch (ArgumentException) { Console.WriteLine("元素 = \"txt\" 已经存在."); } Console.WriteLine("键rtf对应的值为: {0}.", openWith["rtf"]); openWith["rtf"] = "winword.exe";//设置字典数据内容 Console.WriteLine("修改rtf之后的值为 = {0}.", openWith["rtf"]); openWith["doc"] = "winword.exe"; try { Console.WriteLine("键tif对应的值为 = {0}.", openWith["tif"]); } catch (KeyNotFoundException) { Console.WriteLine("键tif不存在."); } string value = ""; if (openWith.TryGetValue("tif", out value))//读取字典内容 { Console.WriteLine("键tif对应的值为= {0}.", value); } else { Console.WriteLine("键tif不存在."); } if (!openWith.ContainsKey("ht"))//判断字典是否包含键值 { openWith.Add("ht", "hypertrm.exe"); Console.WriteLine("增加的ht键对应值为: {0}", openWith["ht"]); } Console.WriteLine(); foreach (KeyValuePair<string, string> kvp in openWith)//遍历字典元素 { Console.WriteLine("键 = {0}, 值 = {1}", kvp.Key, kvp.Value); } Dictionary<string, string>.ValueCollection valueColl = openWith.Values; Console.WriteLine(); foreach (string s in valueColl) { Console.WriteLine("值 = {0}", s); } Dictionary<string, string>.KeyCollection keyColl = openWith.Keys; Console.WriteLine(); foreach (string s in keyColl) { Console.WriteLine("值 = {0}", s); } Console.WriteLine("移除doc键"); openWith.Remove("doc"); if (!openWith.ContainsKey("doc")) { Console.WriteLine("键doc不存在."); } Console.ReadLine(); }
实例的运行结果如图2-14所示。
图2-14 使用字典实例运行结果
程序说明:
(1)实例中,首先定义了Dictionary<string, string>实例,该泛型实例化之后,是一个键值都为字符串类型的键值集合对象。
(2)通过实例的Add方法,将几个键值添加到对象集合中。
(3)通过openWith["键名称"]来设置或者获取某个键对应的值内容。
(4)通过TryGetValue方法,获取某个键对应值的内容。
(5)通过ContainsKey方法,判断是否存在某一键名称。
(6)通过foreach遍历ValueCollection值集合,可以获取泛型实例中的所有值。通过遍历KeyCollection键集合,可以获取泛型实例中的所有键。
2.5.6 使用堆栈
堆栈是一种后进先出类型的数据集合对象。所谓后进先出,是指先存储到集合中的数据会被保存到堆栈的底层,由于堆栈的数据是从顶部先取走,所以在数据从堆栈取出的时候,最先保存的数据会被最后才提取出来。
下面通过一个实例介绍如何使用堆栈。
【本节示例参考:\Source Code\C02\ InitStack】
使用堆栈代码示例:Program.cs
class Program { public static void Main() { // 初始化堆栈 Stack myStack = new Stack();//定义堆栈实例 myStack.Push("Hello");//入栈 myStack.Push("World"); myStack.Push("!"); // 显示堆栈属性. Console.WriteLine("堆栈属性"); Console.WriteLine("\t数量: {0}", myStack.Count); Console.Write("\t值s:"); PrintValues(myStack); Console.ReadLine(); } public static void PrintValues(IEnumerable myCollection) { foreach (Object obj in myCollection)//遍历堆栈数据 Console.Write(" {0}", obj); Console.WriteLine(); } }
实例的运行结果如图2-15所示。
图2-15 使用堆栈实例运行结果
程序说明:
(1)实例中,首先通过Stack的Push方法,在初始化的构造函数实例中,添加了3个Stack元素。然后通过调用Count属性,显示该实例中的元素数量。
(2)通过foreach遍历Stack对象中的每个对象,将每个对象的内容显示到控制台中。
2.5.7 使用队列
队列与堆栈不同,它表示的是一种先进先出的数据集合对象,即先进入队列的数据将会被首先取出来。下面通过一个实例介绍如何使用队列。
【本节示例参考:\Source Code\C02\ InitQueue】
使用队列代码示例:Program.cs
class Program { public static void Main() { // 初始化队列 Queue myQ = new Queue(); myQ.Enqueue("Hello"); myQ.Enqueue("World"); myQ.Enqueue("!"); // 显示队列属性 Console.WriteLine("Queue属性"); Console.WriteLine("\t数量: {0}", myQ.Count); Console.Write("\t值:"); PrintValues(myQ); Console.ReadLine(); } public static void PrintValues(IEnumerable myCollection) { foreach (Object obj in myCollection) Console.Write(" {0}", obj); Console.WriteLine(); } }
实例的运行结果如图2-16所示。
图2-16 使用队列实例运行结果
程序说明:
(1)实例中,首先通过Queue的Enqueue方法,在初始化的构造函数实例中添加了3个Queue元素。然后通过调用Count属性,显示该实例中的元素数量。
(2)通过foreach遍历Queue对象中的每个对象,将每个对象的内容显示到控制台中。
2.6 委托和事件
委托和事件是C#中两种重要的处理方式,通过委托和事件可以实现程序的一些特定场景。本节将介绍如何使用委托进行回调以及动态地进行事件的注册和移除。
2.6.1 委托和事件的定义
委托是一种引用方法的类型。一旦为委托分配了方法,委托将与该方法具有完全相同的行为。委托方法的使用可以像其他任何方法一样,具有参数和返回值。
事件是类在发生其关注的事情时用来提供通知的一种方式。在类中实现了事件处理之后,可以在事件发生时,按照预先定义好的处理代码来进行处理。
说明:事件使用委托来为触发时将调用的方法提供类型安全的封装。
2.6.2 使用委托进行回调
委托类型继承于.NET中的Delegate类,由于实例化委托是一个对象,所以可以将其作为参数进行传递,也可以将其赋值给属性。这样便可以将一个委托作为参数来接受,并且以后可以调用该委托。下面通过一个实例介绍如何使用委托进行回调。
【本节示例参考:\Source Code\C02\ UseDelegate】
使用委托进行回调代码示例:Program.cs
//定义委托 public delegate void Del(string message); class Program { //委托方法 public static void DelegateMethod(string message) { System.Console.WriteLine(message); } static void Main(string[] args) { //声明委托实例 Del handler = DelegateMethod; //调用委托方法 handler("Hello World"); Console.ReadLine(); } }
实例的运行结果如图2-17所示。
图2-17 使用委托进行回调实例运行结果
代码说明:
(1)方法中首先定义了一个委托Del。
(2)定义了一个用于委托调用的方法DelegateMethod。
(3)在Main函数中,首先声明了委托实例调用的方法名称,然后直接调用该委托实例,执行方法。
2.6.3 动态注册和移除事件
事件是类在发生其关注的事情时用来提供通知的一种方式。由于事件的特殊性,使得事件在触发之前需要首先注册到事件列表中,同样如果不需要触发事件,则将已经定义的事件进行移除。
下面通过一个实例介绍如何实现事件的动态注册和移除。【本节示例参考:\Source Code\C02\ UseDelegate】
事件的动态注册和移除代码示例:Program.cs
namespace UseEvent { using MyCollections; class Program { static void Main(string[] args) { // 创建新列表 ListWithChangedEvent list = new ListWithChangedEvent(); // 创建一个类,用于侦听列表的更改事件 EventListener listener = new EventListener(list); // 在列表中添加和移除项 list.Add("item 1"); list.Clear(); listener.Detach(); Console.ReadLine(); } } class EventListener { private ListWithChangedEvent List; public EventListener(ListWithChangedEvent list) { List = list; // 将“ListChanged”添加到“List”中的Changed事件 List.Changed += new ChangedEventHandler(ListChanged); } // 每当列表更改时就会进行以下调用 private void ListChanged(object sender, EventArgs e) { Console.WriteLine("在事件触发时候被调用."); } public void Detach() { // 分离事件并删除列表 List.Changed -= new ChangedEventHandler(ListChanged); List = null; } } } namespace MyCollections { using System.Collections; // 用于对更改通知进行挂钩的委托类型 public delegate void ChangedEventHandler(object sender, EventArgs e); // 一个类,其作用与ArrayList相似, // 但在每次列表更改时发送通知。 public class ListWithChangedEvent : ArrayList { // 一个事件,每当列表元素更改时,客户端可利用该事件 // 获得通知。 public event ChangedEventHandler Changed; // 调用Changed事件;每当列表更改时调用 protected virtual void OnChanged(EventArgs e) { if (Changed != null) Changed(this, e); } // 重写可更改列表的某些方法; // 在每个重写后调用事件 public override int Add(object value) { int i = base.Add(value); OnChanged(EventArgs.Empty); return i; } public override void Clear() { base.Clear(); OnChanged(EventArgs.Empty); } public override object this[int index] { set { base[index] = value; OnChanged(EventArgs.Empty); } } } }
实例的运行结果如图2-18所示。
图2-18 事件的动态注册和移除实例运行结果
代码说明:
(1)实例首先创建了一个继承于ArrayList类的自定义类ListWithChangedEvent。在该类中,重写了基类的OnChanged、Add、Clear和this等方法属性。然后定义了ChangedEventHandler作为类的委托对象。
(2)然后创建了EventListener类,该类用于事件的监听,通过ListChanged方法监听列表是否变化,通过Detach方法来注销事件。
(3)最后在Main函数中,定义了ListWithChangedEvent对象和EventListener对象,并且将ListWithChangedEvent实例注册到EventListener监听对象中。随即在ListWithChangedEvent对象中添加了一条记录,然后清除,该操作引发了事件。
2.7 错误和异常处理
在程序处理中,可能会遇到系统内部或者外部的错误,这种错误被定义为异常。在.NET框架中,提供了一系列处理异常和错误的机制。异常处理使用try、catch和finally关键字来尝试可能未成功的操作、处理失败,以及在事后清理资源。
注意::try语句用来定义在可能出现异常的语句中设定范围;catch语句用来捕获异常并进行处理;finally语句用来处理在异常出现或者语句正常执行之后的后续处理部分。
例如,在数据库操作中,通常使用这种异常处理机制,保证数据库的连接资源被正常释放,如下面代码所示。
try { //打开数据库连接,并进行数据库操作 } catch { //捕获数据库操作异常,进行异常处理 } finally { //关闭数据库连接,保证数据库资源的回收 }
下面通过一个实例介绍如何实现错误和异常处理。
【本节示例参考:\Source Code\C02\ UserFinally】
错误和异常处理代码示例:Program.cs
static void Main(string[] args) { //定义2各整型数组 int[] array1 ={ 0, 0 }; int[] array2 ={ 0, 0 }; try { //数组复制,引发超出最大边界异常 Array.Copy(array1, array2, -1); } catch (ArgumentOutOfRangeException e) { //捕获异常,显示错误信息 Console.WriteLine("错误: {0}", e); } finally { //执行finally语句的代码 Console.WriteLine("该语句总是被执行!"); } Console.ReadLine(); }
实例的运行结果如图2-19所示。
图2-19 错误和异常处理实例运行结果
程序说明:
(1)程序中首先定义了两个整型的数组,然后在try语句中复制数组。由于复制的参数设定超出最大边界,从而引发异常。
(2)在catch语句中,捕捉ArgumentOutOfRangeException异常,然后将异常的信息显示到控制台中。
(3)在finally语句中,直接输出信息到控制台中,表明该段代码永远都会被执行。
2.8 小结
本章介绍了.NET框架中的C#语言。作为框架中的一个基础语言,C#有着很广泛的应用。本章通过实例的形式介绍了C#的如下基本语言基础:类型、语句、运算符、类、结构、集合以及委托和事件等。这些语法作为使用C#语言的基础知识,在C#语言编程中扮演着非常重要的角色,读者只有掌握了这些语言基础知识,才能更好地使用C#语言。