面向对象的三大特点

C#是一种面向对象的编程语言,广泛用于开发各类应用程序。同时,C#作为一种多范式语言,也支持面向过程编程。面向对象的特点我们耳熟能详,但新手可能会说不清楚,为什么要这样设计。今天我们就对面向对象的三大核心特点:封装,继承,多态来进行超详细讲解。

面向对象编程是一种以对象为中心的编程范式,它将数据和操作数据的方法组合成对象。在面向对象编程中,程序被组织成一组相互作用的对象,这些对象通过彼此发送消息来传递数据。
面向过程编程是一种以过程为中心的编程范式,它主要关注解决问题时所需要执行的步骤。在面向过程编程中,程序被组织成一系列的函数或方法,这些函数或方法按照特定的顺序被调用来完成任务。

封装(Encapsulation)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BankAccountWithoutEncapsulation
{
public decimal Balance; // 公开的余额字段

public void Deposit(decimal amount)
{
Balance += amount;
}

public void Withdraw(decimal amount)
{
Balance -= amount;
}
}

举一个简单的小例子,假设一种场景,有一个银行账户类,包括余额和存取款方法等。如果直接将余额设为public,其他类能直接从外部访问账户类的余额信息,这种设计存在一些问题:

  • 数据不受限制:外部代码可以随意修改Balance字段,而没有任何限制,这可能导致不合法的操作,如负数余额。
  • 无法实施验证:无法在存款或取款方法中实施验证,以确保金额不为负数或其他不合法值。
  • 缺乏安全性:敏感的账户余额可以在外部被直接访问,而不受任何控制。

这种设计明显是不合理的,那么应该怎样修改呢?我们可以试着将Balance声明为private,添加一个public方法来访问Balance,外部代码无法直接访问它,而必须使用公共方法来与余额进行交互,这样就解决了上述问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class BankAccountWithEncapsulation
{
private decimal balance; // 私有的余额字段

public decimal GetBalance()
{
return balance; // 通过公共方法返回余额
}

public void Deposit(decimal amount)
{
if (amount > 0)
{
balance += amount;
}
}

public void Withdraw(decimal amount)
{
if (amount > 0 && balance >= amount)
{
balance -= amount;
}
}
}

这就是一个简单的运用了封装的例子,体现了封装的数据隐藏和安全性。
那么具体什么是封装呢?封装其实就是一种将数据(属性)和操作数据的方法(方法)组合成一个单元,并且把这些内部细节隐藏起来,只暴露必要的公共接口供其他对象访问的机制。封装的主要目标是隔离对象的内部实现,以提高代码的可维护性、安全性和可重用性。封装的关键概念包括:

  • 数据隐藏:封装将对象的数据(通常是字段或属性)设为私有或受限访问,使其不能直接被外部对象访问,从而防止外部对象直接修改或破坏对象的内部状态。

  • 公共接口:封装通过提供公共方法或属性,允许其他对象与封装对象进行交互。这些公共接口定义了外部对象可以如何与对象互动,并提供了访问对象数据和执行对象操作的途径。

  • 数据验证和保护:通过封装,可以在公共接口中添加数据验证逻辑,以确保数据的合法性,防止不合法的访问和修改。

  • 隐藏实现细节:封装允许对象内部的实现细节保持私有,外部对象不需要了解对象的内部工作原理,只需要知道如何与对象互动。

  • 安全性:封装提高了数据的安全性,因为外部对象无法直接访问和修改对象的私有数据。

通过封装,面向对象编程能够将复杂性管理在对象内部,提供了清晰、简化的公共接口,使代码更易于理解、维护和扩展,有助于构建模块化、可靠和可维护的应用程序。

继承(Inheritance)

继续假设这样一个场景,我们想做一个动物管理系统,涉及到很多个动物种类,假如直接为每一种动物都写一个类,那么每一个类都要包含名称,种类,科属等字段,并且每一个类都会有一些相同的方法,例如吃饭,休息等。这样写下来代码就会有非常大的重复和冗余。那么该怎样改进呢?
我们可以将这些动物的共同属性和方法提取出来,放在一个类中,提供一个基础的框架,以定义动物的基本属性和行为。在这个框架的基础上,每个动物类都可以添加自己独有的属性和方法,例如”Cat”类可以添加属性”毛色”和方法”捉老鼠”,”Bird”类可以添加属性”翅膀长度”和方法”飞翔”。
这就是继承的思想。这个被提取出来的公共类就叫做基类,在这个框架的基础上进行拓展的类就叫做派生类。继承允许我们构建层次化结构,其中基类定义了共享特征,而派生类继承这些特征并添加特定于自身的内容,从而实现了代码重用和抽象化,这有助于更清晰地表示现实世界中的关系和层次结构。继承的好处非常多,例如:

  • 代码重用:继承允许子类(派生类)继承父类(基类)的属性和方法,从而避免了重复编写相同的代码。这促进了代码的重用,减少了代码冗余。
  • 抽象化和通用性:基类可以定义通用的属性和方法,捕获多个子类之间共享的特性。这种抽象化有助于更好地建模现实世界的关系,并提供通用性。
  • 层次化组织:继承允许创建层次结构,通过将相关的类组织在一起,更清晰地表示它们之间的关系。这有助于代码的结构和理解。
  • 可扩展性:新的子类可以轻松地添加到继承层次结构中,而无需修改现有的代码。这提高了应用程序的可扩展性,使其能够适应新的需求。
  • 维护性:继承有助于代码的维护,因为通用代码只需在一个地方维护,而不需要在每个子类中进行修改。这降低了维护的成本。
    -多态性:继承是多态性的基础。多态性允许不同的子类对象对相同的方法进行不同的实现,从而提高了代码的灵活性和可扩展性。
  • 封装实现细节:基类可以封装实现细节,隐藏其内部工作原理,允许子类专注于其独特的功能。
  • 重用已有类:继承允许在现有类的基础上构建新的类,以便重用已有类的功能,并添加新的功能。

总之,继承是面向对象编程中的重要概念,它提供了许多好处,包括代码重用、抽象化、层次化组织、可扩展性、维护性和灵活性,这使得继承成为构建复杂应用程序的强大工具。

继承的传递性:传递机制 a->b,b->c,c具有a的特性。
继承的单根性:在C#中一个类只能继承一个父类,不能有多个父类。

多态(Polymorphism)

多态是个比较抽象的概念,我们可以继续通过一个小例子来理解多态的概念。回合制游戏中,往往有这样一种场景:场上有一组不同类型的怪物,例如巨魔,巫师,恶龙,怪物依次对玩家进行攻击。假设没有继承和多态,需要为每种类型的怪物都设置一个列表,巨魔列表,恶龙列表,狼人列表,依次遍历所有列表,调用对应怪物类的Attack方法来对玩家进行攻击。
再有了继承的概念之后,我们可以将所有怪物的共同属性和方法提取出来,变成一个Monster基类,不同怪物类都可以继承Monster类,这样就只需要一个List<Monster>就能管理所有怪物,不需要多个列表。
但这样依然会有一个问题,在遍历这个列表的过程中,我们需要依次判断列表元素对应的怪物种类是什么,然后再执行对应怪物子类的攻击方法,这样做依然很不方便,需要大量的switch和if语句,这时候,就引入了多态的概念。
多态是什么呢?多态顾名思义,就是对象的多种状态。它表示在不同的对象上可以执行相同的操作,但这些操作可以根据对象的实际类型而具有不同的行为。多态性的核心思想是:在同一个方法或属性被不同类的对象调用时,可以根据对象的实际类型来执行不同的操作。
多态性有两种主要形式:

  • 编译时多态性(静态多态性):这是方法的重载和方法的重写,它们在编译时确定方法调用的方式。方法的重载是指在同一个类中有多个方法,但它们具有相同的名称,但不同的参数列表。方法的重写是指派生类可以覆盖基类中的方法,以提供自己的实现。
  • 运行时多态性(动态多态性):这是多态性的经典形式,它允许在运行时根据对象的实际类型来确定方法调用的方式。这通常涉及到继承和方法覆盖。在运行时,基于对象的实际类型,可以调用适当的方法实现。

落到这个例子上,多态是如何实现的呢?首先我们定义一个Moster基类,包含一个virtual修饰的Attack方法,其他怪物子类继承这个基类,在子类中重写(覆盖)这个Attack方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Monster
{
public virtual void Attack()
{
Console.WriteLine($"怪物攻击了!");
}
}
class Troll : Monster
{
public override void Attack()
{
Console.WriteLine($"巨魔挥舞着大棒攻击!");
}
}
class Wizard : Monster
{
public override void Attack()
{
Console.WriteLine($"巫师发射了火球攻击!");
}
}
class Dragon : Monster
{
public override void Attack()
{
Console.WriteLine($"恶龙喷射火焰攻击!");
}
}

假设场上的怪物都用List<Monster> monsters来存储,我们遍历这个列表,调用Attack方法时,根据每个怪物的实际类型,会自动执行对应的攻击方式。

1
2
3
4
foreach (Monster monster in monsters)
{
monster.Attack();
}

多态性的优势在于可以将不同类型的对象当作通用的基类对象处理,从而提高代码的可重用性、可维护性和可扩展性。多态性使代码更具灵活性,能够处理不同情况下的相同方法调用,从而更好地模拟现实世界的复杂性。
多态的优点主要有:

  • 提高代码的灵活性和通用性:多态性允许将不同类型的对象视为其共同的基类对象,从而编写通用代码,可以处理多种不同类型的对象。这提高了代码的通用性和灵活性。
  • 简化代码:使用多态性可以减少重复的代码,因为可以将通用的操作应用于多个对象,而不需要为每个对象编写特定的操作。
  • 支持动态绑定:多态性允许在运行时动态地选择要调用的方法,而不需要在编译时知道对象的具体类型。这提供了更大的灵活性,适应不同的运行时条件。

总结

总之,封装、继承和多态性有助于构建更安全、可维护、可扩展和通用的软件系统。它们提供了一种有效的方式来管理复杂性,降低代码的复杂性,从而更好地满足现实世界问题的需求。