写点什么

大话设计模式 | 0 面向对象基础

用户头像
Puran
关注
发布于: 2020 年 06 月 03 日
大话设计模式 | 0 面向对象基础

大话设计模式》是作者「程杰」通过趣味的场景设置,以诙谐的表达来解读和剖析「面向对象」编程思维和「设计模式」。书中的示例代码是以 .NET 的 C# 语言编写而成。

本文是我对《大话设计模式》的学习系列笔记的第一篇,面向对象基础。

1. 对象与类

首先,一切事物皆为对象。对象是一个自包含的实体,用一组可识别的特性和行为来标识。面向对象编程是指针对对象来进行编程,英文名为 Object-Oriented Programming。

类是具有相同的属性和功能的对象的抽象的集合。

举例来说,「猫」是一个类,「耶啵」这只猫是一个真实的对象,从类创建对象的过程叫做实例化。



在 C# 中,类的定义是通过 class 关键字来定义的,类名的首字母要大写,且对外公开的方法需要使用 public 修饰符。对象这个实例,是通过 new 关键字来创建的(实例化)。



class Cat
{
public void Shout()
{
Console.WriteLine("喵");
}
}
class Program
{
static void Main(string[] args)
{
Cat cat = new Cat(); //声明一个Cat的对象cat并将其实例化
cat.Shout();
}
}



2. 构造方法

构造方法,又叫构造函数,是为了对类进行初始化。构造函数与类同名,无返回值,也不需要 void,在 new 实例化时候调用。

所有的类都有构造方法,如果没有定义,则系统会默认生成空的构造方法,如上例中的 Cat()。若有定义,则默认的构造方法会失效。



在构造方法中,可以使用方法重载,以创建同名的多个方法的能力。在使用方法重载时,方法名必须相同,参数类型或个数必须不同。



class Cat
{
public Cat()
{}
public Cat(string name)
{}
}



3. 属性

属性包含 getset 两个方法,get 访问器返回与声明的属性相同的数据类型,表示的意思是调用时可以得到内部字段的值或引用;set 访问器没有显示设置参数,但它有一个隐式参数,用关键字 value 表示,它的作用是调用属性时可以给内部的字段或引用赋值。

在调用属性的代码来说,属性也可以看作是一个字段。下面通过一些代码段来看下。

private int shoutNum = 3;
public int ShoutNum
{
get
{
return shoutNum;
}
set
{
shoutNum = value;
}
}



ShouNum 属性是 public 的,其 get 方法表示外界调用时可以得到 shoutNum 的值,set 方法有一个隐式的参数,用关键字 value 表示,作用是外界调用属性时可以给内部的字段 shoutNum 赋值。

这里,为什么不直接把 shoutNum 字段的修饰符改为 public 以实现既读又写呢?因为我们期望对那些对外公开的数据做更多的控制,不能让调用者任意的读取或写入。就好比一个房间,其门和窗是 public 的,但房间内部的东西希望是 private 的。



class Program
{
static void Main(string[] args)
{
Cat cat = new Cat();
cat.ShoutNum = 5;
}
}



面向对象的三大特征分别是封装、继承和多态,下面来依次讲解。

4. 封装

每个对象都包含它能进行操作所需要的所有信息,而不必依赖其他对象来完成自己的操作,这个特性称为封装。这样,方法和属性都包装在类中,通过类的实例来实现。

封装的好处包括:

  • 良好的封装能减少耦合

  • 类内部的实现可以自由地修改

  • 类具有清晰的对外接口,如 ShoutNum 属性和 Shout 方法。



5. 继承

对象的继承代表了一种 is-a 的关系,如果两个对象A和B可以表述为B是A,那么表明B可以继承A,比如猫(B)是哺乳动物(A)就是一种继承和被继承的关系。继承者(子类、派生类)可以看作是被继承者(父类、基类)的特殊化,除了具备被继承者的一些特性外,还具备自己独有的个性。

  • 子类拥有父类的非 private 的属性和功能

  • 子类具有自己的属性和功能(可扩展)

  • 子类可以以自己的方式实现父类的功能(方法重写)



比如,对猫和狗,都可以抽象为动物,将其共有的名字、叫的次数这个功能放到动物这个类中,通过继承以避免重复的代码。

继承的缺点就是,当父类发生变化时,子类必须也不得不变。也会破坏了包装,将父类的细节暴露给子类,增大了两个类之间的耦合性。



当类之间是 is-a 的关系时,可以考虑使用继承。但当两个类之间时 has-a 关系时,比如人有两只手,则不能使用继承了。

6. 多态

多态表示不同的对象可以执行相同的动作,但是要通过它们自己的实现代码来执行。需要注意以下三点:

  • 子类以父类的身份出现

  • 子类在工作时以自己的方式来实现

  • 子类以父类的身份出现时,子类特有的属性和方法不可以使用



为了让子类的实例完全接替来自父类的类成员,父类必须将该成员声明为虚拟的(virtual 关键字),子类通过使用 override 关键字来重写父类的实现。



比如,猫和狗都会叫,但是叫声不同,此时可以让动物有一个 Shout 的虚方法,然后猫和狗去重写这个Shout 方法以实现各自的叫声,以实现多态。

class Animal
{
...
public virtual string Shout()
{
return "";
}
}
class Cat : Animal
{
...
publich override string Shout()
{
return "喵";
}
}
class Dog : Animal
{
...
publich override string Shout()
{
return "汪";
}
}



7. 重构

当我们在原来的基础上,好需要增加动物的种类来实现不同的叫声时,会发现在不同的动物的 Shout 方法中,除了叫声不同外,其他都是相同的,此时就需要对我们之前的代码进行改造了,来消除一些重复。

比如把 Shout 方法变为普通的方法放到 Animal 中,同时增加一个另一个方法 getShoutSound 来获取不同动物的叫声。这样,只需在不同的动物的实现中重写 getShoutSound 即可,将所有的重复都转移到父类的 Animal 中了。

class Animal
{
...
public string Shout()
{
string result = getShoutSound();
return result;
}
protected virtual string getShoudSound()
{
return "";
}
}



8. 抽象类

C# 允许把类和方法声明为 abstract`,及抽象类和抽象方法。抽象类通常代表一个抽象概念,它提供一个继承的出发点,当设计一个新的抽象类时,一定是用来继承的。所以,在一个以继承关系形成的等级结构里面,树叶节点应该是具体类,而树枝节点均应该是抽象类。

抽象类有以下几点需要注意:

  • 抽象类不能实例化

  • 抽象方法是必须要被子类重写的方法

  • 如果类中包含抽象方法,那么类就必须定义为抽象类



前面的 Animal 类其实就是一个抽象的概念,是不可能实例化的,所以可以将其改造为抽象类:

abstract class Animal
{
...
protected abstract string getShoudSound(); //抽象方法没有方法体
}



9. 接口

接口是把隐式公共方法和属性组合起来,以封装特定功能的一个集合。声明接口在语法上与声明抽象类相同,但是不允许提供接口中任何成员的执行方式。一旦类实现了接口,类就可以支持接口所指定的所有属性和成员。实现接口的类,就必须要实现接口中的所有方法和属性。一个类可以支持多个接口,多个类也可以支持相同的接口。

接口通过 Interface 来声明,接口名称前需要加I,接口中的方法或属性前不能有修饰符、方法没有方法体。



比如对前面的猫,如果希望某些猫有特异功能,可以变出一些东西。但是特异功能并不是普通的猫有的能力,所以不能作为猫的一个方法。此时就需要借助接口了。

通过定义一个「变东西」的接口,然后让某些猫,比如机器猫,来实现定义好的「变东西」的接口。

interface IChange
{
string ChangeThing(string thing);
}
class MachineCat : Cat, IChange //继承Cat,并实现IChange接口
{
...
public string ChangeThing(string thing) // 实现接口的方法
{
return "变";
}
}



抽象类与接口的不同点:

  • 类是对对象的抽象,抽象类是对类的抽象,接口是对行为的抽象

  • 如果行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象化

  • 抽象类是从子类中发现公共的东西,然后泛化处父类,然后子类继承父类;而接口根本就不知道子类的存在,方法如何实现还不确认,预先定义。



10. 集合

介绍集合前,先讲讲数组。数组在内存中是连续的,可以快速地从头到尾遍历元素并对元素进行修改;但数组在创建时必须要指定数组变量的大小,且在两个元素之间添加元素较为困难。

.NET Framework 针对这一问题提供了用于数据存储和检索的专用类,这些类统称为集合。这些类提供对堆栈、队列、列表和哈希表的支持,其中,ArrayList 是最为常用的一种。

ArrayList 是命名空间 System.Collection 下的一部分,它是使用大小可按需动态增加的数组实现 IList 接口。ArrayList 的默认初识容量为0,随着元素的添加,容量会根据需要重新分配自动增加,可通过整数索引访问集合中的元素。

using System.Collections;
class program
{
IList arrayAnimal; //声明集合变量
...
arrayAnimal = new ArrayList(); //实例化ArrayList对象,此时并没有指定其大小
...
arrayAnimal.Add(new Cat("cat name")); //调用集合的Add方法增加对象
...
arrayAnimal.RemoveAt(1); //调用集合的RemoveAt方法按索引移除对象
...
foreach (Animal item in arrayAnimal) //遍历ArrayList集合
{
...
}
}



ArrayList 是以 Object 类型来存储所有元素的,不管是字符串类型还是整型都能接受,但是在遍历运行时,会导致类型不匹配而出错,这说明 ArrayList 不是类型安全的。

由于 ArrayList 都是需要将值类型「装箱」为 Obejct 对象,在使用集合的元素时,还需要执行「拆箱」操作,这会带来很大的性能损耗(相对于简单的赋值,装箱和拆箱过程需要大量的计算,对值类型进行装箱时,必须分配并构造一个全新的对象)。

  • 装箱,即把值类型打包到 Obejct 引用类型的一个实例中,比如整型变了 i 被装箱并赋值给对象 o

int i = 123;
object o = (object) i; //装箱
  • 拆箱时指从对象中提取值类型,比如对象 o 拆箱并将其赋值给整型变量 i

o = 123;
i = (int) o; //unboxing



11. 泛型

泛型是具有占位符(类型参数)的类、结构、接口和方法,这些占位符是类、结构、接口和方法所存储或使用的一个或多个类型的占位符。泛型集合类可以将类型参数用作它所存储的对象的类型的占位符。

泛型集合需要使用System.Collections.Generic的命名空间,List 类是 ArrayList 类的泛型等效类,该类使用大小可按需动态增加的数组实现 IList 泛型接口。用法是在 IListList 后面加 <T>,其中 T 是需要指定的集合的数据或对象类型。



using System.Collections.Generic;
class program
{
IList<Animal> arrayAnimal; //声明泛型集合变量,此集合变量只能接受Animal类型
arrayAnimal = new List<Animal>(); //实例化List对象,此时也需要指定类型为Animal
...
}



泛型集合相对非泛型集合有两个优点:

  1. 类型安全

  2. 不需要对元素进行装箱操作,如果集合元素都是值类型,性能会更优



12. 委托与事件

委托是对函数的封装,可以当作给方法的特征指定一个名称。委托是一种引用方法的类型,一旦为委托分配了方法,委托将与该方法具有完全相同的行为。委托用关键字 delegate 来声明。



事件是委托的一种特殊形式,当发生有意义的事情时,事件对象处理通知过程。事件是说在发生其他类或对象关注的事情时,类或对象可通过事件通知它们。事件用关键字 event 来声明。



class TomCat
{
...
public delegate void TomCatShouEventHandler(); //声明委托TomCatShouEventHandler
public event TomCatShouEventHandler TomCatShout; //声明事件TomCatShout,它的事件类型时委托TomCatShouEventHandler
public void Shout()
{
if (TomCatShout != null)
{
TomCatShout(); //当执行Shout()方法时,如果TomCatShout中有对象登记事件,则执行TomCatShout()
}
}
}
class program
{
static void Main(string[] args)
{
...
TomCat tomcat = new TomCat("Tom");
Mouse mouse = new Mouse("Jack");
tomcat.TomCatShout += new TomCat.TomCatShouEventHandler(mouse.Run); //将Mouse的Run方法通过实例化委托TomCatShouEventHandler登记到TomCat的事件TomCatShout中
}
}



完整的代码实现可以参考:https://github.com/puran1218/DesignPatternWithCSharp



发布于: 2020 年 06 月 03 日阅读数: 47
用户头像

Puran

关注

GIS从业者,正在往开发的路上小跑。 2018.03.29 加入

从业4年的GIS开发小白,work@esri。

评论

发布
暂无评论
大话设计模式 | 0 面向对象基础