CS3L18——多态vob

本章代码关键字

1
2
3
virtual        //使用 virtual 修饰的方法,在子类里可以通过 override 去重写
override //重写父类 virtual 修饰的方法的关键字
base //在重写方法中调用父类方法的关键字

多态

多态就是按字面的意思就是“多种状态”,让继承同一父类的子类们,在执行相同方法是有不同的表现
主要目的:同一父类的对象 执行相同行为(方法)有不同的表现

多态要解决的问题:让同一个对象有唯一行为的特征

以前没有学习多态相关关键字时,要在子类重写一个父类方法需要使用 new​ 关键字
但是 new​ 关键字在 里氏替换情况下,会出现问题,例如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Father
{
public void SpeakName()
{
Console.WriteLine("Father的方法");
}
}
class Son : Father
{
public new void SpeakName()
{
Console.WriteLine("Son的方法");
}
}

internal class Program
{
static void Main(string[] args)
{
Father f = new Son();
f.SpeakName(); //这里就会出现问题,虽然是Son对象但却执行的是父类的方法
(f as Son).SpeakName(); //用as可以解决这个执行父类方法的问题,但是这样就破坏了对象的唯一性,因为一个对象可以使用两种类的方法了
}
}

输出:

1
2
Father的方法
Son的方法

可见,当使用父类装载子类时,调用子类通过 new​ 覆盖的方法还是会调用父类的方法
虽然用 as​ 可以解决这个执行父类方法的问题,但是这样就破坏了对象的唯一性,因为一个对象可以使用两种类的方法了
这也让里氏替换失去意义,因为我们还是要知道子类具体是什么类型才能执行子类方法

多态的实现

我们目前已经学过的多态:编译时多态 —— 函数重载,开始就写好的

我们将学习的:运行时多态(vob、抽象函数、接口),这次是vob

  • v: virtual​(虚函数)
  • o: override​(重写)
  • b: base​(父类)

父类里使用 virtual​ 修饰的方法,在子类里可以通过 override​ 去重写,子类重写的方法可以通过 base​ 来调用父类的方法

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class GameObject
{
public string name;
public GameObject(string name)
{
this.name = name;
}
//虚函数可以被子类重写
public virtual void Atk()
{
Console.WriteLine("游戏对象进行攻击");
}
}

class Player : GameObject
{
public Player(string name) : base(name) { }

//重写虚函数
public override void Atk()
{
//base的作用,代表父类 可以通过base来保留父类的行为,就是调用父类的这个方法,是否使用视需求而定
base.Atk();
WriteLine("玩家对象进行攻击");
}
}
class Monster : GameObject
{
public Monster(string name) : base(name) { }

public override void Atk()
{
WriteLine("怪物对象进行攻击");
}
}

internal class Program
{
static void Main(string[] args)
{
//vob就可以彻底的覆盖父类的方法,保证了同一个对象有唯一行为的特征
GameObject p = new Player("MrTang");
p.Atk();

GameObject m = new Monster("monster");
m.Atk();
}
}

输出:

1
2
3
游戏对象进行攻击
玩家对象进行攻击
怪物对象进行攻击

可以看见,vob就可以彻底的覆盖父类的方法,保证了同一个对象有唯一行为的特征

里氏替换原则的意义也在这里体现,在函数传递时,可以将使用基类的参数,外部传入子类对象,
在函数内部不需要关心这个基类继承了什么子类,我们只需要调用父类声明好的方法,就可以调用子类的方法

例如一个拥有 Atk​ 行为的 GameObject​,外部可以传入各种各样的基于 GameObject​ 派生的对象,
尽管它们有不同的实现,成员也可能不同,但它们派生自 GameObject​,就一定有 Atk​ 行为,
因此函数内部也只需要调用 Atk​ 行为即可,而无需知道它是什么类型

下面的 ObjectAtk​ 函数就可以接受派生自 GameObject​ 各种参数,而不需要在意参数具体是什么类型

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
29
30
31
32
33
34
35
36
37
38
39
class GameObject
{
public virtual void Atk()
{
Console.WriteLine("游戏对象进行攻击");
}
}

class Player : GameObject
{
public override void Atk()
{
WriteLine("玩家对象进行攻击");
}
}
class Monster : GameObject
{
public override void Atk()
{
WriteLine("怪物对象进行攻击");
}
}

internal class Program
{
static void Main(string[] args)
{
Player p = new Player("MrTang");
Monster m = new Monster("monster");

ObjectAtk(p);
ObjectAtk(m);
}

static void ObjectAtk(GameObject obj)
{
obj.Atk();
}
}

输出:

1
2
玩家对象进行攻击
怪物对象进行攻击