在学习设计模式之前,C#语言中一些基本的面向对象的知识还是应该具备的,比如像继承、多态,接口、抽象类,集合、泛型等。
A.2 类与实例
什么是对象?
一切事物(事和物)都是对象,对象就是可以看到、感觉到、听到、触摸到、尝到、或闻到的东西。
准确地说,对象是一个自包含的实体,用一组可识别的特性和行为来标识。
什么是类? 类就是具有相同的属性和功能的对象的抽象的集合。
实例化: 实例就是一个真是的对象。实例化就是创建对象的过程,使用 new 关键字来创建。
A.3 构造方法
构造方法,又叫构造函数,其实就是对类进行初始化。
构造方法与类同名,无返回值,也不需要 void,在 new 时候调用。
所有类都有构造方法,如果你不编码则系统默认生成空的构造方法。若你有定义的构造方法,那么默认的构造方法就会失效了。
A.4 方法重载
方法重载提供了创建多个同名的方法的能力,但这些方法需使用不同的参数类型。
注意:方法重载时,这些方法必须要方法名相同,但参数类型或个数必须要有所不同,否则重载就没有意义了。
方法重载的意义:
方法重载可在不改变原方法的基础上,新增功能。——方法重载算是提供了函数可扩展的能力。
A.5 属性与修饰符
属性,本质上是一个或一对方法,但对于调用它的代码来说,它是一个字段。
因此,属性适合于以字段的方式使用方法调用的场合。(除了最简单的 get、set,还可以加入一些其他语句)
(字段:字段是与类相关的变量,用于存储类要满足其设计所需要的数据。字段通常是私有的类变量)
修饰符:
public: 所修饰的类成员可以允许其他任何类来访问,俗称公有的。
protected: 对子类公开,但不对其他类公开。(在父类中有些字段既想被继承,有不想对其它类公开,这时选用 protected )
private: 只允许同一个类中的成员访问,其他类包括它的子类无法访问,俗称私有的。(如果没加修饰符则默认为 private)
通常字段都是私有的(private),而属性都是公有的(public)。
由于是对外的,所以属性的名称一般首字母大写,而字段则一般首字母小写或前加“_”
对于对外界公开的数据,我们通常希望能做更多的控制,这就好像我们的房子,我们并不希望房子是透明的,那样你在家里的所有活动
全部都被看得清清楚楚,毫无隐私可言——如果把字段声明为 public,那就意味着不设防的门窗,任何时候,调用者都可以读取或写入,
这是非常糟糕的一件事。如果把对外的数据写成属性,那情况就会好很多。——多了层控制
附:对于方法而言同样存在公有和私有,一般不需要对外界公开的方法都应该设置其修饰符为 private(私有)。这才有利于 “封装”。
A.6 封装
每个对象都包含它能进行操作所需要的所有信息,这个特性称为封装,因此对象不必依赖其他对象来完成自己的操作。
封装有很多好处:
第一、 良好的封装能够减少耦合,
第二、 类内部的实现可以自由地修改,
第三、 类具有清晰的对外接口。(定义的 public 属性和方法)
(代码有大量重复不会是什么好事情——所以接下来的继承也很重要)
A.7 继承
在继承关系中,继承者(子类)可以完全替换被继承者(父类),反之则不成立。
继承定义了类如何相互关联,共享特性。
继承的工作方式是,定义父类和子类(或叫做基类和派生类),其中子类继承父类的所有特性。子类不但继承了父类的所有特性,还可以定义新的特性。
第二、子类具有自己的属性和功能,即子类可以拓展父类没有的属性和功能;
第三、子类还可以以自己的方式实现父类的功能。(方法重写)
在 C# 中,子类从它的父类中继承的成员有:方法、域、属性、事件、索引指示器。(但对于构造方法有一些特殊,它只能被调用,不能被继承)
对于调用父类的成员,可以用 base 关键字。(子类构造方法需要调用父类同样参数类型的构造方法,用 base 关键字代表父类)
public Cat () : base () { }
public Cat (string name) : base(name) { }
不用继承的话,如果要修改功能,就必须在所有重复的方法中修改,代码越多,出错的可能性就越大,而继承的优点是,继承使得
所有子类公共的部分都放在了父类,使得代码得到了共享,这就避免了重复,另外,继承可使得修改或扩展继承而来的实现都较为容
易。
继承也是有缺点的,那就是父类变,则子类不得不变——所以对父类的修改一定要小心,父类一定要是所有子类公共的部分。
另外,继承会破坏包装,父类实现细节暴露给子类(一个类的实现细节会被继承它的类所看到),这其实是增大了两个类之间的耦合
性——继承显然是一种类与类之间强耦合的关系——一方的改变会影响到另一方,这就是耦合性高的表现。
那什么时候用继承才是合理的呢?——当两个类之间具备 “is-a” 的关系时(B是A,类似“猫是动物”)
A.8 多态
多态表示不同的对象可以执行相同的动作,但要通过它们自己的实现代码来执行
这里有几点需要注意:
第一、子类以父类的身份出现
第二、子类在工作时以自己的方式来实现——以父类的身份调用子类的方法
第三、子类以父类的身份出现时,子类特有的属性和方法不可以使用
虚方法 + 方法重写:
为了使子类的实例完全接替来自父类的类成员,父类必须将该成员声明为虚拟的。—— 在返回类型前加 virtual 关键字
通常虚拟的是方法,但其实除了字段不能是虚拟的,属性、事件和索引器都可以是虚拟的。
尽管方法可以是虚拟的,但虚方法还是有方法体的,可以实际做些事情。
然后,子类可以选择使用 override 关键字,将父类实现替换为它自己的实现,这就是方法重写。
需要注意的是:对象的声明必须是父类,而不是子类,实例化的对象是子类,这才能实现多态。
要深刻理解并会合理利用多态,不去研究设计模式是很难做到的。也可以反过来说,没有学过设计模式,那么对多态、乃至对面向
对象的理解多半都是肤浅和片面的。
A.9 重构
有时子类方法依然存在重复,继续将重复的部分放到父类中(并且在父类中去掉原来的 virtual),把不重复的部分单独拿出来写成一
个虚方法(virtual 修饰)
此时的子类就极其简单了——除了构造方法和重写虚方法之外,所有的重复都转移到了父类。
由于不重复,所以需求的更改都不会影响到其他类。(这其实也是一个设计模式,叫模板方法,详见第10章)
A.10 抽象类
对于实例化没有任何意义的父类,可以改成抽象类。(比如 Animal 类其实根本就不可能实例化的,因为一只猫长什么样可以想象,但实例化一个动物,动物长什么样?没有具体对象与之对应)
C# 允许把类和方法声明为 abstract,即抽象类和抽象方法。(抽象方法没有方法体,直接在括号后加“;”)
抽象类需要注意几点:
第一,抽象类不能实例化; (“动物”实例化是没有意义的)
第二,抽象方法是必须被子类重写的方法; (不重写的话它的存在是没有意义的)(抽象方法可以看成是没有实现体的虚方法)
第三,如果类中包含抽象方法,那么类就必须定义为抽象类,不论是否还包含其他一般方法。
抽象类应该拥有尽可能多的共同代码,拥有尽可能少的数据。
到底什么时候应该用抽象类呢?
抽象类通常代表一个抽象概念,它提供一个继承的出发点,
当设计一个新的抽象类时,一定是用来继承的,
所以,在一个以继承关系形成的等级结构里面,树叶节点应当是具体类,而树枝节点均应当是抽象类。
(也就是说,具体类不是用来继承的。我们作为编程设计者,应该要努力做到这一点)
A.11 接口
接口是把隐式公共方法和属性组合起来,以封装特定功能的一个集合。
一旦类实现了接口,类就可以支持接口所指定的所有属性和成员。
声明接口在语法上与声明抽象类完全相同,但不允许提供接口中任何成员的执行方式。所以接口不能实例化,不能有构造方法和字段,不能有修饰符,比如 public、private 等;
还有,实现接口的类必须要实现接口中的所有方法和属性。
接口用 interface 声明,接口名称前要加 “I”,接口中的方法或属性前面不能有修饰符,接口中的方法没有方法体。
抽象类和接口的区别:
从表象上来说,抽象类可以给出一些成员的实现,接口却不包含成员的实现;抽象类的抽象成员可被子类部分实现,接口的成员
需要实现类完全实现;一个类只能继承一个抽象类,但可实现多个接口等等。
但这些都是从两者的形态上去区分的,我觉得还有三点能帮助我们去区分抽象类和接口:
第一,类是对对象的抽象,抽象类是对类的抽象,接口是对行为的抽象。(接口是对类的局部(行为)进行抽象;而抽象类是对类整体的抽象)
第二,如果行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象类。
第三,这一点更加关键,从设计角度讲,抽象类是从子类中发现了公共的东西,泛化出父类,然后子类继承父类,而接口是根本不知道子类的存在,方法如何实现还不确认,预先定义。——这里说明的是抽象类和接口设计的思维过程。
(在实例中,先是有一个 Cat 类,然后再有一个 Dog 类,观察后发现它们有类似之处,于是泛化出 Animal 类,这也体现了敏捷开发的思想,通过重构改善既有代码的设计。 事实上,只有小猫的时候,你就去设计一个动物类,这就极有可能会成为过度设计了。 所以说抽象类往往都是通过重构得来的,当然,如果你事先意识到多种分类的可能,那么事先就设计出抽象类也是完全可以的)
(而接口就完全不是一回事,比如我们是动物运动会的主办方,在策划时大家一商议,觉得应该设置如跑得最快、跳得最高、飞得最远……等比赛项目,而此时,主办方其实还不清楚会有什么样的动物来参加运动会,所有的这些比赛项目都可能是完全不相同的动物在比,它们将如何去实现这些行为也不得而知,此时,能做的事就是事先定义这些比赛项目的行为接口)
要想真正的把抽象类和接口用好,还是需要好好用心地去学习设计模式。只有真正把设计模式理解了,那么你才能算是真正会合理应用抽象类和接口了。
A.12 集合
数组在内存中连续存储,因此:
可以快速而容易地从头到尾遍历元素,
可以快速修改元素等等。
缺点:创建时必须要指定数组变量的大小,还有,在两个元素之间添加元素也比较困难。
所以,.NET Framework 提供了用于数据存储和检索的专用类,这些类统称为集合。这些类提供对 堆栈、队列、列表 和 哈希表 的支持。(大多数集合类实现相同的接口)
A.13 泛型
泛型是具有占位符(类型参数)的类、结构、接口和方法。这些占位符是类、结构、接口和方法所存储或使用的一个或多个类型的占位符。
泛型集合类可以将类型参数用作它所存储的对象的类型的占位符;类型参数作为其字段的类型和其方法的参数类型出现。用法上关键就是在 IList 和 List 后面加“<T>”,这个 “T”
就是你需要指定的集合的数据或对象类型。
用接口 IList 声明一泛型集合变量 IList<Animal> arrayAnimal;
注意:IList<Animal> 表示此集合只能接收 Animal 类型,其他不可以。
实例化 List 对象 arrayAnimal = new List<Animal>();
注意,此时也需要指定 List<T> 的 “T” 是 Animal
其实,List 和 ArrayList 在功能上是一样的,不同就在于,List 在声明和实例化时都需要指定其内部项的数据或对象类型,这就避免了
类型安全问题和装箱拆箱的性能问题。
通常情况下,都建议使用泛型集合,因为这样可以获得类型安全的直接优点而不需要从基集合类型派生并实现类型特定的成员。 此外,
如果集合元素为值类型,泛型集合类型的性能通常优于对应的非泛型集合类型(并优于从非泛型基集合类型派生的类型),因为使用泛型时
不必对元素进行装箱。
A.14 委托与事件
委托是对函数的封装,可以当做给方法的特征指定一个名称。
委托是一种引用方法的类型。 一旦为委托分配了方法,委托将与该方法具有完全相同的行为。
而事件则是委托的一种特殊形式,当发生有意义的事情时,事件对象处理通知过程。—— 事件其实就是设计模式中观察者模式在 .NET 中
的一种实现方式。
委托对象用关键字 delegate 来声明。
事件对象用 event 关键字声明。—— 在发生其他类或对象关注的事情时,类或对象可以通过事件通知它们。
public delegate void CatShoutEventHandler(); //声明委托 CatShoutEventHandler
public event CatShoutEventHandler CatShout; //声明事件 CatShout,它的事件类型是委托 CatShoutEventHandler
这里声明了一个叫做 CatShoutEventHandler 的委托,而这个委托所能代表的方法是无参数、无返回值的方法。
然后声明了一个对外公开的 public 事件 CatShout,它的事件类型是委托,表明事件发生时,执行被委托的方法。