第3章 C#面向对象基础
面向对象编程的英文简称是OOP(Object Oriented Programming),该项技术是目前运用最广泛的程序化设计方法,几乎已经完全取代了过去的面向过程编程。C#从一诞生开始,就是为面向对象编程所准备的。类是面向对象编程的核心部件,它描述了一组具有相同特性和行为的对象。基于面向对象的应用程序,就是由几个或几十个甚至更多的类组成,且类之间总是保持着或多或少的关系。类其实也是数据类型,所以在面向对象编程中程序员可以自定义数据类型,这和面向过程编程是有本质区别的。
本章主要内容:
● 类的定义
● 类的数据成员和函数成员
● 事件和委托
● 接口的使用
● 面向对象的三大特征
● 其他面向对象的主题
3.1 类的基本概念
在C#中,类可以看成是一种数据结构,它自身封装了数据成员和函数成员等。其中数据成员包括字段、常量和域等,而函数成员主要包括方法、属性、事件、索引器和操作符等。本节将对类的结构和用法进行详细说明。
3.1.1 C#中的类定义
在C#中,用class关键字来定义类,基本结构如下所示。
[attributes] [modifiers] class identifier [:base-list] { class-body }
其中attributes表示附加的声明信息,modifiers表示类的访问修饰符,identifier表示类的名称,base-list表示继承的基类和实现的接口的列表,基类在前,接口在后,且用逗号隔开。
在C#中,类的访问修饰符主要分为public、private、protected和internal。它们的具体用法如下所示。
public:它具有最高的访问级别,对访问公共成员没有限制; private:它的访问级别最低,仅限于它的包含类; protected:能在它的包含类或包含类的派生类中访问; internal:只能在同一程序集的文件中。
原则上,一个类只能使用一种访问修饰符,但有一个特例需要注意,具体如下所示。
protected internal:仅限于从包含类派生的当前程序集或类型。
还有一些修饰符能与以上修饰符相结合,得到一些特殊的限制,比如abstract和sealed等,如下所示。
public abstract:可以在任何地方访问,但不能实例化,只能继承; public sealed:可以在任何地方访问,只能实例化,不能派生。
3.1.2 字段
字段实际上相当于类的变量,它在类中的应用十分广泛,看一个简单的例子,如下面代码所示。
public class Car { public string Name; public string Color; public double Price; } Car car = new Car(); car.Name="BMW"; car.Color="White"; car.Price=80000.00;
在上例中,定义了一个Car类,它包含了三个字段,分别为Name、Color和Price。通过在类的外部对该类进行实例化,对类的字段进行赋值。这里需要注意的是,类中的字段也必须要定义成public类型才能在类的外部被访问。
3.1.3 常量
常量在类中所处的地位和字段差不多,只是它不可变而已。通常,定义常量用关键字const,如下面代码所示。
public const int age = 25;
3.1.4 域
域的声明过程和字段比较相似,但它们之间有一个很重要的区别,即域只能声明在类的内部,而不能声明在类的方法的内部。域分为实例域和静态域,实例域只能通过类的实例进行调用,而静态域可以直接通过类名进行调用。下面看一个例子,代码如下。
class Student { public static int count = 37; //声明域 static void Main(string[] args) { Console.WriteLine("学生人数为:"+Student.count.ToString()); Console.ReadKey(); } }
运行结果如图3-1所示。
图3-1 运行结果图
以上代码声明了一个静态域count,因为是静态的,所以它能直接用类名进行调用。此外,域也可以分为公有域和私有域。公有域可以在类的外部被修改,而私有域(也称为只读域)不能在类的外部被修改,声明私有域的关键字是readonly,如下面的例子所示。
class Student { public readonly int count = 37; //声明私有域 static void Main(string[] args) { Student sd = new Student(); Console.WriteLine("学生人数为:"+sd.count++); Console.ReadKey(); } }
如果试图运行该程序,会出现如图3-2所示的错误信息。
图3-2 错误列表
这是因为域count是私有域,用readonly关键字标识的私有域不能在外部被修改。如果去掉readonly关键字,则count变为公有域,此时运行后输出结果如图3-3所示。
图3-3 运行结果
3.1.5 类的方法
在C#中,方法的定义与其他语言一样,包括三个部分,分别为访问修饰符、输入参数和返回类型。方法的访问修饰符的类型和类的差不多,如表3-1所示。
表3-1 方法修饰符
类的方法被创建以后,必须要被调用才有意义。下面的代码是调用类的方法的例子。
public abstract class Compute { public virtual int Method(int x, int y) { return x + y; } } public class Use:Compute { public override int Method(int x, int y) { return base.Method(x, y); } static void Main(string[] args) { Use use = new Use(); Console.WriteLine(use.Method(2,3)); Console.ReadKey(); } }
上面的代码中首先定义一个抽象类Compute,它只能被继承,此外它包含了一个虚拟方法Method(),该方法只能在它的包含类的派生类中被重写后才能使用。然后定义Compute类的派生类Use,并且在Use类中重写Method()方法。最后通过实例化Use类,输出结果,如图3-4所示。
图3-4 运行结果
方法被调用时,参数传递十分重要。同其他编程语言一样,C#中方法的参数传递也分为值传递和引用传递。它们之间的区别比较难以理解,下面通过例子来说明。
public class Use { public int Method1(int x) { x=3*x; return x; } public int Method2(ref int x) { x=3*x; return x; } static void Main(string[] args) { Use use = new Use(); int x = 2; Console.WriteLine("输出结果是:" + use.Method2(ref x)); //输出引用传递 //的结果 Console.WriteLine("输出结果是:"+use.Method1(x)); //输出值传递的结果 Console.ReadKey(); }
以上代码主要定义了两个方法,Method1()是值参数类型,而Method2()引用参数类型,最后输出它们的结果,如图3-5所示。
图3-5 运行结果
此时,将两个输出语句的顺序颠倒,具体如下所示。
Console.WriteLine("输出结果是:"+use.Method1(x)); //输出值传递的结果 Console.WriteLine("输出结果是:" + use.Method2(ref x)); //输出引用传递的结果
此时再看输出结果,如图3-6所示。
图3-6 运行结果
比较以上两个输出结果,发现并不相同。原因是引用参数使方法引用的是原来的变量,而值参数引用的仅是原来变量的副本。交换输出语句前的程序中,因为先调用了引用参数传递,所以更改了原来的变量,使x从2变成了6,所以值传递的结果为18。相反,当值传递先被调用时,它并没有改变原来的变量,所以两个输出结果均为6。
3.1.6 类的属性
类的属性提供比较灵活的机制来读取、编写或计算私有字段的值,可以像使用公有数据成员一样使用属性。属性必须要由访问器进行读写,它的一般声明格式如下所示。
[attributes] [modifiers] type identifier { declaration }
其中attributes表示附加的声明信息,modifiers表示修饰符,和类的方法的修饰符差不多,type表示属性类型,declaration表示对属性的读写操作。如下面的例子所示。
class Fruit { private string name; public string Name { get { return name; } set { name = value; } } static void Main(string[] args) { Fruit fr = new Fruit(); fr.Name = "Apple"; Console.WriteLine("这种水果为:"+fr.Name); Console.ReadKey(); } }
运行结果如图3-7所示。
图3-7 运行结果
属性的一个重要特点是含有get和set访问器,set用于写,get用于读。它们也可以缺省,只含有get访问器的属性为只读属性,只含有set访问器的属性为只写属性。将上面的代码替换为如下所示代码。
class Fruit { private string name; public string Name { get { return name; } } static void Main(string[] args) { Fruit fr = new Fruit(); fr.name = "Apple"; Console.WriteLine("这种水果为:"+fr.Name); Console.ReadKey(); } }
此时的Name为只读属性,所以必须要对name进行赋值才能正确输出结果,输出结果与图3-7一致。
3.1.7 类的索引器
索引器是C#所特有的类成员,它的主要作用是对象能向数组一样被方便地引用。索引器的声明与属性的声明比较类似,如下所示。
Public type this[index] { get { //代码 } set { //代码 } }
索引器具有以下特点。
(1)索引器没有具体的名字,需要用this关键字对对象进行索引。this关键字指向被访问成员所在的当前实例,可以在构造函数和实例方法中实现对成员的访问,但不能访问静态成员。
(2)索引器不能定义为静态的。
(3)索引器的参数index只能是传值类型,不能出现ref和out关键字。
下面是一个关于索引器的例子。
class Student { public string[] Name = new string[10]; public Student() { for (int i = 0; i < 10; i++) { Name[i] = i.ToString(); } } public string this[int index] //创建索引器 { get { string str; if (index >= 0 && index < 10) { str = Name[index]; } else { str = "null"; } return str; } set { if (index >= 0 && index < 10) { Name[index] = value; } } } static void Main(string[] args) { Student sd = new Student(); sd[3] = "zhangwei"; sd[4] = "weiyi"; sd[5] = "lirong"; for (int i = 0; i < 12; i++) { Console.WriteLine(sd[i]); } Console.ReadKey(); } }
上面的代码通过索引器实现了对Name数组的访问,运行结果如图3-8所示。
图3-8 运行结果
3.1.8 类的构造函数和析构函数
类的构造函数能被编译器自动执行,它具有以下特点。
(1)构造函数必须与类同名。
(2)构造函数不能有返回类型。
(3)当访问一个类时,它的构造函数最先被执行。
(4)一个类可以有多个构造函数,如果没有定义构造函数,系统会自动生成一个默认的构造函数。
构造函数又分为实例构造函数和静态构造函数,其区别如表3-2所示。
表3-2 实例构造函数与静态构造函数
下面通过例子进行说明,示例代码如下。
class Student { public static int x; public int y; public int z; public int m; public int n; //构造函数一 public Student() { y = 2; z = 2; } //构造函数二 public Student(int m, int n) { this.m = m; this.n = n; } //构造函数三 static Student() { x = 5; } static void Main(string[] args) { Console.WriteLine("x="+Student.x); Student sd1 = new Student(); Console.WriteLine("y={0}, z={1}", sd1.y, sd1.z); Student sd2 = new Student(3,3); Console.WriteLine("m={0}, n={1}", sd2.m, sd2.n); Console.ReadKey(); } }
本例共为Student类创建了三个构造函数,其中构造函数三是静态构造函数。例子运行结果如图3-9所示。
图3-9 运行结果
类的析构函数与构造函数的过程刚刚相反,它主要用于销毁类的实例。它不能带有参数,不能含有修饰符,不能被调用,且它也必须与类同名。为了区别于构造函数,通常在前面加符号“~”。其语法如下所示。
class Student { …… ~Student() { …… } }
析构函数通常会被自动执行,只有一些非常特殊的情况下才需要被用到,比如非托管资源的清理。
3.1.9 事件
事件相关知识的内容太多,在本章的后面部分将用单独一节进行讲解。
3.2 Visual Studio中的类向导
在Visual Studio 2008(以下简称VS2008)中,提供了创建类和类的成员的快捷方式,在本节中将通过例子进行详细说明。其创建步骤如下。
(1)打开VS2008,在D:\C#\ch3目录下建立名为“ClassWizard”的控制台应用程序。打开“解决方案资源管理器”界面,右键单击当前工程“ClassWizard”,选择“添加”—“新建项”命令,添加新类Book.cs,如图3-10所示。
图3-10 添加类
(2)选择“视图”—“类视图”菜单,如图3-11所示。
图3-11 类视图
此时可以看出当前工程的类的结构图,包含两个类,Book类和Program类,其中Book类是刚被创建的,而Program类是随工程一起被创建的。在图3-11中,下半部分窗格主要是显示被选中类的具体成员。因为Book类是刚被创建的,所以它不含有任何成员。
(3)现在开始为Book类添加成员,在图3-11中,右键单击“Book”,选择“查看类关系图”命令,弹出如图3-12所示的对话框。右键单击空白处,选择“添加”命令,其菜单如图3-13所示。
图3-12 类的关系图
图3-13 添加类的成员
(4)为Book类添加一个bookname字段,其属性设置如图3-14所示;然后添加一个Price属性,如图3-15所示。
图3-14 添加字段
图3-15 添加属性
(5)接着再添加方法Count,如图3-16所示。
图3-16 添加方法
(6)添加完以上成员后,在“解决方案资源管理器”下打开Book.cs,代码如下所示。
class Book { private string[] bookname; public double Price { get { throw new System.NotImplementedException(); } set { } } public double Count() { throw new System.NotImplementedException(); } }
从以上代码可以看出,bookname字段、Price属性和Count方法已经添加到Book类中。其中.NotImplementedException()表示无法实现请求的方法或操作时引发的异常。将上面的代码替换为如下所示代码。
class Book { //字段 public string[] bookname = new string[4]; public double price; //属性 public double Price { get { return price; } set { price = value; } } //方法 public double Count(double Price) { Price = Price * 0.8; return Price; } }
在Main()函数中添加如下代码。
Book book = new Book(); book.bookname[0] = "C# Programming"; book.bookname[1] = "C++ Programming"; book.bookname[2] = "C Programming"; book.bookname[3] = "Java Programming"; for (int i = 0; i < 4; i++) { Console.WriteLine("书名:" + book.bookname[i]); Console.WriteLine("原价:"); string s = Console.ReadLine(); Console.WriteLine("打折后的价格:"); Console.WriteLine(book.Count(Convert.ToDouble(s))); } Console.ReadKey();
程序运行结果如图3-17所示。
图3-17 程序运行结果
本例主要是通过VS2008中的类向导实现了类成员的添加,完成的功能是将Book类中的书名,书的原价和书打折后的价格输出。
3.3 事件和委托
事件是C#中的又一个重要概念,在发生其他类或对象需要关注的事情时,本类或对象可以通过事件来通知它们。发送事件的类称为事件的发送者,而接收事件的类称为事件的订阅户。
3.3.1 委托
委托是事件应用过程中必不可少的一个环节,委托首先是在Visual J++中提出的,后来被C#引用。如果一个类需要调用另一个类的方法,可以有三种方式,即实例方式、静态方式和委托方式。应用委托调用方法的流程如图3-18所示。
图3-18 委托使用流程图
下面通过一个例子来说明委托的具体用法。打开VS2008,在D:\C#\ch3目录下建立名为DelegateTest的控制台应用程序。打开工程,添加如下代码。
class Test { public delegate void Mydelegate(string str); //声明委托 public int s = 3; public void Method1(string str) //方法一 { s = 5; } public void Method2(string str) //方法二 { s = 7; } public void Call(Mydelegate d, string str) //调用委托中的方法 { d(str); Console.WriteLine(str); } static void Main(string[] args) { Test test = new Test(); Mydelegate d1 = new Mydelegate(test.Method1); //在委托中包含方法一 Mydelegate d2 = new Mydelegate(test.Method2); //在委托中包含方法二 test.Call(d2, "成功调用了委托"); Console.WriteLine("输出结果:"+test.s.ToString()); Console.ReadKey(); } }
代码说明如下。
(1)首先需要定义委托,使用的关键字是delegate,代码如下所示。
public delegate void Mydelegate(string str); //声明委托
它的返回类型和参数必须要和它调用的方法的返回类型和参数相匹配。
(2)接下来是定义该委托需要调用的方法,本例定义了两个方法,用于对字段s进行不同的修改,如下所示。
public void Method1(string str) //方法一 { s = 5; } public void Method2(string str) //方法二 { s = 7; }
(3)创建委托对象,对声明的方法进行包含,如下所示。
Test test = new Test(); Mydelegate d1 = new Mydelegate(test.Method1); //在委托中包含方法一 Mydelegate d2 = new Mydelegate(test.Method2); //在委托中包含方法二
(4)最后即是完成方法的调用,如下所示。
public void Call(Mydelegate d, string str) //调用委托中的方法 { d(str); Console.WriteLine(str); }
这里使用了两个参数,第一个是创建的委托对象,它能直接通过要调用方法的参数完成对方法的调用,第二个参数是调用的方法的参数。程序的运行结果如图3-19所示。
图3-19 运行结果
3.3.2 委托的事件处理程序
前面提到,事件需要订阅者,当事件发生时,订阅者会给出相应的事件处理程序。事件处理程序本身是简单的函数形式,它的参数和返回类型必须和调用它的委托相匹配。委托在这里的作用是包含事件处理程序,当事件被触发时,通过委托来执行事件处理程序。下面通过例子进行说明,主要功能是通过在Class2中定义并触发事件,在Class1中调用事件处理程序。
打开VS2008,在D:\C#\ch3目录下建立名为EventTest的控制台应用程序,打开工程,添加如下所示代码。
namespace EventTest { //继承 public class MyEventArgs : EventArgs { public string str; } //声明委托对象 public delegate void MyEventHandler1(object sender, MyEventArgs e); class Class1 { //创建委托对象并包含事件处理函数 public Class1(Class2 class2) { MyEventHandler1 mh1 = new MyEventHandler1(Method1); //订阅事件 class2.Event1 += mh1; } //事件处理函数 public void Method1(object sender, MyEventArgs e) { Console.WriteLine("事件处理结果:"+e.str); } } //通过委托来调用被包含的方法 class Class2 { //定义事件 public event MyEventHandler1 Event1; //触发事件 public void mEvent1(MyEventArgs e) { if (Event1 ! = null) { Event1(this, e); } } } class Class3 { static void Main(string[] args) { Class2 class2 = new Class2(); Class1 class1 = new Class1(class2); MyEventArgs e1 = new MyEventArgs(); e1.str = "aaa"; class2.mEvent1(e1); Console.ReadKey(); } } }
代码说明如下。
(1)首先还是需要定义委托,事件中的委托和普通委托有一定的区别,它的参数形式比较固定,如下所示。
public delegate void MyEventHandler1(object sender, MyEventArgs e);
事件中的委托具有两个参数,第一个参数表示事件的触发对象,第二个参数表示处理事件的对象,它是从EventArgs类继承而来,EventArgs类包含很多事件处理对象类,在GUI中比较常见,如鼠标事件处理类MouseEventArgs。本例的MyEventArgs是从EventArgs中继承而来(有关继承的知识将会在本章后面部分讲解),如下所示。
public class MyEventArgs : EventArgs { public string str; }
(2)接下来需要在Class2中完成事件的声明和触发,如下所示。
class Class2 { //定义事件 public event MyEventHandler1 Event1; public void mEvent1(MyEventArgs e) { if (Event1 ! = null) { //触发事件 Event1(this, e); } } }
(3)在Class1类中定阅事件并编写事件处理程序,如下所示。
class Class1 { //创建委托对象并包含事件处理函数 public Class1(Class2 class2) { MyEventHandler1 mh1 = new MyEventHandler1(Method1); //订阅事件 class2.Event1 += mh1; } //事件处理函数 public void Method1(object sender, MyEventArgs e) { Console.WriteLine("事件处理结果:"+e.str); } }
在Class1中完成了Class2的Event1事件的订阅,以上工作由委托mh1完成。相应的事件处理代码包含在Method1()中。
(4)最后,输出事件处理结果,如下所示。
Class2 class2 = new Class2(); Class1 class1 = new Class1(class2); MyEventArgs e1 = new MyEventArgs(); e1.str = "aaa"; class2.mEvent1(e1); Console.ReadKey();
程序的运行结果如图3-20所示。
图3-20 运行结果
3.3.3 委托中的GUI事件
从上一节的例子可以看出,事件的应用步骤是比较复杂的。在.NET Framework中,运用事件最多的部分是GUI控件,比如Button的Click(单击)事件。是不是每应用一次事件就必须做出与上例相似的操作?答案是否定的。在GUI事件的应用中,.NET Framework已经完成了大量的工作,下面通过例子进行说明。
注意:以下例子会用到控件知识,不懂控件的读者可先跳过这部分。
打开VS2008,在D:\C#\ch3目录下建立名为GUIEventTest的Windows应用程序,打开工程,为当前窗体添加一个Button控件。右键单击该Button控件对象,选择“属性”,转到“属性”的“事件”窗口,如图3-21所示。
图3-21 事件窗口
从图3-21可以看出,Button包含了很多事件,双击Click事件,窗口中显示的代码如下所示。
private void button1_Click(object sender, EventArgs e) { }
button1_Click()为事件处理函数,它具有两个参数,这在前面已经讲过。需要注意的是它使用的事件处理对象EventArgs类。此时,打开Form1.Designer.cs,在有关Button的设计器代码中有如下语句。
this.button1.Click += new System.EventHandler(this.button1_Click);
可见,编译器已经订阅好了该事件,读者需要做的工作仅是向button1_Click()中添加程序执行代码。比如添加如下所示代码。
this.Text = "GUI事件";
完成对当前窗体标题的修改,运行程序,结果如图3-22所示。
图3-22 运行结果
本节主要介绍了C#中的事件处理机制及委托的用法。C#中的事件主要分为两类,一类是GUI事件,.NET Framework为这类事件的应用做了大量工作,它的使用较简单;另一类是非GUI事件,它的事件声明、订阅和处理都需要读者自己编写,使用比较复杂,但在一些场合它是很必要的。总的来说,事件具有以下特点。
(1)事件的发送者决定何时发送事件,事件的订阅者决定执行何种操作来响应事件。
(2)一个事件可以同时有多个订阅者,一个订阅者可以响应多个事件。
(3)没有订阅者的事件不会被调用。
(4)具有多个订阅者的事件被触发时,会同步调用多个事件处理程序。
(5)在.NET Framework中,事件是基于EventHandler委托和EventArgs基类的。
通过本节的学习,读者应该对事件有了一个比较初步的认识,在以后的章节中具体的应用场合还会继续讲解事件。
3.4 面向对象的特征
面向对象主要具有三大特征,即继承、多态和封装。正因为这些机制的存在,才使得应用程序变得更为简单和丰富多彩。本节将对以上三个特征进行详细介绍,此外还会提到面向对象中另一个重要知识点——重载。
3.4.1 继承
继承是指一个类A能利用另一个类B的资源(包括属性和方法等),其中B类被称为基类(或父类),而A类被称为派生类(或子类)。继承的使用语法如下所示。
public class A { public A{} } public B:A { public B{} }
类的继承用符号“:”进行标识,以上代码定义了B类继承于A类。如果B类继承于A类,那么B类是否能访问A类中的全部成员?答案是否定的。这也就是下面要说明的有关派生类在基类中的访问权限问题。
(1)大多数而并非所有类都可以作为基类被继承,比如带有sealed修饰符的密封类不能被继承。
(2)基类中只有两种成员能被派生类访问,包括public和protected类型的成员。其中,protected类型是专为派生类设计的,该类型的成员只能在派生类中进行访问。
(3)在派生类中可以修改基类中的以下成员,包括虚拟成员(virtual)和抽象成员(abstract)。其中对虚拟成员的修改是在派生类中重写该成员的执行代码;而对于抽象成员而言,它在基类中,没有执行代码,需要在派生类中进行添加。
可能有些读者对于以上的讲解感觉比较抽象,下面通过一个简单的例子进行说明。
打开VS2008,在D:\C#\ch3目录下建立名为InheritTest的控制台应用程序。首先为当前工程添加一个基类ClassF,代码如下所示。
class ClassF { //公有字段 public string name="zhangwei"; public string age; //公有属性 public string Age { get { return age; } set { age = value; } } //虚拟方法 public virtual double Income(double time) { double income = time * 100.0 + 2000.0; return income; } }
基类ClassF中,定义了一个公有字段,一个公有属性和一个虚拟方法。下面是派生类ClassS的代码。
class ClassS:ClassF { //重写虚拟方法 public override double Income(double time) { double income = time * 100.0 + 3000.0; return income; } static void Main(string[] args) { ClassS cs = new ClassS(); Console.WriteLine("姓名:"); //继承公有字段 Console.WriteLine(cs.name); Console.WriteLine("工龄:"); //继承公有属性 cs.Age= Console.ReadLine(); Console.WriteLine("工资:"); //继承虚拟方法 Console.WriteLine(cs.Income(Convert.ToDouble(cs.Age)).ToString()); Console.ReadKey(); } }
在派生类ClassS中,调用了ClassF类中的公有字段name、公有属性Age并重写了虚拟方法Income()。此处,需要注意的是重写虚拟方法需要用到override关键字。运行程序,结果如图3-23所示。
图3-23 运行结果
3.4.2 多态
多态是面向对象的又一个重要特征,它主要是指同一操作(如方法)作用于不同的类的实例,将产生不同的结果。多态主要是通过在派生类中对基类中的成员进行替换或重定义完成。下面通过例子进行说明。
打开VS2008,在D:\C#\ch3目录下建立名为PolymorphismTest的控制台应用程序。打开工程,添加如下代码。
//基类 class ClassF { public virtual void Out() { Console.WriteLine("调用了基类中的方法!"); } } //派生类1 class ClassS1:ClassF { public override void Out() { Console.WriteLine("调用了派生类1中的方法!"); } } //派生类2 class ClassS2:ClassF { public override void Out() { Console.WriteLine("调用了派生类2中的方法!"); } } //输出结果 class Test { static void Main(string[] args) { ClassF[] cf = new ClassF[3]; cf[0] = new ClassF(); cf[1] = new ClassS1(); cf[2] = new ClassS2(); foreach (ClassF c in cf) { c.Out(); } Console.ReadKey(); } }
上面代码演示了多态性的实现过程,通过在两个派生类中重写基类中的虚方法实现。运行结果如图3-24所示。
图3-24 运行结果
如果要在派生类中隐藏基类中的非虚成员,可以使用new关键字。代码如下所示。
class ClassF { public void Out() { Console.WriteLine("调用了基类中的方法!"); } } class ClassS1:ClassF { public new void Out() { Console.WriteLine("调用了派生类1中的方法!"); } } class Test { static void Main(string[] args) { ClassS1 cs = new ClassS1(); Console.WriteLine(cs.Out().ToString()); //调用派生类中的方法 Console.WriteLine(((ClassF)cs).Out().ToString()); //调用基类中的方法 Console.ReadKey(); } }
运行结果如图3-25所示。
图3-25 运行结果
上面代码同样也实现了多态的功能。多态本身理论比较难,本节仅通过两个例子对简单的多态应用进行了说明,多态的主要作用是提高代码的重用性和简化程序结构。
3.4.3 封装
封装是指将对象的信息进行隐藏,只是提供一个访问接口,使它的使用者无法看到对象的具体信息。在类中,通过不同的修饰符能让类的成员实现公开或隐藏。通过这些修饰符,类实现了很好的封装。封装的主要用途是防止数据受到意外的破坏,代码如下所示。
class Test { private int a; public int wr() { return a; } public void rd(int value) { a = value; } } class Program { static void Main(string[] args) { Test ts = new Test(); ts.rd(3); ts.wr(); } }
上面的代码中,使Test类中的私有字段被访问,但又很好地保护了它的数据不被破坏。封装的内容远不止这些,但基本思想是一致的。读者应该注意对封装思想的学习,这样才能很好地应用封装的操作。
3.4.4 重载
重载是面向对象中除三大特征外的又一个重要知识点,它是指在类中同名成员的不同定义。它的主要作用是使程序逻辑更加清晰。重载主要包括方法重载和运算符重载,本节将通过例子对这两者进行详细介绍。
3.4.5 方法重载
方法重载是C#中运用最广泛的一种重载方式,它是指在类中建立名称相同但参数不同的方法。方法重载主要是为了解决操作同一类对象需要使用不同方法的问题,如计算一类图形的面积。图形中包括矩形、圆和椭圆,它们的面积计算公式是不同的,这里就需要用到重载的概念。下面举例进行说明。
打开VS2008,在D:\C#\ch3目录下建立名为Overload1Test的控制台应用程序。打开工程,添加如下代码。
class Area { //计算矩形面积 public int Count(int x, int y) { return x * y; } //计算圆面积 public double Count(double r) { return Math.PI * r * r; } //计算椭圆面积 public double Count(double a, double b) { return Math.PI * a * b; } static void Main(string[] args) { Area area =new Area(); Console.WriteLine("矩形面积为:" + area.Count(4, 6)); Console.WriteLine("圆的面积为:"+area.Count(3.4)); Console.WriteLine("椭圆的面积为:"+area.Count(2.5,3.6)); Console.ReadKey(); } }
图3-26 运行结果
在Area类中分别定义了计算三种图形面积的方法,三种方法具有相同的名称,不同的参数和返回类型,实现了方法的重载。程序运行结果如图3-26所示。
3.4.6 运算符重载
运算符重载主要是为了在类中扩展运算符的功能,以完成一些特殊的操作。重载运算符需要用到operator关键字。下面通过例子进行说明。
打开VS2008,在D:\C#\ch3目录下建立名为Overload2test的控制台应用程序。打开工程,添加如下代码。
class Test { public int real; public int img; public Test(int real, int img) { this.real = real; this.img = img; } public static Test operator +(Test x, Test y) { return new Test(x.real+y.real, x.img+y.img); } static void Main(string[] args) { Test t1 = new Test(2,3); Test t2 = new Test(4,5); Test t3 = t1 + t2; Console.WriteLine("该复数的实部为:" + t3.real); Console.WriteLine("该复数的虚部为:" + t3.img); Console.ReadKey(); } }
这是一个实现复数加法的经典例子,通过在Test类中重载“+”运算符实现了复数的加法。运行结果如图3-27所示。
图3-27 运行结果
在运算符重载的使用过程中,应注意以下情况。
(1)并非所有运算符都能被重载,不能被重载的运算符包括“=”、“? :”、“->”、“new”、“is”、“sizeof”和“typeof”。
(2)重载运算符不能改变原来运算符的优先级和操作数。
(3)比较运算符必须成对重载,如“==”和“! =”, “>”和“<”等。
(4)重载运算符可由派生类继承。
3.5 接口
接口是面向对象中的又一个重要概念,它用于定义类或结构的行为特征。接口包含事件、方法、属性和索引器4种成员,它只包含这些成员的签名而不包含实现,这一点和抽象类比较相似;而且接口不能包含字段,且它的所有成员都必须是公开的。
3.5.1 接口的声明
接口的声明需要采用interface关键字,如下所示。
interface MyInterface { void MyMethod(); string MyProperty { get; } }
在接口MyInterface中,定义了一个方法成员和一个属性。细心的读者可能会发现以上定义中,接口成员没有采用修饰符,如果试图添加任何修饰符,均会出现“修饰符无效”的错误。因为在接口的定义中规定了所有接口成员必须是公开的。
3.5.2 接口的使用
前面提到,接口只能包含成员的签名,不能包含成员的实现。接口成员必须要在继承该接口中的类中才能实现。下面对MyInterface接口中的MyMethod()方法进行实现,代码如下所示。
class Program:Interface1 { public void MyMethod() { Console.WriteLine("实现了该接口的方法!"); } static void Main(string[] args) { Program pg = new Program(); pg.MyMethod(); Console.ReadKey(); } }
运行结果,如图3-28所示。
图3-28 运行结果
上面代码中,实现了接口MyInterface中的MyMethod()方法并完成输出。在使用接口时,应注意以下问题。
(1)接口自身不能被实例化,需要在继承它的类中才能使用。
(2)接口不能包含字段。
(3)接口不能包含静态成员。
(4)接口成员默认是public类型的,不能在接口成员前面加任何修饰符。
(5)类和结构可以从多个接口继承。
(6)接口本身也可以从其他接口继承,它的继承机制和类的继承机制一样。
3.6 面向对象的其他主题
本章前面部分以类为重点介绍了面向对象的相关知识,但仅有类是不够的,面向对象还有一些其他主题,比如命名空间等。在本节中,将对面向对象中其他一些主题进行介绍,以帮助读者更好地理解面向对象的本质。
3.6.1 命名空间
在.NET Framework中,一个命名空间就是一个逻辑的命名系统,它用于指定一个范围,并在该范围组织代码(包括类、接口、结构体和枚举等)。命名空间在前面的代码中已经多次用到,如果需要使用包含在命名空间中的类,则需要使用using指令包含该命名空间,如下所示。
using System; …… Console.WriteLine("命名空间的用法!");
System是命名空间,Console是该命名空间中的一个类。因为包含了System命名空间,所以才能使用它里面的类。同样,也可以用如下方式访问Console类,如下所示。
System.Console.WriteLine("命名空间的用法!");
这里直接使用了显示的命名空间前缀来完成Console类的访问。这是一种不推崇的方法,因为它会增加代码的复杂性。命名空间前面不能加任何修饰符,它的关键字是namespace。在命名空间的内部,也可以包含命名空间,称为命名空间的嵌套,如下所示。
namespace NameSpaceA { namespace NameSpaceB { class Program { } } }
NameSpaceB嵌套在NameSpaceA命名空间中,下面为等效代码。
namespace NameSpaceA.NameSpaceB { class Program { } }
如果一个命名空间的名字很长,但又必须要使用多次,.NET Framwork中提供了一种命名空间别名的方式来简化。如下所示。
using ns=NameSpaceName;
在需要使用该命名空间的地方,可以直接用ns代替。为了帮助读者能更好地理解命名空间的用法,下面给出一个完整的例子进行说明,代码如下所示。
using nsp = NameSpaceA.NameSpaceB.NameSpaceC; //使用命名空间别名 //定义嵌套命名空间 namespace NameSpaceA { namespace NameSpaceB { namespace NameSpaceC { public class cs { public void output() { Console.WriteLine("命名空间的学习!"); } } } } } //调用嵌套命名空间中的类成员 namespace ns { class Program { static void Main(string[] args) { nsp.cs c = new nsp.cs(); c.output(); Console.ReadKey(); } } }
上面的代码主要实现了命名空间的嵌套,命名空间的别名及使用命名空间中包含的类成员,运行结果如图3-29所示。
图3-29 运行结果
3.6.2 程序集
程序集是.NET Framework应用程序的基本构造块,当生成C#应用程序时,VS会在当前工程的Debug目录下生成可移植可执行的文件,通常是.exe或.dll文件。在较大的项目中,程序集的作用是十分明显的。项目经理可以把项目划分成几个单独的模块,由不同的人员进行开发,然各自生成程序集,最后通过一定的方式将这些程序集组合起来即可。
程序集具有以下特点。
(1)程序集以.exe或.dll格式的文件存在。
(2)能在多个应用程序之间实现程序集的共享。
(3)在单个应用程序中可以使用程序集的两个版本。
3.6.3 类库
在.NET Framework中,类库是由命名空间组成,同时又是类、接口和值类型组成的库,这些库能对系统功能进行访问,是建立.NET Framework应用程序、组件和控件的基础。.NET Framework中包含了大量的系统类库供用户使用,调用这些类库时,系统会自动添加,只需用using指令包含类库提供的命名空间即可,比如前面经常使用的System命名空间。
但系统提供的类库有时候并不能完全满足用户的要求,此时就需要自定义类库。下面通过例子说明类库的编写和调用。
打开VS2008,在D:\C#\ch3目录下建立名为ClassLibraryTest的类库程序。打开工程,添加如下代码。
namespace ClassLibraryTest { public class Test { public int Add(int x, int y) { return x + y; } public int Dec(int x, int y) { return x - y; } public int mul(int x, int y) { return x * y; } public int dev(int x, int y) { return x / y; } } }
在ClassLibraryTest命名空间的Test类中定义了4个方法,分别用于计算两个整数加、减、乘和除的结果。选择“生成”—“生成解决方案”命令,或者按F6键将以上代码生成类库。此时转到当前工程的Debug目录下,可以看到名为ClassLibraryTest.dll的动态链接库文件。下面需要做的工作是调用该类库,主要分为以下几个步骤。
(1)打开VS2008,在D:\C#\ch3目录下建立名为ClTest的控制台应用程序,打开工程,首先需要对该类库进行引用。选择“项目”—“添加引用”—“浏览”标签,转到该类库工程的Debug目录,如图3-30所示。
图3-30 添加引用
(2)转到“解决方案资源管理器”界面上,可以看到该类库已经被添加,如图3-31所示。
图3-31 添加类库
(3)需要包含该类库中的命名空间,如下所示。
using ClassLibraryTest;
(4)在当前工程中添加如下代码。
namespace Cltest { class Program { static void Main(string[] args) { Test ts = new Test(); Console.WriteLine("两个数相加的结果为:" + ts.Add(6, 3)); Console.WriteLine("两个数相减的结果为:" + ts.Dec(6, 3)); Console.WriteLine("两个数相乘的结果为:" + ts.mul(6, 3)); Console.WriteLine("两个数相除的结果为:" + ts.dev(6, 3)); Console.ReadKey(); } } }
(5)运行程序,结果如图3-32所示。
图3-32 运行结果
3.7 小结
本章主要介绍了面向对象技术的基本内容。首先是类的相关知识的介绍,类是面向对象技术中最基础也是最重要的内容,本章分别从类的定义、类的访问权限和类的成员等方面对类的用法进行了说明。其中采用public、private、protected和abstract等修饰符设置了类的不同访问权限;类的成员包括常量、字段、属性、索引器、方法和事件等,本章通过例子对以上成员进行了详细的说明。接下来介绍了面向对象的三大特征——继承、多态和封装,它们是面向对象技术的核心部分。在有关接口的内容中简单介绍了接口的声明和使用。最后介绍了面向对象技术中的其他一些主题,包括命名空间、程序集和类库等。