? 在C#8.0中,针对接口引入了一项新特性,就是可以指定默认实现,方便对已有实现进行扩展,也对面向Android和Swift的Api进行互操作提供了可能性。下面我们来看看该特性的具体规则与实现。
一、主要应用场景:
在不破坏影响已有实现的情况下,可以添加新成员。这解决了在第三方已经大量使用了的接口上进行扩展带来问题的痛点。
二、规则与限制:
1. 支持的成员:方法、属性、索引器、 及各种静态成员。不支持实例字段、实例事件、自动属性、实例构造和析构函数
2. 支持修饰符:private, protected, internal, public, virtual, abstract, sealed, static, extern, and partial.
3. 默认访问级别为public,可以显式指定,也可以不指定。
4. 除过sealed和private修饰的方法体之外,其他带有方法体的成员默认都是virtural成员
5. 接口中的默认实现只属于该接口和继承它的子接口,但不能被它的实现继承,所以只能通过接口变量调用。除非接口的实现中进行了再次实现。
6. 多层次继承的接口,调用最接近实现的接口的默认实现。也就是“层次最接近、最新实现最近、同级的new比overrided更接近”。
7. 在类中实现并覆盖接口中的成员,无需用new和override关键字,与接口的实现机制是保持一致,无需任何修改或操作。
三、实现举例:
1. 先定义一个接口IFlyable,代码如下:
public interface IFlyable { //支持const常量 public const int MAX_SPEED = 200; const int MIN_SPEED = 0; //默认public,可以省略 public const string SPEED_UOM = "m/s"; private static readonly Dictionary<string, string> nameDic; //支持静态构造函数,不支持实例构造函数 static IFlyable() { nameDic = new Dictionary<string, string>() { {nameof(MAX_SPEED),MAX_SPEED.ToString()}, {nameof(MIN_SPEED),MIN_SPEED.ToString()} }; } //支持索引器,但是其中的变量也只能是静态变量。 string this[string key] { get { string tmp; if (nameDic.ContainsKey(key)) { tmp = nameDic[key]; } else { tmp = string.Empty; } return tmp; } } int Speed { get;} //默认为public和virtual,所以此处的virtual和public可有可无 public void Initialize() { var defaultSpeed = AverageSpeed(); Initialize(defaultSpeed); WriteLine($"{nameof(IFlyable) + "." + nameof(Initialize)} at default {defaultSpeed} {SPEED_UOM}"); } // 私有带有方法体的成员是允许存在的 private int AverageSpeed() { return (MAX_SPEED + MIN_SPEED) / 2; } void Initialize(int speed); //默认为public和virtual,所以此处的virtual和public可有可无 void Fly() { WriteLine($"{nameof(IFlyable) + "." + nameof(Fly)}"); } }
2. 再定义一个IAnimal接口:
public interface IAnimal { //默认为public和virtual,可以显式指出该成员时virtual void SayHello() { WriteLine($"{nameof(IAnimal) + "." + nameof(SayHello)}"); } void Walk() { WriteLine($"{nameof(IAnimal) + "." + nameof(Walk)}"); } }
3. 定义一个IFlyableAnimal接口,继承自前两个接口
public interface IFlyableAnimal:IAnimal,IFlyable { public new const int MAX_SPEED = 300; //重写IAnimal的SayHello, void IAnimal.SayHello() { WriteLine($"override {nameof(IFlyableAnimal) + "." + nameof(SayHello)} "); } //因为IFlyableAnimal接口继承了IAnimal的接口,加new关键字来隐藏父类的继承的SayHello,可以不加,但会有警告。 public new void SayHello() { WriteLine($"new {nameof(IFlyableAnimal) + "." + nameof(SayHello)}"); } //因为IFlyableAnimal接口继承了IFlyable的接口,接口继承的默认实现是无法用override关键字的 public void Walk() { WriteLine($"new {nameof(IFlyableAnimal) + "." + nameof(Walk)}"); } }
4. 定义一个类Sparrow,来实现接口IFlyableAnimal
public class Sparrow : IFlyableAnimal { //实现IFlyable中的Speed接口 public int Speed { get; private set; } //实现IFlyable中的Initialize(int speed)接口 public void Initialize(int speed) { this.Speed = speed; WriteLine($"{nameof(Sparrow) + "." + nameof(Initialize)} at {Speed} {IFlyable.SPEED_UOM}"); } //实现并覆盖接口中的SayHello,无需用new和override关键字,与接口的实现机制是保持一致,无需任何修改或操作。 public virtual void SayHello() { // 注意的使用IFlyableAnimal.SPEED_UOM,类只能实现接口,不能继承接口的默认实现,但是接口可以继承父接口的默认实现 WriteLine($"{nameof(Sparrow) + "." + nameof(SayHello)} at {Speed} {IFlyableAnimal.SPEED_UOM}"); } }
5. 对前面的定义进行调用
static void Main(string[] args) { Sparrow bird = new Sparrow(); bird.Initialize(98); //Sparrow中实现并覆盖了Initialize,所以可以直接用类调用 bird.SayHello();//Sparrow中实现并覆盖了,所以可以直接用类调用 //bird.Fly(); Fly不可访问,因为Bird没有实现也不会继承接口中的实现,所以不拥有Fly方法。 //IFlyableAnimal 继承了IAnimal和IFlyable的默认实现,通过该变量调用SayHello和Fly方法 IFlyableAnimal flyableAnimal = bird; flyableAnimal.SayHello(); flyableAnimal.Fly(); //IFlyableAnimal继承自IAnimal和IFlyable,而Sparrow类又继承自了IFlyableAnimal,所以可以用IAnimal和IFlyable变量调用 IAnimal animal = bird; animal.SayHello(); IFlyable flyable = bird; flyable.Initialize(); flyable.Fly(); Monster monster = new Monster(); IAlien alien = monster; //alien.Fly();//编译器无法分清是‘IBird.Fly()‘ 还是 ‘IInsect.Fly()‘ } //输出: //Sparrow.Initialize at 98 m/s //Sparrow.SayHello at 98 m/s //Sparrow.SayHello at 98 m/s //IFlyable.Fly //Sparrow.SayHello at 98 m/s //Sparrow.Initialize at 100 m/s //IFlyable.Initialize at default 100 m/s //IFlyable.Fly
四、多层次继承产生的问题
因为接口时可以多重继承的,这样就会出现类似C++里产生菱形继承问题。如下图所示,IBird和IInsect都继承了IFlyable,而IAlien又同时继承IBird和IInsert两个接口,并做了新的实现,这时,他们就构成了一个菱形或者钻石形状。这时候,实现了IAlien的Monster类,就会无法分清改用哪个Fly
代码如下:
public interface IBird : IFlyable { void Fly() { WriteLine($"{nameof(IBird) + "." + nameof(Fly)}"); } } public interface IInsect : IFlyable { void Fly() { WriteLine($"{nameof(IInsect) + "." + nameof(Fly)}"); } } public interface IAlien : IBird, IInsect { } public class Monster : IAlien { public int Speed { get; private set; } = 1000; public void Initialize(int speed) { this.Speed = speed; } }
下面调用语句alien.Fly就会导致混淆,编译器无法分清此Fly到底是IBird.Fly() 还是IInsect.Fly():
static void Main(string[] args) { Monster monster = new Monster(); IAlien alien = monster; //alien.Fly();//编译器无法分清是‘IBird.Fly()‘ 还是 ‘IInsect.Fly()‘ }
对于这种问题的解决方案,是要么通过更为具体的接口(IBird或IInsect)来调用相应成员,要么在Monster类中实现Fly,再通过类来调用。
五、总结
C#8.0的接口默认实现对于软件的功能的扩展提供了比较大的灵活性,同时,也引入了一些规则,使得掌握其的成本增加。这里,我尽力求对其一些规则等做出了总结,并展现了示例予以说明,肯定又不足指出,希望大家指正。