3.3 面向对象程序
C#是面向对象的语言,面向对象使用“类”来封装其属性和方法等成员,这种封装能起到一定的隐藏作用。其他对象使用该对象时,不需要知道其实现的细节,只需要通过相互之间定义的接口进行交互和通信。采用面向对象的程序可维护性较好,源程序易于阅读理解和修改,降低了复杂度。
3.3.1 类
在C#中,类是一种功能强大的数据类型,而且是面向对象的基础。类定义属性和行为,可以声明类的实例,从而利用这些属性和行为。类中包含数据成员(常数、域和事件)、功能成员(方法、属性、索引、操作符、构造函数、析构函数)和嵌套类型。类支持继承,派生的类可以对基类进行扩展和特殊化,使得程序代码可以复用,子类中可以继承祖先类中的部分代码。由于类封装了数据和操作,从类外面看,只能看到公开的数据和操作,而这些操作都在类设计时进行安全性考虑,因而外界操作不会对类造成破坏。
C#中提供了很多标准的类,用户在开发过程中可以使用这些类,这样大大节省了程序的开发时间。C#中也可以自己定义类,类的定义方法为:
上面代码中“类名”是自定义类的名字,该名字要符合标识符的要求。“父类名”表示从哪个类继承。“:父类名”可以省略,如果没有父类名,则默认从Object类继承而来。Object类是每个类的祖先类,C#中所有的类都是从Object类派生出来的。“类修饰符”用于对类进行修饰,说明类的特性,类的每个成员都需要设定访问修饰符,不同的修饰符会造成对成员访问能力不一样。如果没有显示指定类成员访问修饰符,默认类型为私有类型修饰符。C#中类成员修饰符的定义和使用方法如表3-11所示。
表3-11 成员修饰符的定义和使用方法
3.3.2 属性、方法和事件
在C#中,按照类的成员是否为函数将其分为两大类,一种不以函数形式体现,称为“成员变量”,主要有以下几个类型。
● 常量:代表与类相关的常量值。
● 变量:类中的变量。
● 事件:由类产生的通知,用于说明发生了什么事情。
● 类型:属于类的局部类型。
另一种以函数形式体现,一般包含可执行代码,执行时完成一定的操作,被称为“成员函数”,主要有以下几个类型。
● 方法:完成类中各种计算或功能的操作,不能和类同名,也不能在前面加“~”波浪线符号。方法名不能和类中其他成员同名,既包括其他非方法成员,又包括其他方法成员。
● 属性:定义类的值,并对它们提供读、写操作。
● 索引指示器:允许编程人员在访问数组时,通过索引指示器访问类的多个实例,又称下标指示器。
● 运算符:定义类对象能使用的操作符。
● 构造函数:在类被实例化时首先执行的函数,主要是完成对象初始化操作。构造函数必须和类名相同。
● 析构函数:在类被删除之前最后执行的函数,主要是完成对象结束时的收尾操作。构造函数必须和类名相同,并前加一个“~”波浪线符号。
3.3.3 构造函数
当创建一个对象时,系统首先给对象分配合适的内存空间,随后系统自动调用对象的构造函数。因此构造函数是对象执行的入口函数,非常的重要。在定义类时,可以给出构造函数也可以不定义构造函数。如果类中没有构造函数,系统会默认执行System.Object提供的构造函数。如果要定义构造函数,那么构造函数的函数名必须和类名一样。构造函数的类型修饰符总是公有类型public的,如果是私有类型private的,表示这个类不能被实例化,这通常用于只含有静态成员的类中。构造函数由于不需要显示调用,因而不用声明返回类型。构造函数可以带参数也可以不带参数。具体实例化时,对于带参数的构造函数,需要实例化的对象也带参数,并且参数个数要相等,类型要一一对应。如果是不带参数的构造函数,因而在实例化时对象不具有参数。
下面是一个类中构造函数的代码:
在以上代码中,第1行~第4行定义了一个Human类。它有两个成员变量name和age分别表示人的姓名和年龄。第5行~第8行是Human类带两个参数的构造函数,通过参数给两个成员变量赋值。第9行~第12行是Human类带参数的构造函数,在函数中直接给两个成员变量赋值,这种在函数或方法中定义的参数称为“形式参数”,简称为“形参”。
上面的例子中定义了两个不同的构造函数,像这样在一个类中如果有两个函数(包括构造函数)或者方法名称相同,但参数个数不同或者参数的类型不同,称为“方法的重载”。实现方法时系统会自动选择合适的类型和调用的函数相匹配。
在使用定义的类时,可以使用任何一个构造函数创建实例化对象。对象是类的实例化,只有对象才能包含数据,执行行为,触发事件,而类只不过就像int一样是数据类型,只有实例化才能真正发挥作用。对象具有以下特点:
● C#中使用的全都是对象。
● 对象是实例化的,对象是从类和结构所定义的模板中创建的。
● 对象使用属性获取和更改它们所包含的信息。
● 对象通常具有允许它们执行操作的方法和事件。
● 所有C#对象都继承自Object。
● 对象具有多态性,对象可以实现派生类和基类的数据和行为。
对象的声明就是类的实例化,类实例化的方式很简单,通过使用new来实现,例如:
上面代码中第1行使用前面定义的Human类默认构造函数实例化对象p1。第2行使用前面定义的Human类带两个参数的构造函数实例化对象p2,这种在调用函数或方法时提供的参数值称为“实际参数”,简称为“实参”。
3.3.4 继承
继承是面向对象的一个重要特性,C#中支持类的单继承,即只能从一个类继承。继承是传递的,如果C继承了B,并且B继承了A,那么C继承在B中声明的public和protected成员同时也继承了在A中声明的public和protected成员。继承性使得软件模块可以最大限度地复用,并且编程人员还可以对前人或自己以前编写的模块进行扩充,而不需要修改原来的源代码,大大提高了软件的开发效率。
在定义类的时候可以指定要继承的类,语法如下:
例如下面的代码,类B从类A中继承,类A被称为“基类”或“父类”,类B被称为“派生类”或“子类”:
上面的代码中第1行定义了一个类A,第4行定义了继承自类A的类B。
派生类是对基类的扩展,派生类可以增加自己新的成员,但不能对已继承的成员进行删除,只能不予使用。基类可以定义自身成员的访问方式,从而决定派生类的访问权限。且可以通过定义虚方法、虚属性,使它的派生类可以重载这些成员,从而实现类的多态性。
一个派生类自动包含来自基类的所有字段。创建一个对象时,这些字段需要初始化。因此,有时候,需要通过调用基类的构造函数来对基类的字段进行初始化。在定义了构造函数的基础上,可以使用base关键字来调用基类的构造函数:
以上代码中第9~第11行定义了一个继承与Human类的派生类Femal。其中,第16行使用base关键字调用了Human类的带参的构造函数。
3.3.5 多态
多态是面向对象程序设计的一个重要特征,利用多态性可以设计和实现一个易于扩展的系统。具体说多态是指向不同的对象发送同一个消息时,不同的对象在接收时会产生不同的行为(即方法)。
继承的一个结果是派生与基类的类在方法上有一定的重叠,因此,可以使用相同的语法从同一个基类实例化对象。例如,如果基类Animal(动物)有一个方法EatFood(进食),则从派生它的类Cow(牛)和Chicken(鸡)中调用这个方法,其语法是类似的:
多态性则更推进了一步,可以把某个基本类型的变量赋予其派生类变量,如下所示:
这里不需要强制类型转换,然后就可以通过这个变量调用基类的方法:
结果是调用了派生类中的EatFood执行代码。在派生于同一个类的不同对象上执行任务时,多态性是一种极为有效的技巧,使其代码大大地简化了。
当把chicken1赋予animal1后,再次调用EatFood方法时,其执行的代码就变成了Chicken类中的代码。
可以发现animal1实际上是对一个Cow类的引用。程序判断出它应该是调用Cow类的EatFood方法。同样,当把一个chicken对象让animal1引用时,它调用的是Chicken类的EatFood方法。这样同一个语句调用不同方法就是多态。
在C#中有两种多态性,一种是编译时的多态性,这种多态性是通过函数的重载实现的,由于重载函数的参数、数量或者是类型不同,所以编译系统在编译期间就可以确定用户所调用的函数是哪一个重载函数。另外一种是运行时的多态性,这种多态性是通过虚成员方式实现的。运行时的多态性是指系统在编译时不确定选用哪个重载函数,而是直到系统运行时,才根据实际情况决定采用哪个重载函数。
在定义类成员时,可以使用virtual关键字,virtual关键字用于修改方法或属性的声明。被virtual关键字修饰的方法或属性被称作虚拟成员,虚拟成员的实现可由派生类中的重写成员更改。
不能将virtual修饰符与static、abstract、override等修饰符一起使用,此外在静态属性上使用virtual修饰符是错误的。通过包括使用override修饰符的属性声明,可以在派生类中重写虚拟继承属性,这种重写的方法称为重写基方法。
C#中关于override重写的要求如下:
● 不能重写非虚方法或静态方法。
● 重写基方法必须与重写方法具有相同的名字。
● 重写声明不能更改虚方法的可访问性,重写方法和虚方法必须具有相同的访问级修饰符。
● 不能使用new、static、virtual、abstract等修饰符修改重写方法。
● 返回值类型必须与基类中的虚拟方法一致。
● 参数列表中的参数顺序、数量和类型必须一致。
下面是一个子类Sun重写基类Father中smile方法的代码:
上面的代码中第2行在Father类中使用关键字virtual声明smile方法可以被派生类所重写。第6行在Father类的派生类Sun类中使用关键字override重写父类的smile方法。
【实例3-11】多态的使用
在管理系统中,用户和管理员都需要通过登录才能进入系统。一般用户登录将进入用户界面,而管理员登录将进入后台管理界面,也就是说登录方法是不一样的,本练习就使用类的多态性来解决该问题。具体实现步骤如下:
01 启动Visual Studio 2012,执行“文件”︱“新建项目”命令,在弹出的“新建项目”对话框中创建一个名为“实例3-10”的控制台应用程序。
02 在解决方案资源管理器中生成“实例3-10”的项目,单击目录下的Program.cs文件,在该文件中编写如下逻辑代码:
在上面的代码中第1行~第3行声明了一个抽象类Person,其中第2行定义一个抽象方法Login,该方法是没有被实现的,即没有方法体,需要子类去实现。第4行~第14行定义了一个继承于Person类的子类——普通用户类User,其中,第5行~第12行定义了实现父类中的抽象方法Login,第6行判断输入密码和用户名如果正确,第18行、第19行就输出登录成功和进入用户界面的提示,否则第22行输出登录失败的提示;第15~第25行定义一个继承于Person类的子类——管理员类Admin,其中,第16行~第24行定义了实现父类中的抽象方法Login,判断输入密码和用户名如果正确,输出登录成功和进入后台管理界面的提示,否则输出登录失败的提示。
第26行~第40行定义Program类,其中在第27行~第39行的Main方法中,第28行实例化一个Person类型的泛型集合类对象person;第29行~第31行分别实例化两个User类对象和一个Admin对象;第32行~第34行调用person对象的Add方法将三个对象添加到泛型集合对象中;第35行~第37行通过循环遍历调用泛型集合中对象的Login方法,输出登录结果。
03 按快捷键Ctrl+F5,程序运行的效果如图3-18所示。从运行结果可以看到,当遍历泛型集合并调用子类的方法,得到不同的结果,很好地体现了多态性的效果。
图3-18 运行结果
3.3.6 接口
由于在C#中只支持单继承,但有时候需要使用多继承来实现一些功能,所以在C#中,使用接口来实现这样的功能。有了接口以后,可以把继承的作用做更近一步的提升。接口只指出方法的名称、返回类型和参数。方法的具体实现,则不是接口需要关心的。也就说,接口继承允许将一个方法的名称和它的实现彻底的分离。
为了声明一个接口,需要使用interface关键字。接口和类一样可以有方法、属性和事件等成员,但与类不同的是,接口仅仅提供成员的声明,并不提供成员的实现。接口的语法格式如下:
关键字interface、接口名和接口体是必须的,其他项是可选的。接口修饰符可以是new、public、protected、internal和private。类似于类的继承性,接口也有继承性。派生接口继承了父接口中的函数成员说明。接口允许多继承,在接口声明的冒号后列出被继承的接口名字,多个接口名之间用分号分割。
在声明接口时,要注意以下内容:
● 接口成员只能是方法、属性、索引指示器和事件,不能是常量、域、操作符、构造函数或析构函数,不能包含任何静态成员。
● 接口成员声明不能包含任何修饰符,接口成员默认访问方式是public。
● 接口类似于抽象基类,继承接口的任何非抽象类型都必须实现接口的所有成员。
● 不能直接实例化接口。
接口的方法也像普通类方法那样声明,不同的是接口方法没有被实现,除了在实现的结束位置有一个分号之外,其他部分都与普通方法一样。例如以下代码:
在C#中,属性也可以成为接口成员。当属性的普通实现与字段(域)相关时,虽然字段不能成为接口成员,但这并不妨碍属性的使用,因为属性的实现是独立于它的说明的。这些属性的主要途径是封装实现。下面是一个接口声明的示例:
为了实现一个接口,需要声明一个类或者是结构,让它们从接口继承,然后实现所有接口的内容,包括属性或方法等。定义好接口之后,声明可以实现接口的类的语法如下:
如果类要继承一个父类,同时要实现多个接口,则以“,”隔开,语法如下:
定义实现接口的类后,就可以在该类中去实现接口中定义的接口成员。
【实例3-12】接口的使用
本例通过使用接口机制来实现例3-11的功能,具体实现步骤如下:
01 启动Visual Studio 2012,执行“文件”︱“新建项目”命令,在弹出的“新建项目”对话框中创建一个名为“实例3-12”的控制台应用程序。
02 在解决方案资源管理器中生成“实例3-12”的项目,单击目录下的Program.cs文件,在该文件中编写如下逻辑代码:
上面的代码中第1行~第3行声明了一个接口类IPerson,其中第2行定义一个抽象方法Login,该方法是没有被实现的,即没有方法体,需要子类去实现。第4~13行定义了一个继承于IPerson接口类的子类——普通用户类User,其中,第5~12行定义了实现父类中的抽象方法Login。第14~第23行定义了一个继承于IPerson接口类的子类——管理员类Admin,其中,第15~22行定义了实现父类中的抽象方法Login。
03 按快捷键Ctrl+F5,程序运行的效果如图3-18所示。
3.3.7 委托和事件
委托其实也是一种引用方法的类型,创建了委托,就可以声明委托变量,也就是委托实例化。实例化的委托就是委托的对象,可以为委托对象分配方法,也就是把方法名赋予委托对象。一旦为委托对象分配了方法,委托对象将与该方法具有完全相同的行为。委托对象的使用可以像其他任何方法一样,具有参数和返回值,如下面的示例所示:
上面的代码中第1行定义了一个名为Delete的委托,该委托封装了包含两个整数类型的参数,且返回值为整型。第2行声明dl为委托Delete的对象,就可以把方法名赋给该对象。
方法的分配比较自由,任何与委托的签名(由返回类型和参数组成)匹配的方法都可以分配给该委托的对象。这样就可以通过编程方式来更改方法调用,还可以向现有类中插入新代码。只要知道委托的签名,便可以分配委托方法。
将方法作为参数进行引用的能力使委托成为定义回调方法的理想选择。例如,可以向排序算法传递对比较两个对象的方法的引用。分离比较代码使得可以采用更通用的方式编写算法。
委托具有以下特点:
● 委托类似于C++函数指针,但它是类型安全的。
● 委托允许将方法作为参数进行传递。
● 委托可用于定义回调方法。
● 委托可以链接在一起;例如,可以对一个事件调用多个方法。
● 方法不需要与委托签名精确匹配。
构造委托对象时,通常提供委托包装方法的名称或使用匿名方法。实例化委托后,委托将把对它进行的方法调用传递给方法。调用方传递给委托的参数被传递给方法,来自方法的返回值(如果有)由委托返回给调用方。这被称为调用委托。可以将一个实例化的委托视为被包装的方法本身来调用该委托。
例如,为上面声明的委托定义一个方法,代码如下:
定义一个委托的实例,把上面的方法赋给该委托实例,代码如下:
委托类型派生自.NET Framework中的Delegate类。委托类型是密封的,不能从Delegate中派生委托类型,也不可能从中派生自定义类。由于实例化委托是一个对象,所以可以将其作为参数进行传递,也可以将其赋值给属性。这样,方法便可以将一个委托作为参数来接受,并且以后可以调用该委托,这称为异步回调,是在较长的进程完成后用来通知调用方的常用方法。以这种方式使用委托时,使用委托的代码无需了解有关所用方法实现方面的任何信息。此功能类似于接口所提供的封装。
虽然委托允许间接调用任何数量的方法,但仍然必须显示调用委托。许多情况下,都需要在发生某个事件时,让委托自动运行。在.NET Framework中,事件允许定义和捕捉特定的事件,并安排调用委托来处理发生的事情。
首先要声明一个拟用作事件来源的类,然后在这个类中声明一个事件。事件来源通常都是一个类,它负责监视它的环境,并在发生某个事件时引发一个事件。声明一个事件时,采用的方式与声明一个字段非常的相似。然而,由于事件要随同委托一起使用,所以事件的类型必须是一个委托,而且必须在声明前有一个event关键字做为前缀,其语法格式如下:
其中事件修饰符就是以前常提到的访问修饰符,如new、public、protected、internal、private、static。事件所声明的类型(type)则必须是delegate类型,而此委托类型应预先声明。
下面的代码演示了如何声明一个事件:
上面的代码首先声明了一个委托类型EventHandler。然后在类MyClass中使用EventHandler声明一个事件Click。
声明了事件后还要对事件进行订阅,事件的订阅是通过为事件加上左操作符“+=”来实现的,例如:
上面的代码把方法加入到事件集合中的语法和把方法加入委托相同,这样,只要事件被触发,所订阅的方法就会被调用。
事件撤消则采用左操作符“-=”来实现:
上面的代码将方法从事件的内部委托集合中移除,把这种称为取消订阅。
和委托一样,可以调用方法来调用事件,从而引发该事件,引发一个事件时,所有连接的委托都会按顺序调用。下面是调用事件的代码:
以上代码中,当类MyClass的OnClick方法被调用时,就触发了Click事件。这是一种常见的写法,null的判断是必须的,因为事件字段显示为null,只有在一个方法使用+=操作符来订阅它之后,才能变为非null。如果试图引发一个null事件,就会得到一个错误。如果定义事件的委托需要参数,那么在引发事件时,必须提供合适的参数。下面以一个完整的示例说明事件的声明及使用。
【实例3-13】事件的使用
本例使用C#的事件机制,实现标准的事件,自定义显示当前时间的事件。当用户在控制台输入姓名后,调用自定义的事件输出欢迎辞和当前的时间。
01 启动Visual Studio 2012,创建一个名为“实例3-13”的控制台应用程序。
02 在解决方案资源管理器中单击程序目录下的Program.cs文件,在该文件的Program类中编写如下逻辑代码:
上面的代码中第1行声明一个TimeEventHandler的委托,包含一个字符串类型的参数。第2行~第7行定义一个MyTime的类,其中第3行声明了TimeEventHandler委托类型的事件Timer;第4行~第7行定义方法OnTimer,其中第5行判断Timer事件如果不为空,则在第6行引发事件。第9行~第13行定义一个ProcessTime的类,其中第10行~第12行定义GenerateTime方法处理事件,第11行输出欢迎辞和当前的时间。第14行~第21行定义Main方法。第16行获得用户的输入。第17行实例化ProcessTime类的对象p。第18行实例化MyTime类的对象t。第19行将事件处理程序添加到Timer事件的调用列表中,即订阅事件。第20行通过调用OnTimer方法使用事件。
03 按快捷键Ctrl+F5,程序运行的效果如图3-19所示。
图13-19 运行结果