在面向对象编程中,虚函数、抽象类和接口都是用于实现多态性的关键概念,它们各自有着独特的用途和语义。
以下的代码举例用RPG游戏中的怪物与玩家举例。
虚函数
虚函数(关键字virtual)是在基类(父类)中声明的函数,可以在派生类(子类)中进行重写。这提供了运行时多态性,意味着调用哪个方法取决于对象的实际类型,而不仅仅是变量的类型。虚函数通常在基类中提供一个默认实现,但派生类可以使用Override关键字来提供一个不同的实现。
定义一个怪物(敌人)基类,怪物可以攻击玩家,再定义一个史莱姆,史莱姆作为怪物自然可以攻击玩家。我们希望输出的是“某某怪物攻击了玩家“而不是“怪物攻击了玩家”,因此对Attack方法进行重写。
class Enemy
{
int HP;
int ATK;
float moveSpeed;
public virtual void Attack()
{
Console.WriteLine("怪物攻击了玩家!");
}
}
class Slime:Enemy
{
public override void Attack()
{
base.Attack();
Console.WriteLine("史莱姆攻击了玩家!");
}
}
class Program
{
static void Main()
{
Slime slime = new Slime();
slime.Attack();
}
}
最后输出的是两段话,原因是base.Attack();它的作用是引用了Enemy类中的Attack方法的实例,如果我们不需要可以将其注释或删除。
抽象类
抽象类(abstract )是指未被成员函数实现的类,不能被实例化只能被继承,因此抽象类专为基类而生。
如上例,我们并不需要输出“怪物攻击了玩家”,换句话说我们并不需要Enemy类中的Attack方法,因为每个怪物的Attack是不同的,此时可以将其改成抽象类。
abstract class Enemy //改成抽象类时 对应方法也要加上关键字
{
int HP;
int ATK;
float moveSpeed;
public abstract void Attack();
}
class Slime : Enemy
{
public override void Attack()
{
//base.Attack(); //会报错 原因是无法调用抽象基成员
Console.WriteLine("史莱姆攻击了玩家!");
}
}
这样就输出“史莱姆攻击了玩家!”这一句话了。
接口
接口(interface)作为另一种实现多态性的手段,定义了一组方法、属性、事件和索引器的签名集合,但不包含任何实现细节。任何实现接口的类都必须提供接口中所有成员的具体实现,这使得接口成为了一种严格的契约,确保了实现该接口的类具备一致的公共行为,而不必关心其内部实现。接口内方法默认public。
我们拓写怪物的行为,除了攻击还可以受伤和死亡,将其封装成接口,接口通常用I开头,方便区分和管理。
interface IEnemy
{
void Attack();
void Damage();
void Dead();
}
abstract class Enemy : IEnemy
{
int HP;
int ATK;
float moveSpeed;
public abstract void Attack();//接口需要对所有方法进行定义 因此此时可以复用之前的Attack方法使用抽象类
public void Damage()
{
Console.WriteLine("敌人受伤!");
}
public void Dead()
{
Console.WriteLine("敌人死亡!");
}
}
class Slime : Enemy
{
public override void Attack()
{
//base.Attack();
Console.WriteLine("史莱姆攻击了玩家!");
}
}
class Program
{
static void Main()
{
Slime slime = new Slime();
slime.Attack();
slime.Damage();
slime.Dead();
}
}
有关“接口隔离原则”
接口隔离原则(Interface Segregation Principle,ISP)是面向对象设计原则中的一条。其核心思想是“客户端不应该依赖它不需要的接口”,目的是解决接口臃肿和不一致的问题。具体实现方法是将一个大的接口拆分成多个小的接口,每个小的接口只包含一个独立的功能。
假设我们要添加一个新的怪物,取名为“黄金史莱姆”,它的作用是可以对周围的怪物进行治疗,但不能攻击玩家。但它不能写在怪物基类里,因为怪物并不会治疗,这是黄金史莱姆的特有技能。黄金史莱姆不能攻击玩家,因此也不能直接继承怪物基类,所以需要对IEnemy接口里的方法拆成各个小接口,只关注各个接口实现即可。
interface IAttacker
{
void Attack();
}
interface IDamageable
{
void Damage();
}
interface IKillable
{
void Dead();
}
interface IHealer
{
void Heal();
}
class GoldenSlime : IDamageable, IKillable, IHealer
{
public void Damage()
{
Console.WriteLine("黄金史莱姆受伤!");
}
public void Dead()
{
Console.WriteLine("黄金史莱姆死亡!");
}
public void Heal()
{
Console.WriteLine("黄金史莱姆治疗了敌人!");
}
}
虽然从代码量上看着有点长,但从设计角度而言这是值得的。
三者联系
先说结论——“特别虚就是抽象,特别抽象就是接口”。
“特别虚就是抽象”
虚方法在达到某种“极端”的情况下,其实质上转变为了抽象方法,进而形成了抽象类。当一个类中包含了一个或多个只有声明没有实现的虚方法时,这个类就变成了抽象类,因为这样的类无法被实例化,必须由派生类提供具体实现。
像上文探讨的是基类中Attack方法对于派生类并不需要,所以改成抽象类。实际上基类的Attack方法不包含任何实现细节,即方法体为空,相当于base.Attack调用为空操作,也能实现效果。因此虚方法内容为空时可以理解为抽象(不是划等号)。
class Enemy //虚方法实现 派生类可以使用base关键字
{
public virtual void Attack()
{
}
}
abstract class Enemy //抽象类实现 派生类不能使用base关键字
{
public abstract void Attack();
}
“特别抽象就是接口”
如果抽象类中的所有方法都是抽象的,没有具体的实现,那么这个类实际上起到了接口的作用。
当一个抽象类中没有具体实现,只包含抽象方法时,它在功能上类似于接口,要求派生类必须提供所有抽象方法的实现。不过,从严格意义上讲,抽象类和接口在语言特性和使用场景上仍存在区别,比如抽象类可以包含非抽象成员,而接口不可以;一个类只能继承一个抽象类,但可以实现多个接口等。
abstract class Enemy //抽象实现Enemy类
{
public abstract void Attack();
public abstract void Damage();
public abstract void Dead();
}
interface IEnemy //接口实现Enemy类
{
void Attack();
void Damage();
void Dead();
}
抽象类侧重于描述一类事物的共性特征以及部分未知的具体实现,它可以有非抽象成员(即有具体实现的方法和属性),并且可以有多个层次的继承关系。
虚函数是为了实现多态,允许派生类改变或扩充基类中函数的行为。
接口则更加纯粹,只关注一组行为的规范而不关心内部实现细节,一个类可以实现多个接口,因此接口常用来表示“has-a”某种能力或者角色,而不是“is-a”某种类型的关系。
如有不对,欢迎指出,感谢观看!