C# 第六篇 面向对象设计
一、什么是面向对象设计
对象是事物存在的实体,如人类、书桌、计算机、高楼大厦等。
1.1 类的概念
类和结构实际上都是创建对象的模板,每个对象都包含数据,并提供了处理和访问数据的方法。类定义了类的每个对象(称为实例)可以包含什么数据和功能。类是同一类事物的统称,如果将现实世界中的一个事物抽象成对象,类就是这类对象的统称,比如鸟类、家禽类人类等。
class PhoneCustomer { public const string DayOfSendingBill = "Monday"; public int CustomerID; public string FirstName; public string LastName; }
1.2 面向对象程序设计的三大基本特征
三大基本特征:封装、继承和多态。
1.2.1 封装
核心思想,将对象的属性和行为封装起来,称之为类,类对客户隐藏其实现的细节,这就是封装的思想。
1.2.2 继承
多个类的同一类特性可被继承,继承是实现重复利用的重要手段,子类通过继承复用父类的属性和行为,同时又添加了子类特有的属性和行为。
1.2.3 多态
将父类对象应用于子类的特征就是多态。例如,创建一个螺丝类,螺丝又两个属性,粗细和螺纹密度,再创建两个类,一个长螺丝类与短螺丝类。并且他们继承螺丝类。子类不仅继承父类的特征也有其自身的特征,这样的效果就是多态化结构。
二、类
类是一种数据结构,它可以包含数据成员(变量与常量)、函数成员(方法、属性、构造函数和析构函数等)和嵌套类型。
2.1 类的声明
使用class关键字来声明类,语法如下:
class 类名{}
2.2 类的成员
类的定义包括类头和类体两个部分,其中类头是class关键字定义的类名,类体是用一对{}大括号括起来的。
在类体中主要定义类的成员,类的成员包括字段、属性、方法、构造函数等。
2.2.1 字段
字段就是程序中的常量或变量。
2.2.2 属性
属性是对现实实体特征的抽象,提供对类或对象的访问。属性声明语法如下:
[权限修饰符][类型][属性名] //指定属性的访问级别,指定属性的类型,属性的名称 { get{get访问器体} //相当于一个具有属性类型返回值的无参数方法 set{set访问器体} //相当于一个具有单个属性类型值参数和void返回类型的方法 }
get 为可读 set 为可写
2.2.3 构造函数
构造函数是一种特殊的函数,它是在创建对象时执行的方法。构造函数具有与类相同的名称,通常用来初始化对象的数据成员。构造函数特点如下:
没有返回值,构造函数的名称要与本类的名称相同,定义语法如下:
public class Book { public class Book //无参数构造方法 { } public class Book(int args) //有参数构造方法 { } }
2.2.4 静态构造函数
静态构造函数只执行一次,编写静态构造函数的主要原因是有一些静态字段或属性,需要在第一次使用时,从外部源中初始化。
public class Book { static class Book //静态构造函数 { } }
2.2.5 析构函数
析构函数主要用来释放对象资源,.NET Framework 类库具有垃圾回收功能,当某个类的实例被认为不再有效,并符合析构条件时,类会被回收。
析构函数是以~加类名来命名的。例如:
~program() { }
一个类中,只能定义一个析构函数。
2.3 权限修饰符
C#中的权限修饰符主要包括private、protected、internal、protected internal和public,这些权限修饰符控制着对类和类的成员变量、方法的访问。
权限修饰符 | 应用范围 | 访问范围 |
private | 所有类和成员 | 只能在本类中访问 |
protected | 类和内嵌类的所有成员 | 在本类和其子类中访问 |
internal | 类和内嵌类的所有成员 | 在同一程序集中访问 |
protected internal | 类和内嵌类的所有成员 | 在同一程序集和子类中访问 |
public | 所有类和成员 | 任何代码都可以访问 |
在定义类时,只能使用public或internal,这取决于是否希望在包含类的程序集外部访问它。不能使用private、protected、protected internal定义类,因为这些权限修饰符对于包含在命名空间的类是没有意义的,只能用于成员,但是可以使用他们定义内嵌类。
namespace demo { public class program { private class test { } } }
如果有内嵌类型,内么内嵌类型总是可以访问外部类型的所有成员。
三、方法
方法是用来定义类可执行的操作,它包含一系列语句的代码块。从本质上讲,方法就是和类相关联的动作。
3.1 方法的声明
方法在类或结构中声明时需要指定访问级别、返回值、方法名称及方法参数,方法参数放在括号中,并用逗号隔开。也可声明无参数方法,基本格式如下:
修饰符 返回值类型 方法名(参数列表) { //方法的具体实现; }
修饰符可以是private、protected、internal、public中的任何一个。返回值类型是指方法返回数据的类型(可以任意类型),如果不需要返回值,则使用void关键字。
如方法有返回值,则必须使用return关键字返回一个指定类型的数据。
3.2 方法的参数
在调用方法时,可以给方法传递一个或多个值,传给方法的值称为实参。在方法内部,接收实参的变量称为形参。形参在紧跟着方法名的括号中声明。C#中方法的参数主要有4种,分别为值参数、ref参数、out参数和params参数。
3.2.1 值参数
值参数是在声明时不加修饰的参数,它表明实参与形参之间按值传递。
3.2.2 ref参数
按引用传递,对形参的修改会传递到实参当中。
3.2.3 out参数
用来定义输出参数,会导致参数通过引用来传递,REF必须在传递前进行赋值,OUT定义的参数可不赋值使用。要使用out参数,则方法声明和调用都必须显示使用。
3.2.4 params参数
声明时,如有多个相同类型的参数,则可以使用params,它是一个一维数组,主要用来指定在参数数目可变时所采用的方法参数。
3.3 重载方法
重载方法是指方法名相同,但参数的数据类型、个数或顺序不同的方法
四、类的静态成员
C#程序中,把共享的变量或方法用static进行修饰,它们被称作静态变量或静态方法,也可称为类的静态成员。如果在声明类的时使用static关键字,则该类是一个静态类。静态类的成员必须是静态的,不能定义实例变量、实例方法或实例构造函数。
class program { public static int add(int x,int y) { return x+y; } static void Main(string[] args) { console.WriteLine("{0}+{1} = {2}",23,34,program.add(23,34)); } }
五、对象
所有的问题都可通过对象来处理,对象可以操作类的属性和方法解决问题,所以了解对象的产生、操作和销毁尤关重要。
5.1 对象的创建
对象可以认为是一类事物的抽象出某一个特例,通过这个特例来处理这类事物出现的问题。C#中通过new操作符来创建对象。创建对象语句如下:
Test test = new Test(); Test test = new Test("a");
Test 类名;test 创建Test类对象;new 创建对象操作符;"a" 构造函数的参数;
当用户使用new操作符创建一个对象后,可以使用“对象.类成员”来获取对象的属性和行为。对象的属性和行为在类中是通过类成员变量和成员方法的形式来表示的,所以当对象获取类成员时,也就相应地获取了对象的属性和行为。
public class cStocklnfo //自定义库存商品类 { public string FullName 〃自动实现属性 { get; set; } public void ShowGoods () //定义一个无返回值类型的方法 ( Console.WriteLine ("库存商品名称:”); Console .WriteLine (FullName); //输出属性值 } } class Program { static void Main(string(] args) { cStocklnfo stockinfo = new cStocklnfo (); //创建 cStocklnfo 对象 stockinfo. FullName = ”笔记本电脑七 //使用对象调用类成员属性 stockinfo.ShowGoods () ; //使用对象调用类成员方法 Console.ReadLine (); } }
5.2 对象的销毁
C#拥有一套完整的垃圾回收机制,用户不必担心废弃的对象占用内存,垃圾回收器将回收无用的但占用内存的资源。
在介绍垃圾回收机制之前,首先需要了解何种对象会被.NET垃圾回收器视为垃圾,主要包括以下两种情况。
1、对象引用超过其作用范围,则这个对象将被视为垃圾;
2、将对象赋值为null;
5.3 类与对象的关系
类是一种抽象的数据类型,但是其抽象的程度可能不同,而对象是一个类的实例。例如,将农民看作一个类,张三和李四就可以各为一个对象。
类是具有相同(或相似)结构、操作和约束规则的对象组成的集合,而对象是某一类的具体化实例,每个类都是具有某些共同特征的对象的抽象。
六、继承
继承是面向对象程序设计重要的特性之一,它源于人们认识客观世界的过程,是自然界普遍存在的一种现象。
在程序设计中实现继承,表示这个类拥有它继承的类的所有公有成员或受保护成员。在面向对象程序设计中,被继承的类称为父类或基,实现继承的类称为子类或派生类。
6.1 继承的实现
继承的基本思想是基于某个基类扩展出一个新的派生类,派生类可以继承基类原有的属性和方法,也可以增加原来基类所不具备的属性和方法,或者直接重写基类中的某些方法。
在C#中使用“:”来标识两个类的继承关系。在继承一个类时,类成员的可访问性是一个重要的问题。派生类(或子类)不能访问基类(或父类)的私有成员,但是可以访问其公共成员,也就是说,只要使用public声明类成员,就可以让一个类成员被基类(或父类)和派生类(或子类)同时访问,同时也可以被外部的代码访问。
为了解决基类成员的访问问题,C#还提供了另外一种可访问性:protected (它表示受保护成员)。只有基类(或父类)和派生类(或子类)才能访问protected成员,外部代码不能访问protected成员。
class Goods { } class JHInfo:Goods 〃正确:继承单个类 { }
1、C#只支持单继承,而不支持多重继承,即在C#中一次只允许继承一个类,不能同时继承多个类。
2、在实现类的继承时,子类的可访问性一定要低于或等于父类的可访问性。例如,下面的代码是错误的。
创建一个Computer类用来作为父类,再创建一个Pad类,继承自Computer类,重写父类方法,并使用base关键字调用父类方法原有的逻辑,代码如下:
//父类:电脑 class Computer { public string sayHello() { return "欢迎使用"; } } //子类:平板电脑 class Pad:Computer { public new string sayHello() { //子类重写父类方法 return base.sayHello() + "平板电脑"; } } class Program { static void Main(string[] args) { //子类调用父类方法,在结果后添加字符串 Computer pc = new Computer(); Console.WriteLine(pc.sayHello()); //电脑类 Pad ipad = new Pad(); Console.WriteLine(ipad.sayHello()); Console.ReadLine(); } }
第10行代码在子类中定义sayHello方法时,使用了一个new关键字,这是因为子类中的sayHello方法与父类中的sayHello方法同名,而且返回值与参数完全相同,这时,在父类中调用sayHello方法会产生歧义,所以加了new关键字来隐藏父类的sayHello方法。
6.2 base关键字
如果子类重写了父类的方法,就无法调用父类的方法了吗?如果想在子类的方法中实现父类原有的方法怎么办?为了解决这些问题,C#提供了 base关键字。
base关键字的使用方法与this关键字类似。this关键字代表本类对象;base关键字代表父类对象,使用方法如下。
base.property; //调用父类的属性 base.method(); //调用父类的方法
如果要在子类中使用base关键字调用父类的属性或方法,则父类的属性和方法必须定义为public类型或protected类型。
使用base关键字可以指定创建子类实例时应调用的父类构造函数。例如在父类Goods中定义一个构造函数,用该构造函数为定义的属性赋初始值,原始代码如下:
class Goods { public string TradeCode { get; set; } //定义商品编号 public string FullName { get; set; } //定义商品名称 } class JHInfo : Goods //继承 Goods 类 { public string JHID{get; set; } //定义进货编号 public void showinfo() //输出进货信息 { Console.WriteLine("进货编号:{0}\n商品编号:{1}\n商品名称:{2}", JHID, TradeCode, FullName); } } class Program { static void Main(string[] args) { JHInfo jh = new JHInfo(); //创建 JHInfo 对象 jh.TradeCode = "100001"; //设置父类中的 TradeCode 属性 jh.FullName = "笔记本电脑"; //设置父类中的FullName属性 jh.JHID = "JH00001"; //设置JHID属性 jh.showinfo(); //输出信息 Console.ReadLine(); } }
调整如下:
public class Goods(string tradecode,string fullname) { TradeCode = tradecode; FullName = fullname; }
在子类JHInfb中定义构造函数时,即可使用base关键字调用父类的构造函数,代码如下。
public JHInfo(string jhid, string tradecode, string fullname):base(tradecode,fullname) { JHID = jhid; }
注意:访问父类成员只能在构造函数、实例方法或实例属性中进行,因此,在挣态方法中使用base关键字是错误的。
6.3 继承中的构造函数与析构函数
在进行类的继承时,派生类的构造函数会隐式调用基类的无参构造函数,但是,如果基类也是从其他类派生的,则C#会根据层次结构找到顶层的基类,并调用基类的构造函数,然后再依次调用各级派生类的构造函数。析构函数的执行顺序正好与构造函数相反。继承中的构造函数和析构函数执行顺序示意图如图所示。
七、多态
多态使得派生类的实例可以直接赋予基类的对象,然后直接通过这个对象调用派生类的方法。在C#中,类的多态性是通过在派生类中重写基类的虚方法来实现的。
7.1 虚方法的重写
在C#中,方法在默认情况下不是虚拟的,但(除了构造函数)可以显式地声明为virtual。在方法前面加上virtual关键字,则称该方法为虚方法。例如,下面代码声明一个虚方法。
public virtual void Move() { Console.WriteLine("交通工具都可以移动"); }
将方法定义为虚方法后,可以在派生类中重写虚方法。重写虚方法需要使用0verride关键字,这样在调用方法时,可以调用对象类型的相应方法。
public override void Move() { Console.WriteLine("火车都可以移动"); }
类中的成员字段和静态方法不能声明为virtual,因为virtual只对类中的实例函数和属性有意义。
示例代码:
class jtgj { string name; //定义字段 public string Name //定义属性为字段赋值 { get { return name; } set { name = value; } } public virtual void Move() { Console.WriteLine("{0}都可以移动",Name); } } class Train : jtgj { public override void Move() { Console.WriteLine("{0}在铁轨上行驶", Name); } } class Car : jtgj { public override void Move() { Console.WriteLine("{0}在公路上行驶", Name); } } class Program { static void Main(string[] args) { jtgj JTGJ = new jtgj(); //创建交通工具类的实例 Train train = new Train(); //创建Train类的实例 Car car = new Car(); //创建Car类的实例 //使用基类和派生类对象创建交通类的数组 jtgj[] jtgjs = { JTGJ, train,car }; JTGJ.Name = "交通工具"; train.Name = "火车"; car.Name = "汽车"; jtgjs[0].Move(); jtgjs[1].Move(); jtgjs[2].Move(); Console.ReadLine(); } }
7.2 抽象类与抽象方法
如果一个类不与具体的事物相联系,而只是表达一种抽象的概念或行为,仅作为其派生类的一个基类,那么这样的类就可以声明为抽象类。
在C#中声明抽象类时需要使用abstract关键字,其具体语法格式如下:
访问修饰符 abstract class 类名:基类或接口 { //类成员 }
在声明抽象类时,除abstract关键字、class关键字和类名外,其他的新是可选项。
抽象类主要用来提供多个派生类可共享的基类的公共定义,它与非抽象类的主要区别如下。
1、抽象类不能直接实例化。
2、抽象类中可以包含抽象成员,但非抽象类不可以。
3、抽象类不能被密封。
使用抽象类模拟去商场买衣服的场景,然后通过派生类确定到底去哪个商场买衣服,买什么样的衣服,示例代码如下:
public abstract class Market { public string Name { get; set; } //商场名称 public string Goods { get; set; } //商品名称 public abstract void Shop(); //抽象方法,用来输出信息 } public class WallMarket : Market //继承抽象类 { public override void Shop() //重写抽象方法 { Console.WriteLine(Name + "购买"+ Goods); } } class Program { static void Main(string[] args) { Market market = new WallMarket(); //使用派生类对象创建抽象类对象 market.Name ="沃尔玛"; market.Goods ="七匹狼西服"; market.Shop(); Console.ReadLine(); } }
7.3 接口的使用
由于c#中的类不支持多重继承,但是客观世界岀现多重继承的情况又比较多,所以为了避免传统的多重继承给程序带来的复杂性等问题,同时保证多重继承带给程序员的诸多好处,在c#中引出了接口的概念,通过接口可以实现多重继承的功能。
接口提出了一种约定,或者称为规范,让使用接口的程序设计人员必须严格遵守接口提出的约定。接口就可以看作这种标准,它强制性地要求派生类必须实现接口约定的規范,以保证派生类必须拥有某些特性。
在C#中声明接口时,需要使用interface关键字,其语法格式如下。
修饰符 interface 接口名称:继承的接口列表 { 接口内容; }
接口可以继承其他接口,类可以通过其继承的基类(或接口)多次继承同一个基类。
接口中的成员默认是公共的,因此,不允许加访问修饰符。
接口具有以下特征:
1、接口类似于抽象基类,继承接口的任何类型都必须实现接口的所有成员。
2、接口中不能包含构造函数,因此不能直接实例化接口。
3、接口可以包含属性、方法、索引器和事件。
4、接口中只能定义成员,不能实现成员。
5、接口中定义的成员不允许加访问修饰符,因为接口成员永远是公共的。
6、接口中的成员不能声明为虚拟或静态。
拓展:接口实现范例