# 面向对象的概念

一般提及面向对象编程就总是会提到面向过程,前面的事例都是面向过程的,它们更倾向于解决某个问题,但是这常会导致单一应用程序,即所有的功能常常包含在一个代码模块中,而使用面向对象思想的话,常常会根据功能将这些代码分到不同的模块里,而且每个模块尽量独立于其他模块,这样大大增加了重用代码的机会。

传统的应用程序中,执行流程常是线性的,写代码就是重点着眼于解决问题的每步过程上,在OOP中以结构,数据的含义以及数据和数据之间的交互操作作为基础,重点着眼于项目的设计阶段上,以问题中的每个对象以及每个对象有什么样的行为作为主要研究目标,极大地增加了可扩展性。以顾客进入饭馆吃饭为例来说明面向对象和面向过程的区别:

面向过程时是这样分析的,基本上就是拿到问题就写代码实现:

  1. 顾客进入饭馆,坐下点餐。
  2. 服务员将点餐清单传到后厨。
  3. 后厨根据点餐清单做饭。
  4. 服务员将做好的饭端上饭桌。
  5. 顾客高兴地吃饭。
  6. 顾客吃完结账。

面向对象时是这样分析的:

  1. 分析这个过程中涉及的对象,顾客,服务员,后厨。
  2. 每个人应该有的行为:
    1. 顾客:
      1. 点餐
      2. 吃饭
      3. 结账
    2. 服务员:
      1. 将点餐清单传到后厨。
      2. 把做好的饭端上饭桌。
    3. 后厨:
      1. 做饭
  3. 根据上面的分析,写代码实现。

# 对象的含义

从上面可以看出,在OOP中"对象"是主要研究对象,是程序的基本构件,它基本由两部分构成:有什么样的数据和有什么样的行为。C#中对象是由类创建的,类是对象的类型,就像int是整数的类型一样,"类的实例"和"对象"是等价的,可以把类看成是对象的蓝图。为了以通用的语法来研究类和对象,将使用统一建模语言(Unified Modeling Language,UML)。在UML中类和对象的表示如下所示:

classObject

类和对象都用矩形图示,但是对象下面有下划线,且冒号后面跟类名,通常省略对象名。

# 对象的数据

在C#中,对象的数据在类中用属性和字段来存储,同一个类的不同的对象其拥有的属性和字段可能会有不同的值。

属性和字段有什么区别吗?

  1. 属性不提供对数据的直接访问,它能在对数据访问的同时做一些限制,比如限制写入大小范围,是否只读。
  2. 在改变一个字段值时,希望改变对象的一些其他状态。

在类中,可以对字段和属性做访问权限的限制,也就是可访问性的限制,有公有,私有和保护。公有是可从类外部访问,私有是只能从类内部访问,而保护则是继承该类的子类可访问。在UML中,数据成员按如下表示,其中前面的+号表示公共成员,-号表示私有成员,#表示保护成员,~表示包可见性,斜体表示虚拟成员,然后是成员名:成员类型。

field

# 对象的行为

在C#中,对象的行为在类中用方法实现,其实就是前面介绍的函数,方法也有访问权限的限制,跟前面的字段和属性是一样的,在UML中它是按如下所示,其中前面的符号表示访问权限,然后是 方法名(参数标识符 参数名:参数类型):返回值类型。其中参数标识符有return,in,out或inout,其中out和inout相当于C#中的out和ref,in相当于默认参数,return相当于传回调用方法的值,它可以放在括号里面也可以放在外面。

operation

# 对象的生命周期

每个对象都有两个重要阶段:

  • 构造阶段:第一次实例化对象时的初始化阶段,由构造函数完成。
  • 析构阶段:在对象被销毁时,常需要执行一些清理工作,由析构函数完成。
# 构造阶段

所有的类至少都包含一个构造函数,这些构造函数中,可能有一个默认构造函数,即没有参数与类同名,还可能包含几个带有参数的构造函数,即非默认构造函数。在C#中使用new关键字来调用构造函数,根据传入参数的类型来决定调用哪个构造函数。构造函数也有访问权限限制,私有的构造函数不能使用new关键字,在外部不可能实例化它们,称为不可创建类,它们有特殊作用。

# 析构函数

C#使用析构函数在对象要被销毁时来执行一些清理工作,一般情况下不需要提供析构函数代码,使用默认析构函数即可,但是如果在销毁对象时完成一些重要操作,就应提供具体的析构函数。

# 静态成员

对象由类实例化,拥有类定义的属性,字段和方法,此外类还有静态属性,静态字段和静态方法,静态成员可以在类的实例之间共享,可将它们看成是类的全局对象,静态成员可以独立于对象实例访问,使用类名.静态成员即可访问它们,在UML语法中,类的静态成员带有下划线。

# 静态构造函数

静态构造函数提供了初始化静态成员的地方,一个类只能有一个静态构造函数,该构造函数不能有访问修饰符,不能带任何参数,它不能直接调用,只能在下面情况下执行:

  • 创建包含静态构造函数的类实例时。
  • 访问包含静态构造函数的类的静态成员时。

这两种情况下会首先调用静态构造函数,但是只调用一次。

# 静态类

我们如果希望一个类只包含静态成员而不能用于实例化对象时,可以使用静态类,而不是将类的构造函数设置为私有,静态类只能包含静态成员,不能包含实例构造函数。

# OOP技术

# 接口

接口是把公共实例(非静态)方法和属性组合起来,以封装特定功能的一个集合。它是一个外部的规范,定义一个接口,实现这个接口的类需要实现该接口的方法。注意,接口不能单独存在,不能实例化一个类那样实例化接口,接口不能包含实现其成员的任何代码,只能定义成员本身,实现过程必须在实现接口的类中完成。在UML中,接口的定义及表示如下:

interface

一个类可实现多个接口,多个类也可实现相同的接口。发布接口后,一般不会修改它,它可表示"每个实现该接口的类都有这些方法和属性"。

# 继承

继承是OOP最重要特性之一,任何类都可以从另一个类继承,派生类拥有父类的所有成员,但是不能访问私有成员。C#中一个类仅能继承一个基类。在UML中用箭头表示继承:

inheritance

基类可以是虚类,成员可以由派生类重写,虚拟成员不能是私有成员,这样会自相矛盾,不能要求派生类重写成员却不能访问。

基类还可以定义为抽象类,抽象类不能直接实例化。要使用抽象类,必须继承这个类,抽象类有抽象成员,这些抽象成员可以在抽象类中实现,也可以由继承的派生类来实现,继承了抽象类的派生类如果不是抽象类,那么就要实现其所有抽象方法。在UML中抽象类的名称以斜体显示,有时它们的方框以虚线显示。

类可以是seal的,这样的类不能作为基类,没有派生类。

在C#中,所有对象都有一个共同的基类object,在 .Net Framework中,它是System.Object类的别名。

# 多态性

继承是OOP最重要的特性之一,继承某类的派生类能调用基类的非私有方法和属性。多态可以使用基类的引用调用派生类的方法,注意并不是只有共享同一个父类的类才能利用多态性,只要子类和孙子类在继承层次结构中有一个相同的类,就可以利用同样的方式实现多态性。

尽管不能像对象那样实例化接口,但可以建立接口类型的变量,然后就可以在实现该接口的对象上,使用这个变量来访问该接口提供的方法和属性,这是接口的多态。

# 对象之间的关系

  • 泛化(Generalization),也叫继承,可以让派生类完整地获得基类的特性。比如老虎狮子继承动物的特性。在UML中用一条带空心箭头的直线表示。
  • 关联(Association),它能在一段时间内将多个对象连接在一起,它是一种静态关系,与运行状态无关,由"常识","规律"和"法律"等因素决定,它是一种强关联关系。比如乘车人和车票,公民和身份证。在UML中用实线箭头表示。
  • 依赖(Dependency),它表示一个对象在运行期间会使用另一个对象,与关联关系不同,依赖关系是一种临时关系,通常是在运行期间产生,随着运行场景的不同,依赖关系会发生变化。它是一种"弱"的关系,不是天然存在的,依赖分为单向依赖和双向依赖,双向依赖是一种很不好的结构,应当尽量保持单向依赖。比如人和刀,在削苹果时,动物和氧气,在生存时是需要的。为了更好地理解关联和依赖,举个例子,比如AB两个对象,A对象保存了B对象的实例,但A对B没有操作,A仅仅是知道B,他们是关联关系,修改B后A不会有啥变化,如果A对象在某个场景中使用了B对象的属性和方法,对B的修改会导致对A的修改,这时A依赖于B。在UML中用虚线箭头表示。
  • 聚合(Aggregation),一个对象能包含另一个对象,但被包含对象不是该对象的一部分。聚合表示一种弱的"拥有"关系,即使整体不存在了,部分依然存在,比如部门和人员,大雁和雁群。在UML中用空心菱形的直线表示。
  • 组合(Composition),也称合成,体现了严格的部分和整体的关系,部分和整体的声明周期一样,是一种强的"拥有"关系,比如大雁和它的翅膀。在UML中用实心菱形的直线表示。

objectRelation

# 其他技术

类可以重新定义该类与运算符之间的关系,比如说大于号等,是比较该类的某几个的字段。此外对象可以激活和使用事件,比如键盘某键监听事件函数,在该键被按下时触发,使用事件可创建事件驱动的应用程序,此类应用广泛。