1.03k likes | 1.17k Views
第 6 章 C# 的面向对象设计. 6.1 面向对象程序设计的基本概念. 面向对象的程序设计( OOP) 的主要思想是将数据及处理这些数据的操作都封装到一个称为类( Class )的数据结构中。使用这个类时,只需要定义一个类的变量即可,这个变量叫做对象( Object )。通过调用对象的数据成员完成对类的使用。 类 在面向对向理论中,类( class) 就是对具有相同特征的一类事物所做的抽象(或者说,归纳)。显然,用户绝对不会把掌上电脑同 MP3 播放机混淆在一起,因为它们分别属于两种不同的类:“电子计算机类”和“随身听设备类”。
E N D
第6章 C#的面向对象设计
6.1 面向对象程序设计的基本概念 • 面向对象的程序设计(OOP)的主要思想是将数据及处理这些数据的操作都封装到一个称为类(Class)的数据结构中。使用这个类时,只需要定义一个类的变量即可,这个变量叫做对象(Object)。通过调用对象的数据成员完成对类的使用。 • 类 • 在面向对向理论中,类(class)就是对具有相同特征的一类事物所做的抽象(或者说,归纳)。显然,用户绝对不会把掌上电脑同MP3播放机混淆在一起,因为它们分别属于两种不同的类:“电子计算机类”和“随身听设备类”。 • 在使用面向对象程序设计语言进行程序设计的过程中,需要依照程序的功能定义各种各样的类,这些类代表着程序中所存在着的各种事物的抽象(也就是归纳出来的共同特征)。
6.1 面向对象程序设计的基本概念 2. 对象 类是一种抽象,而对象(object)则是实例(instance),是具体的。 “书”是一种类,它是所有书籍的总称。而“一本书名为《Visual C#程序设计教程》的书”就是“书”这个类的一个对象,它是很具体地存在着的一本书。 在面向对象方法中,类通常被当作一种模板,对象是通过模板生成的。可以简单地这样理解:对象是使用类这个“模子”,一个个地“印制”出来的,一个类可以 “印制”出多个对象。 如果使用如下格式来代表一个类生成一个对象: 类名 对象名; 则: 电子计算机 ComputerA; 电子计算机 ComputerB; 就代表ComputerA和ComputerB是“电子计算机”类的两个不同对象。
6.1 面向对象程序设计的基本概念 3. 属性 属性说明了这个类的特点。 例如“PC计算机”类则可能具备如下属性: 计算机的名称 CPU类型 内存容量 硬盘容量 主板型号 显示适配器型号 声卡型号 操作系统类型 通过这些属性,就可以将不同的PC计算机区分开。
6.1 面向对象程序设计的基本概念 • 4. 方法 • 类的方法(method)代表了一类事物所具备的动作,是对属性的操作。 • 比如:“石英钟”类的方法有:秒针前进一格、分秒针前进一格、时针前进一格等,而“录像机”类所具备的方法可以有:播放、定格、录像、倒带、快进等。 • 封装 • 封装就是把对象的属性和服务结合成一个独立的系统单位,并尽可能隐蔽对象的内部细节。
6.1 面向对象程序设计的基本概念 5.继承 面向对象的程序设计允许编程人员对类进行继承(inheritance),继承后的类仍具有被继承类的特点,同时又出现新的特点。在类的继承中,被继承的类称为基类(又称为父类),由基类继承的类称为派生类(又称为子类)。派生类自动获得基类的所有属性和方法,而且可以在派生类中添加新的属性和方法。 如汽车类可以用于描述一辆普通汽车所共有的和必须的所有属性和方法,在需要定义奔弛汽车的个性化属性和方法时,可以通过继承汽车类,添加奔驰汽车专有的属性和方法,从而产生奔驰汽车类;同样的方式还可以构成宝马汽车类。 6. 多态性 多态性时指在一般类中定义的属性或行为,被特殊类继承之后,可以具有不同数据类型或表现出不同的行为。就是程序在运行时,会自动判断对象的派生类型,并调用相应的方法。 例如:某个属于“笔”基类的对象,在调用它的“写”方法时,程序会自动判断出它的具体类型,如果是毛笔,则调用毛笔对应的“写”方法,如果是铅笔,则调用铅笔对应的“写”方法。
6.1.1 类 1. 类的声明 几乎所有的C#程序都是在类的基础上构建起来的。 格式: [类修饰符] class 类名[:基类类名] { 类的成员; } 说明:C#支持的类修饰符有:new、public、protected、internal、private、abstract、sealed。
1. 类的声明 例:定义一个车辆类(有三个变量) class Vehicle { public int passengers; //乘客数 public int fuelcap; //所耗燃料 public int mpg; //每公里耗油量 } 让同学们定义一个书类。
1. 类的声明 类建立好后,就可以定义该类的对象。分两步: (1)先定义对象 格式: 类名 对象名; 例:Vehicle minivan; //定义类Vehicle的一个对象 (2) 然后创建类的实例。 格式:对象名=new 类名( ); 例:minivan=new Vehicle( ); //创建一个实例 以上两步也可以合并成一步。 格式: 类名 对象名=new 类名( ) ; 例: Vehicle minivan =new Vehicle( ); 让同学们定义书类的对象
2. 类的成员 在C#中,按照类的是否为函数将其分为两大类: 一种不以函数形式体现,称为成员变量; 一种是以函数形式体现,称为成员函数。 类的具体成员见书上第109页。 类成员的访问修饰符有: (1)public:允许类的内部或外界直接访问; (2)private:不允许外界访问,也不允许派生类访问,即只能在类的内部访问。如果没有设定访问修饰符,默认为private; (3)protected:不允许外界访问,但允许派生类访问; (4)internal:只有本类的成员才能访问。
class ClassA { public int a; private int b; protected int c; public void SetA( ) { a=1; //正确,允许访问类自身公有成员 b=2; //正确,允许访问类自身私有成员 c=3; //正确,允许访问类自身保护成员 } } class ClassB:ClassA { public void SetB( ) { ClassA BaseA=new ClassA( ); BaseA.a=5; //正确,允许访问基类公有成员 BaseA.b=23; //错误,不允许访问基类私有成员 BaseA.c=68; //正确,允许访问基类保护成员 } } class ClassC { public void SetB( ) { ClassA BaseA=new ClassA( ); BaseA.a=5; //正确,允许访问类的其他公有成员 BaseA.b=23; //错误,不允许访问类的其他私有成员 BaseA.c=68; //错误,不允许访问类的其他保护成员 } }
2. 类的成员 静态成员与非静态成员 静态成员是在声明成员时在前面加上static保留字; 非静态成员是在声明成员时前面没有static保留字; 静态成员属于类所有,非静态成员属于类的对象所有。 静态成员的访问格式: 格式:类名.静态成员名
类的静态成员 若将类中的某个成员声明为static,则该成员称为静态成员。类中的成员要么是静态的,要么是非静态的。 类的非静态成员属于类的实例所有,每创建一个类的实例都在内存中为非静态成员开辟了一块区域。而类的静态成员属于类所有,为这个类的所有实例所共享。无论这个创建了多少个对象(实例),一个静态成员在内存中只占有一块区域。
例: using System; class Test { int x; static int y; void F( ) { x=1; //正确,可以直接访问非静态成员 y=1; //正确,可以直接访问静态成员 } static void G( ) { x=1; //错误,不能直接访问非静态成员 y=1; //正确,等价于Test.y=1 } static void Main( ) { Test t=new Test( ); t.x=1; //正确 t.y=1; //错误,不能在类的实例中访问静态成员 Test.x=1; //错误,不能通过类访问类中非静态成员 Test.y=1;//正确 } }
例: using System; class Myclass { public int nIndex=10; static public double fphi=45.6; } class classTest { static void Main( ) { int a=Myclass.nIndex; //错误,因为nIndex是非静态成员 double b= Myclass.fphi; //正确,因为fphi是静态成员 Console.Write(b); } }
例: using System; class A { public static int x=B.y+1; } class B { public static int y=3; static void Main() { Console.WriteLine"X={0},Y={1}",A.x,B.y); } } 运行结果:
using System; class myClass { public int a; //定义一个非静态成员函数 static public int b; void Fun1( ) {a=10; //正确,非静态成员函数可以直接访问非静态成员 b=20; //正确,非静态成员函数可以直接访问静态成员 } static void Fun2( ) {a=10; //错误,静态成员函数不可以直接访问非静态成员 b=20; //正确,静态成员函数可以直接访问静态成员,相当于myClass.b=20 } } class Test { static void Main( ) { myClass A=new myClass( ); //定义对象A A.a=10; //正确,访问类myClass非静态公有成员变量a A.b=20; //错误,不能通过对象直接访问静态公有成员 myClass.a=20; //错误,不能通过类访问类中非静态公有成员 myClass.b=20; //正确,可以通过类访问类myClass的静态公有成员变量 } } 例:
6.1.2 对象 格式: 对象名.成员函数名 或 对象名.数据 例:using System; class Vehicle { public int passengers; //乘客数 public int fluecap; //所耗燃料 public int mpg; //每公里耗油量 } class Demo { static void Main( ) { Vehicle minivan=new Vehicle( ); minivan.passengers=7; minivan.fluecap=16; minivan.mpg=21; int range=minivan.fluecap* minivan.mpg; Console.WriteLine(“乘客数为{0}”, minivan.passengers) Console.WriteLine(“可行驶{0}公里”, range) } } 运行结果如下:
例:(书P112) using System; class Pen { public string Color; private int Price; public void SetPrice(int newPrice) { Price=newPrice; } public int GetPrice( ) { return Price; } public void SetColor(string newColor) { Color=newColor; } public string GetColor() { return Color; } } class Test { public static void Main() { Pen myPen=new Pen(); myPen.SetPrice(5); myPen.Color="BLACK"; Console.WriteLine("The Price is {0}",myPen.GetPrice()); Console.WriteLine("The Color is {0}",myPen.Color); } } 运行结果如下:
例: using System; class myTest {static int instance=1; public static int GetInstance( ) { return instance++; } } class Test { public static void Main( ) { Console.WriteLine(myTest.GetInstance( )); Console.WriteLine(myTest.GetInstance( )); } } 运行结果如下:
例:using System; class Readon { public readonly string x=“declare field”; //字符型只读变量 public readonly int y=10; //整型只读变量 public int z=30; //公共变量 public ReadonlyFieldExam( ) { x=“It is a readonly field”; //在构造函数中可以修改只读变量的值 y*=2; z*=3; } public void Modifyfield( ) { // y=200; //错误 ,无法在类的方法中对只读变量赋值 z=600; //正确,可以修改公共变量z的值 } static void Main( ) { Readon exam=new Readon( ); exam.Modifyfield( ); Console.WriteLine(“x={0}”,exam.x); Console.WriteLine(“y={0}”,exam.y); Console.WriteLine(“z={0}”,exam.z); exam.z=100; //exam.y=exam.z; //错误,无法对只读变量赋值 Console.WriteLine(“z={0}”,exam.z); } } 运行结果如下:
6.2 方法、属性、索引和事件 在编写一个很长的程序时,可以把这个大的程序分割成一些相对独立而且便于管理和阅读的小块程序。这样对程序员和其他用户都很方便。 可以按照程序代码执行的功能或其他依据把相关的语句组织在一起,并给它们注明相应的名称,利用这种方式把程序分块,就形成了类的方法。 方法的功能是通过方法调用实现的。方法调用指定了被调用方法的名字和调用方法所需的信息(参数),调用方法要求被调用方法按照方法参数完成某个任务,并在完成这项任务后由方法返回。如果调用过程出错,则无法完成正常的任务。
6.2.1 方法 方法是类中用于计算或进行其他操作的成员。类的方法主要用来操作类的数据,提供一种访问数据的途径。 格式: 方法修饰符 返回类型 方法名(方法参数列表) { 方法实现部分; } 说明:如果省略“方法修饰符”,该方法为类的私有成员。 “返回类型”指定该方法返回数据的类型,它可以是任何有效的类型。如果方法不需要返回一个值,其返回类型必须是void。 “方法参数列表”是用逗号的类型、标识符对。这里的参数是形参,本质上是一个变量,它用来在调用方法时接收实参传给方法的值,如果方法没有参数,那么参数列表为空。
1. 静态方法 类的成员类型有静态和非静态两种,因此方法也有静态方法和非静态方法两种。使用static 修饰符的方法称为静态方法,没有使用static 修饰符的方法称为非静态方法。 静态方法和非静态方法的区别是:静态方法属于类所有,非静态方法属于用该类定义的对象所有。 2.虚方法 当方法声明中包含virtual修饰符时,方法就被称为虚方法。当没有virtual修饰符时,方法被称为非虚方法,虚方法定义中不能包含static、abstract或override修饰符。
3.覆盖方法 当方法的声明中包含override修饰符时,则此方法被称为覆盖方法。覆盖方法覆盖一个有相同声明的虚方法。覆盖方法声明通过提供一个方法的具体执行代码来使继承而来的、已经存在的虚方法具体化。覆盖方法声明中包含new、static或virtual修饰符中的任何一个都是错误的,而覆盖方法声明可以包含abstract修饰符,即虚方法可以可以被一个抽象方法覆盖。
4.抽象方法 当方法声明中包含abstract修饰符时,这个方法被称为抽象方法。抽象方法隐含的也是一个虚方法。抽象方法声明只在抽象类中被允许。抽象方法声明中包含static或virtual修饰符是错误的。抽象方法声明引入新的虚方法,但是不提供这个方法的执行。 5.外部方法 当方法声明中包含extern修饰符时,这个方法就被称为外部方法。外部方法在外部执行。因此外部方法声明并不提供实际的执行代码,所以外部方法的主体完全由冒号组成。
6.2.2 属性 属性是对现实世界中实体特征的抽象,它提供了对类或对象的特性的访问机制。比如:用户姓名、窗口标题等都可以作为属性。属性所描述的是状态信息,在类的某个实例中属性值表示该对象的状态值。 1.属性的声明 格式: [属性修饰符] 属性的类型 属性名称{方法声明} 说明:类的属性成员的访问方法,同类的域成员完全一样,若属性是静态成员,通过“类名.属性成员名”访问;若属性是非静态成员,通过“对象名.属性成员名”访问。虽然可以像操作变量一样操作一个属性成员,但是在属性成员内部需要向外界提供某个表达式的值,或者接受外界的值以便修改某个域成员。
通过访问器,类的属性成员可以返回一个值(get访问器),或者接受外界通过赋值语句提供的值(set访问器)。通过访问器,类的属性成员可以返回一个值(get访问器),或者接受外界通过赋值语句提供的值(set访问器)。 2.get访问器 格式: get {语句} 说明:get访问器用于向外界返回属性成员的值。通常,get访问器中的语句主要是用return语句返回某一个域成员的值。 例:class Circle { protected int page=2; public int pa { get {return page; } } } class Test {static void Main( ) {Circle MyCircle=new Circle( ); int n=MyCircle.pa*10; //读pa属性 int m=MyCircle.page*10; //错误!Page是protected } }
3.set访问器 格式: set {语句} 说明:set访问器用于外界写入的值。set访问器就像带有一个参数的方法,这个参数的名字是value(注意:参数value是隐含的,不能再定义),它的值就是调用者要写入属性的值。 例:class Circle { protected int page; public int pa { get {return page; } set {page=value;} } } class Test {static void Main( ) {Circle MyCircle=new Circle( ); MyCircle.pa=10; // 对pa属性进行写操作 int n=MyCircle.pa*10; //读pa属性 Console.WriteLine(“n={0}”,n); } } 运行结果如下:
既然可以将一个域成员定义为具有public权限,从而在类的外界直接修改它的值,为什么还需要定义属性成员呢?这主要是因为public成员没有任何安全措施。比如,如果一个类中的某个域成员的值不允许为0,若直接将其定义为具有public权限,则外界就可以将它赋值为0,就是不安全的。通过属性来设置域成员的值,就可以在访问器中加入代码,以判断数据的合法性。既然可以将一个域成员定义为具有public权限,从而在类的外界直接修改它的值,为什么还需要定义属性成员呢?这主要是因为public成员没有任何安全措施。比如,如果一个类中的某个域成员的值不允许为0,若直接将其定义为具有public权限,则外界就可以将它赋值为0,就是不安全的。通过属性来设置域成员的值,就可以在访问器中加入代码,以判断数据的合法性。 例:以下是使用条件运算符进行运算,从而保证类ClassA的nIndex不为负数。 class ClassA { private int nIndex; public int Index { get { return nIndex; } set { nIndex=value>=0?value:0; } } }
例: using System; class Circle { protected int page; public int pa { get {return page; } set { if(value!=0) page=value; else Console.WriteLine("error!"); } } } class Test {static void Main( ) { Circle MyCircle=new Circle( ); MyCircle.pa=0; int n=MyCircle.pa*10; Console.WriteLine("n={0}",n); } } 运行结果如下:
属性定义可以包含get和set两个访问器的定义,也可以只包含其中的一个。根据get和set访问器的存在或不存在,属性按下面特征进行分类:属性定义可以包含get和set两个访问器的定义,也可以只包含其中的一个。根据get和set访问器的存在或不存在,属性按下面特征进行分类: .既包括get访问器也包括set访问器的属性被称为读写属性。 .只包括get访问器的属性被称为只读属性。一个只读属性被赋值是错误的。 .只包括set访问器的属性被称为只写属性。
例: using System; class File { private string s_filename; public string Filename { get {return s_filename; } set { if(s_filename!=value) s_filename=value; } } } class Test {static void Main( ) { File f=new File( ); Console.WriteLine(“请输入文件名:"); f. Filename=Console.ReadLine( ); string s= f. Filename; Console.WriteLine(“已将文件名设定为{0}",s); } } 运行结果如下:
6.2.3 索引 • 索引器是这样一个成员:它能够让对象以类似数组的方式来访问,即可以使对象能用下标来得到一个值。它的引入也是为了使编写的程序更加直观、简洁、易于理解,它以访问数组的方法来访问类的数据成员,而实际的读/写操作则是通过get和set来完成的。尤其对于一些特殊类型,其核心的数据结构包含有数组,通过索引器可以把类的对象直接当作数组来访问,所以索引器也被称为“聪明数组(smart array)”。 • 索引的定义与使用 • 格式: 修饰符 类型名 this[参数列表] • { • set { } • get { } • } • 说明:在set方法中,可以使用一个特殊变量value,用以表示用户指定的值,而get方法使用return返回所得到的值,这与属性相似,但这里没有属性名,而是用this表示索引。 • 使用参数列来表示索引的参数,这与方法相似,但与方法不同的是,索引至少需要一个参数,用方括号[],而不是圆括号(),同时没有方法名,只用this。
例: using System; class MyIndexer { private string [ ] myArray=new string[4]; public string this[int index] { get { if(index<0||index>=4) return null; else return myArray[index]; } set { if(!(index<0||index>=4)) myArray[index]=value; } } } class MainClass { static void Main() { MyIndexer idx=new MyIndexer(); idx[0]="vivid"; idx[1]="Miles"; for(int i=0;i<=3;i++) Console.WriteLine("Element #{0}={1}",i,idx[i]); } } 运行结果如下:
6.2.4 事件 事件为类和类的实例提供了向外界发送通知的能力,是一个使对象或类可以提供公告的成员。用户可以通过提供事件句柄为事件添加可执行代码。 事件的声明分为两种,一种是事件域声明,一种是事件属性声明。 事件声明的格式如下: [事件修饰符] event 事件类型 事件名; 事件属性声明的格式如下: [事件修饰符] event 事件类型 事件名 {访问器}; 事件的类型(type)则必须是一个委托类型,而此委托类型应预先声明。 例如:public delegate void EventHandler
6.2.4 事件 例: public delegate void EventHandler(object sender,Event e); public class Button:Control { public event EventHandler Click; protected void OnClick(Event e) { if (Click !=null) Click(this,e); } public void Reset() { Click = null; } } 触发一个事件的概念与调用由事件成员表示的委托正好相同。
6.3 构造函数与析构函数 C#中有两个特殊的函数:构造函数和析构函数。 构造函数是当对象创建时首先自动执行的函数,主要用于为类中的实例变量赋初值。析构函数是当实例(也就是对象)从内存中销毁时自动执行的函数,主要用来处理对象用完后的收尾工作。 这里所说的自动是指这两个函数的执行是无条件的,系统会自动在创建对象时调用构造函数,而在销毁对象时调用析构函数。
6.3.1 构造函数 在C#中,当创建一个对象时,系统首先为这个对象赋予一个标识符,然后给对象分配合适的内存空间,随后系统就自动调用对象的构造函数。 无论是否定义,所有的类都有构造函数,因为C#自动提供一个默认的构造函数来把类中的实例变量初始化为0或null。但是,一旦你定义了自己的构造函数,默认的构造函数就不再使用。 构造函数的函数名必须和类的名字一样,语法上与方法类似。 构造函数的类型修饰符总是public,因为构造函数主要是在类外创建对象时自动调用。 格式: class 类名 { public 类名( ) { //构造函数体 } }
6.3.1 构造函数 例:using System; class MyClass { public int x; public Myclass() //类MyClass的构造函数 { x=10; } } class ConsDemo { public static void Main() { MyClass t1=new MyClass(); MyClass t2=new MyClass(); Console.WriteLine(“{0}\t{1}”,t1.x, t2.x); } } 运行结果如下:
6.3.1 构造函数 例:using System; class Fruit { public string color; public string shape; public Fruit() //类Fruit的构造函数 { color=“orange”; shape=“round”; } } class Test { public static void Main() {Fruit Orange=new Fruit(); //创建Orange实例 Console.WriteLine(“{0},{1}”,Orange.color, Orange.shape); } } 运行结果如下:
例: using System; class Pen { public string Color; private int Price; public Pen( ) //类Pen的构造函数 { Color=“BLACK”; Price=5;} public void SetPrice(int newPrice) { Price=newPrice; } public int GetPrice( ) { return Price; } public void SetColor(string newColor) { Color=newColor; } public string GetColor() { return Color; } } class Test { public static void Main() { Pen myPen=new Pen(); Console.WriteLine("The Price is {0}",myPen.GetPrice()); Console.WriteLine("The Color is {0}",myPen.Color); } } 运行结果如下:
6.3.2 析构函数 当某个对象用完后,要将其从内存中清除前,必须释放它所占用的内存,这时就需要用到类的析构函数。 定义析构函数与定义构造函数非常相似,析构函数的名称是在类名前加一个“~”符号。 格式: class 类名 { …… ~类名( ) { //析构函数体 } } 注意:如果类没有析构函数,系统就自动调用默认的析构函数来完成内存的回收。在实际编程过程中,一般不需要析构函数。 析构函数以与构造函数相反的顺序被调用。
例:using System; class Decon1 { public Decon1( ) { Console.WriteLine(“调用构造函数Decon1”); } ~Decon1( ) { Console.WriteLine(“调用析构函数Decon1”); } } class Decon2 { public Decon2( ) { Console.WriteLine(“调用构造函数Decon2”); } ~Decon1( ) { Console.WriteLine(“调用析构函数Decon2”); } } class Test { public static void Main() { Decon1 dec1=new Decon1( ); Decon2 dec2=new Decon2( ); } } 运行结果如下:
从上面的输出结果可以看出:程序在执行过程中,依次调用构造函数Decon1、Decon2, 当Main( )方法运行到方法结束的花括号处时,析构函数被依次调用,其顺序正好与构造函数相反。
6.3.3 静态构造函数 静态构造函数也称类构造函数,在第一次使用类之前调用。 通过在构造函数名称的前面使用关键字“static”,就可以定义一个静态的构造函数。静态构造函数 自动被调用,它的调用原则: (1)静态构造函数总是在该类的所有静态变量初始化之后执行。 (2)静态构造函数总是在该类被使用(如访问静态变量、生成实例)之前被调用。 (3)静态构造函数最多被调用一次。
6.3.3 静态构造函数 书第121页例: using System; class Welcome { public Welcome() { Console.WriteLine("构造函数 Welcome()"); } static Welcome() { Console.WriteLine("构造函数 static Welcome()"); } static void Main() { new Welcome(); new Welcome(); } } 运行结果如下:
6.3.3 静态构造函数 例: using System; class A { static A() { Console.WriteLine("Init A"); } public static void F() { Console.WriteLine("A.F()"); } } class test { static void Main() { A.F(); } } 运行结果如下:
6. 4. 1 继承 C#中的派生类只能从一个类中继承。 格式: class 基类类名 { 类的成员; }; class 派生类类名:基类类名 { 类的成员; } 说明:C#中继承的规则见书第123页。