类的多态性,方法重写,方法重载,方法隐藏介绍
前提
阅读本节的前提是对面向对象编程,类,实例等概念有所掌握,你可以查看面向对象编程,类,实例介绍一节来了解他们。
类的多态性
类的多态性是指派生类可以改变调整继承成员的实现,以符合自身的需求,这主要针对实例方法,或本质上是方法的实例成员。多态性使得继承与被继承的对象,在同一功能上的表现可以不同。
变量类型与多态性
在支持面向对象和声明变量类型的语言中,变量的类型与值的数据类型可能不同,通过变量调用的方法,将指向对象对该方法的真实实现,当对象已经对方法做出调整或改变时。
在下面的 C# 代码中,虽然变量kitty
和animal
的类型都是Animal
,且Animal
类的Bark
方法固定,但实际调用Bark
方法却显示了不同的信息,因为变量kitty
对应的真实对象的类型是Kitty
,对于Bark
方法,Kitty
类拥有自己的实现。
// 创建 Animal 和 Kitty 的实例
Animal animal = new Animal();
Animal kitty = new Kitty();
// 分别调用 Bark 方法,将显示不同信息
Console.WriteLine("animal.Bark");
animal.Bark();
Console.WriteLine("kitty.Bark");
kitty.Bark();
animal.Bark
试着发出声音?
kitty.Bark
试着发出声音?
喵喵喵!
方法重写
方法重写(Method Overriding)是实现多态性的最简单方式,一般的做法是在类中再次定义需要重写的实例方法,此过程可能需要特定的关键字,比如,C# 中的override
。
什么是虚方法?
虚方法是指允许被派生类重写的方法,通常对应关键字virtual
。在一些语言中,虚方法是不存在的,因为默认所有方法均可被重写。
由于虚拟方法允许被重写,所以他不应该具有任何阻止该目标实现的特征,比如,私有的。
在派生类中调用基类的方法
在方法重写的过程中,你可能需要使用基类的方法,如果重写的目的在于补充功能。大部分语言会提供相关的关键字来完成此目标,比如,C# 中的base
关键字。
下面展示了之前示例中使用的类Animal
和Kitty
,可以看到Animal
类拥有一个虚方法Bark
,Kitty
类对该方法进行了重写,并调用了基类的Bark
方法。
// 基类 Animal
class Animal
{
// 虚方法 Bark,尝试发出声音
public virtual void Bark()
{
Console.WriteLine("试着发出声音?");
}
}
// 派生类 Kitty
class Kitty : Animal
{
// Kitty 重写了继承自 Animal 的方法 Bark
public override void Bark()
{
// 调用基类的 Bark 方法
base.Bark();
Console.WriteLine("喵喵喵!");
}
}
什么是抽象方法和抽象类?
抽象方法是类中已经声明,但尚未具体实现的方法,他通常需要特定的关键字来标注,比如,C# 中的abstract
,并需要派生类来完成实现,一个方法被抽象的原因是其必要性和缺少通用性的逻辑。
由于需要通过派生类完成实现,抽象方法不能具有任何阻止这一目标实现的特征,比如,私有的。
拥有抽象方法的类被称为抽象类,因为没有完全的具体实现,抽象类无法被实例化。需要指出,抽象类包括了,已继承抽象方法但仍未对其实现的派生类。
抽象方法和虚方法之间的区别
抽象方法要求被具体实现,否则相关类无法实例化,虚方法则表示允许被重写,能否实例化与是否被重写无关。
下面,为Animal
类增加一个抽象方法Run
,Kitty
类需要实现该方法后才能被实例化。
// 基类 Animal
abstract class Animal
{
// …
// 抽象方法 Run,奔跑起来
public abstract void Run();
}
// 派生类 Kitty
class Kitty : Animal
{
// …
// Kitty 必须实现抽象方法 Run,才能被实例化
public override void Run()
{
Console.WriteLine("你看,我跑起来了!");
}
}
// 创建 Kitty 的实例
Kitty kitty = new Kitty();
// 调用 Run 方法
kitty.Run();
你看,我跑起来了!
方法重载
方法重载(Method Overloading)与多态性的关系不大,他允许一个类定义多个名称相同但签名不同的方法,而不是将基类中的方法再次定义。方法重载在一些语言中是不被支持或不可行的,他们不允许一个类拥有名称相同的方法,方法签名仅通过方法名称产生,或者根本没有签名。
重写基类的重载方法需要匹配方法签名
这里需要指出,在重写基类的重载方法时,请注意方法签名是否匹配。
选择方法重载还是可选参数?
当方法内部的逻辑差异不大时,使用可选参数应该是更好,如果语言支持这一功能的话,你将不会看到一排排的方法定义,仅仅是为了多出或缺少的参数。
当不同参数组合带来的逻辑差异较大时,可选择使用方法重载,以使代码更容易维护。
函数
要了解方法签名,你可以查看函数签名一段,好吧,名字不代表一切。
下面,通过重载为Kitty
类添加两个同名的Eat
方法,他们拥有的参数不同。
// 派生类 Kitty
class Kitty : Animal
{
// …
// 使用重载定义两个同名的 Eat 方法
public void Eat(string something)
{
Console.WriteLine($"好耶,今天的午餐是:{something}");
}
public void Eat()
{
Console.WriteLine("今天吃空气?!");
}
}
// 创建 Kitty 的实例
Kitty kitty = new Kitty();
// 调用重载的 Eat 方法
kitty.Eat("小鱼干");
kitty.Eat();
好耶,今天的午餐是:小鱼干
今天吃空气?!
方法隐藏
方法隐藏(Method Hiding/Shadowing)和方法重写是比较容易混淆的,他们均针对基类的方法进行重新定义,但不同的是,通过隐藏定义的方法断绝了自身与基类方法的继承关系,通过重写定义的方法则会保持。方法隐藏和方法重写的选择,将影响多态性的表现效果。
在通过某种类型的变量调用方法时,需要确定实际对象是否存在对该方法的重写,而通过隐藏定义的方法将打断这一过程,前提是这些方法真实的阻断了当前和之后的继承关系,他们不能是私有的,或具有任何导致上述目标无法实现的特征。
在下面的 C# 代码中,变量a
,b
,c
,d
的类型均为A
,但变量b
对应的真实对象的类型为B
,类B
重写了类A
的Show
方法,因此b.Show()
指向类B
对Show
方法的实现。变量c
对应的真实对象的类型为C
,类C
隐藏了类B
的Show
方法,c.Show()
将不会指向类C
对Show
方法的实现,因为类C
的Show
方法与类B
,A
的Show
方法不存在继承关系。变量d
对应的真实对象的类型为D
,类D
重写了类C
的Show
方法,d.Show()
将不会指向类D
,C
对Show
方法的实现,因为类D
,C
的Show
方法与类B
,A
的Show
方法不存在继承关系。
// 所有变量的类型均为 A,但真实的对象类型分别为 A,B,C,D
A a = new A();
A b = new B();
A c = new C();
A d = new D();
// 分别调用他们的 Show 方法
a.Show();
b.Show();
c.Show();
d.Show();
调用了 A 的 Show 方法
调用了 B 的 Show 方法
调用了 B 的 Show 方法
调用了 B 的 Show 方法
// 类 A
class A
{
// 虚方法 Show
public virtual void Show()
{
Console.WriteLine("调用了 A 的 Show 方法");
}
}
// 类 B
class B : A
{
// 重写类 A 的 Show 方法
public override void Show()
{
Console.WriteLine("调用了 B 的 Show 方法");
}
}
// 类 C
class C : B
{
// 隐藏类 B 的 Show 方法,阻断了继承关系
public new virtual void Show()
{
Console.WriteLine("调用了 C 的 Show 方法");
}
}
// 类 D
class D : C
{
// 重写类 C 的 Show 方法
public override void Show()
{
Console.WriteLine("调用了 D 的 Show 方法");
}
}