携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第37天,点击查看活动详情
前言
面向对象编程中最重要的原则之一就是面向接口编程。而C#作为一个精心设计的面向对象高级语言,对接口有着良好的支持。
接口和类的区别
- 接口提供设计规范而非实现,而类主要是为了实现具体的功能。
- 接口中所有的方法都是隐式的抽象方法。而类即可以提供抽象方法也可以提供包含实现的具体方法。
- 一个类或结构体可以实现多个接口。而类只能继承自唯一的一个基类,结构体甚至不能自由继承(除了结构体本身继承自System.ValueType之外)。
接口的定义
举个例子:
public interface IWeapon
{
float GetDamage();
void Use();
}
- 接口使用
interface
关键字定义。 - 接口中只能包含方法,属性,事件和索引器这几种成员。而它们也是类中可以定义为抽象的类成员。
接口成员的访问级别
接口成员总是隐式的声明为public
,并且不能显式的声明任何访问修改符。因此,实现接口时必须为所有成员提供public
实现,例如:
public class Sword : IWeapon
{
private SwordConfig config;
public float GetDamage() => config.swordDamage;
pubiic void Use()
{
Debug.Log("Sword Attacks");
}
}
使用接口引用对象
接口的存在就是为了引用实现它的类的对象实例,使用接口变量而不是直接使用类变量,才是基于接口编程。例如:
IWeapon w = new Sword();
w.Use();
这是从具体类型Sword隐式转换为它所实现的接口IWeapon。 另外即便Sword实现了多个接口,也还是可以直接隐式转换为任意一个其实现的接口。
扩展接口
接口可以继承其他接口,但C#不说继承,而称之为扩展(Extending)。例如:
public interface IGameProperty
{
float GetPrice();
float GetWeight();
}
public interface IWeapon : IGameProperty
{
float GetDamage();
void Use();
}
实现IWeapon接口的类也必须实现IGameProperty接口中所有成员。
显式接口实现
当一个类实现了多个接口时,如果这些接口中有相同签名的成员,那么就会产生冲突。解决这个问题,可以通过显式的实现会产生冲突的接口成员。举个例子:
interface IWeapon { float GetWeight(); }
interface IArmor { int GetWeight(); }
public class MagicArmor : IArmor, IWeapon
{
public int GetWeight()
{
return 100;
}
public float IWeapon.GetWeight()
{
return 100.0f;
}
}
这个例子中,当在一个MagicArmor对象上调用 GetWeight()方法时,调用的是IArmor.GetWeight(),如果要调用IWeapon.GetWeight(),则需要显式转型:
MagicArmor ma = new MagicArmor();
ma.GetWeight(); //call IArmor.GetWeight()
((IWeapon)ma).GetWeight(); //call IWeapon.GetWeight()
显式接口实现除了是为了解决冲突之外,也可能是为了隐藏部分接口实现。
接口成员实现为虚函数
默认情况下,类实现接口的成员时,该成员是一个sealed成员。如果该类的子类需要改写该成员,则需要将其声明为virtual
或abstract
。
子类重新实现父类实现的接口
例如:
public interface IFoo { void Foo(); }
public class Bar: IFoo
{
void IFoo.Foo() { Debug.Log("Bar.Foo"); }
}
public class SubBar: Bar, IFoo
{
public void Foo() { Debug.Log("SubBar.Foo"); }
}
子类SubBar
在继承父类Bar
的同时,又声明其实现了接口IFoo
,而父类Bar
也实现了接口IFoo
。在子类SubBar
中对IFoo
的实现将覆盖父类中的IFoo
实现。
SubBar sb = new SubBar();
sb.Foo(); // SubBar.Foo
((IFoo)sb).Foo(); // SubBar.Foo
更优雅的做法
虽然子类重新实现可以覆盖父类实现的接口,但这么做有两个问题:
- 子类不能调用被覆盖的父类方法
- 父类被覆盖的方法可能并不适合被覆盖,覆盖它可能会引起潜在的bug。比如父类方法可能修改了一些内部状态,而子类并不知道这些内部状态需要设置,甚至这些内部状态是不可以被子类设置的。
为了解决这个问题,我们需要更优雅的做法,即在设计父类时就应该避免子类需要覆盖父类的方法。父类将子类可能要修改的方法定位为虚函数。
- 当父类是隐式实现接口时,将接口方法定义为虚函数。
- 当父类是显示实现接口时,另外定义一个虚函数并提供实现,然后在接口实现方法中调用它:
public class Bar: IFoo
{
void IFoo.Foo() => Foo();
protected virtual void Foo() { Debug.Log("Bar.Foo");}
}
public class SubBar: Bar
{
protected override void Foo() { Debug.Log("SubBar.Foo");}
}
当然了,这其实是设计问题,如果你掌握了这个设计技巧,就不会面临需要子类强行覆盖父类的接口实现的问题。但是如果你在使用一个第三方代码,遇到子类需要改写接口实现,那么覆盖是最后的方法。
接口和装箱
- 结构体转型为接口时会造成装箱。
- 但是在结构体上调用隐式实现的接口方法则不会引起装箱。 例如:
interface IFoo
{
void Foo();
}
struct Bar : IFoo
{
public void Foo(){}
}
Bar b = new Bar();
b.Foo(); // 不会发生装箱
IFoo i = b; //这儿的隐式转型会造成装箱
i.Foo();