.NET C#基础(7):接口 - 人如何和猫互动
0. 文章目的
面向有一定基础的C#初学者,介绍C#中接口的意义、使用以及特点。
1. 阅读基础
了解C#基本语法(如定义一个类、继承一个类)
理解OOP中的基本概念(如继承,多态)
2. 什么是接口
2.1 现实中的协定与接口
猫猫头在整理电脑文件,需要一个小工具来分类文件,于是猫猫头向群里求助:
“有没有小伙伴帮我用Objective-C做一个分类文件的小工具”
群里没有人回答,猫猫头意识到可能是因为会Objective-C的人比较少,于是改问:
“有没有小伙伴帮我用Rust做一个分类文件的小工具”
群里依然没有人回答,猫猫头意识到可能是会Rust的人比较少,但猫猫头此时还意识到,自己只是需要获一个可以分类文件的小工具,用什么语言好像并不重要。于是,猫猫头想了一下,改问:
“有没有小伙伴可以帮我做一个分类文件的小工具”
很快,群里有人用Shell帮猫猫头写了一个小工具,猫猫头用小工具很快完成了任务。
上述例子中,猫猫头在请求帮助时,给出了一个可以帮忙上的‘前提’,即可以提供一个可以分类文件的小工具,而通过这个前提,猫猫头的朋友知道如何帮助猫猫头。我们将这种用于指示两个实体之间(比如猫猫头和TA的朋友之间)如何交互的‘前提’称之为‘协定’。
协定的最大意义在于规范了不同物件间的交互方式,一个物件如果想要知道如何与另一个物件交互,只需要了解与该物件交互所需要遵守的协定,而不需要考虑该物件的具体情况。就像猫猫头只需要一个能分类文件的小工具,而帮忙的朋友到底如何实现这个小工具其实并无所谓。再举个例子,在Windows中通常你只需要点击窗口右上角的三个按钮就可以发出最小化最大化以及关闭窗口的操作指令,而程序则会根据你的指令执行相应操作,这三个按钮就是程序为你提供的与它交互的接口,而至于程序到底如何实现窗口的最小化最大化以及关闭操作,你并不需要关心。
而现实中所谓的接口就是一种协定,例如设备的USB充电接口,任何只要满足USB规范的连接线都可以接入其充电接口并为其供电,而至于电从哪里来设备本身并不关心。接口是使物件得以模块化的重要概念,接口定义了一种‘只要满足即可交互’的规范,这可以极大程度上降低物件之间的耦合。
对于编程语言来说,接口的主要作用也是用于为各个模块之间做出协定,通过协定,模块之间知道如何进行交互,而不需要为各种模块进行特别编码,以此可以最大程度减小模块间的耦合度。通常,在编码时我们所称的接口有两种,一种是模块对外公布的可以完成某一任务的操作方式,例如对于以下类模块:
class Speaker
{
public static void Say()
{
Console.WriteLine("Hello World!");
}
}
如果要让这个类输出‘Hello World!’应该怎么办呢?显然,那就是调用其Say方法:
Speaker.Say();
也就是说,这个类可以实现输出‘Hello World!’的操作,而类对外提供完成这一操作的途径就是调用其提供的Say方法。此时,这个Say就是这个Speaker类提供的接口,它对外协定了要如何让自己输出‘Hello World!’,外部只需按照该协定使用即可。此外,Say方法只协定了如何进行交互,对于外部来说,只需要知道通过调用Say方法就可以输出‘Hello World!’,而对Speaker类来说只需要保证Say方法可以实现输出‘Hello World!’即可,至于具体怎么实现并不重要。
而狭义的接口,就是指一种用于设计类的特殊类型(例如许多OOP语言中的‘interface’类型),本文阐述的是狭义的接口。
2.2 继承与抽象类
(1)基石:继承与多态
在具体讨论接口是什么之前,需要先知道接口最早的样子是什么。首先我们定义一个Animal类,定义一个名为MakeSound的虚方法:
class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("make some noise");
}
}
接下来从Animal派生出两个类,并重写其MakeSound方法:
class Cat
{
public override void MakeSound()
{
Console.WriteLine("meow meow");
}
}
class Dog
{
public override void MakeSound()
{
Console.WriteLine("woof woof");
}
}
由于Cat与Dog继承自Animal,因此像下面这样代码是可以使用的:
void Pet(Animal animal)
{
animal.MakeSound();
}
Cat cat = new Cat();
Pet(cat);
Dog dog = new Dog();
Pet(dog);
(会输出meow meow与woof woof)
Animal类中定义了MakeSound方法,因此其肯定有一个MakeSound方法可以调用,而Cat与Dog都是Animal的子类,因此两者也肯定有一个MakeSound方法可以调用,所以上述代码是安全的。当调用时,由于多态,上述代码会输出‘meow meow’与‘woof woof’。
(2)进一步抽象
上面的代码中,Animal对MakeSound方法的定义的唯一意义在于保证了其有一个名为MakeSound的方法可以调用,而上文的Pet方法里调用Animal对象的MakeSound方式时,因为多态,实际调用的是Cat与Dog中重写MakeSound的方法。也就是说,Animal如何实现MakeSound方法并不重要,因此完全可以不提供Animal中MakeSound方法的实现:
class Animal
{
public virtual void MakeSound()
{
// 方法实现并不重要
};
}
然后另一方面,很多时候我们希望调用的MakeSound方法能做一些有意义的事,这就需要继承自Animal的类都重写该方法,所以我们可能还希望可以在编码时要求Animal的子类重写其MakeSound方法。
C#提供了实现上述需求的方法,在C#中,有一种特殊的类叫做抽象类(abstract class),这种类不允许实例化,并且允许定义一种被称为抽象方法(abstarct method)的方法,抽象方法指的是在当前类中不提供实现,转而由子类提供实现的方法。下面是将Animal类转化为抽象类的示例:
abstract class Animal
{
public abstract void MakeSound(); // 抽象方法不需要也不能提供方法实现,实现由子类完成
}
(在class关键字前添加abstract可将类定义为抽象类,为方法添加abstract修饰符可将方法定义为抽象方法)
现在,继承自Animal类的类型都必须重写其抽象方法MakeSound了,重写抽象方法和重写一般的虚方法一致:
class Cat
{
public override void MakeSound()
{
Console.WriteLine("meow meow");
}
}
class Dog
{
public override void MakeSound()
{
Console.WriteLine("woof woof");
}
}
使用上也没有什么区别:
void Pet(Animal animal)
{
animal.MakeSound();
}
Cat cat = new Cat();
Pet(cat);
Dog dog = new Dog();
Pet(dog);
不知道看到这里你是否意识到了什么:Animal类保证了其子类有一个MakeSound方法可以被调用,所以Pet方法可以假定所有Animal的子类都有一个MakeSound方法,因此可以安全调用。也就是说,Animal类向外保证了其子类必然有一个MakeSound方法可以调用,而继承自Animal的Cat与Dog类都实现了Animal对外的保证。
(3)接口
再进一步来看,其实Pet方法中只需要对象可以提供MakeSound方法即可,至于对象的类型是否与Animal之间存在‘IS-A’关系并不重要,也就是说,对Pet方法来说只要对象可以‘保证’有一个MakeSound方法可以调用即可。现在我们将这一保证抽象出来,并用一个语义更清晰的类名来指代这种保证:
abstract class CanMakeSound
{
public abstract void MakeSound();
}
再进一步,在语言层面上提供语法支持来将其与抽象类区分开(定义为一个‘保证’,而不是一个类),我们使用interface来表示这一保证:
interface CanMakeSound
{
public abstract void MakeSound();
}
由于这只是一种保证,所有里面的方法都只是一种协定(表示有某个方法可以调用),因此方法只需要方法签名即可(返回类型+方法名+参数列表),另外,既然是‘对外保证’可以调用某一方法,那么方法也应该是public的,所以可以默认所有的方法声明都是public的抽象方法:
interface CanMakeSound
{
void MakeSound();
}
这样,通过一步步对抽象类的提炼,我们提出了接口(interface)的这一概念,在简化语法的同时,CanMakeSound提供的‘保证’也有了更明确的语义。
这就是接口的本质:协定行为,指明‘可以做什么’,即‘CAN-DO’。接口是一个概念,不同的语言对接口概念的实现方式不同,例如C#为接口提供了语言级的支持,而一些没有为接口提供语言级支持的编程语言也可以通过上述的抽象类来模拟接口,例如C++,另一方面,C#中的接口其实也可以视为一种特殊的抽象类。
不过在继续之前,我们来看看如果C#没有对接口的支持将会是什么样。在上述例子中我们一开始用抽象类来模拟接口,由于C#不支持多重继承,因此无法同时实现多个抽象类,如果要表示实现多个‘接口’就不得不一层一层继承下去,这会使编码变得复杂以至于难以维护:
abstract class Walkable { ... }
abstract class Flyable { ... }
class WalkableCat : Walkable { }
class WalkableAndFlyableCat : Flyable { }
class SuperCat : WalkableAndFlyableCat { }
上述代码中SuperCat使用了两个辅助类WalkableCat和WalkableAndFlyableCat才得以同时实现Walkable与Flyable,甚至概念上来说SuperCat只是WalkableAndFlyableCat的子类,而不是有‘可以做什么’的保证,语义上也缺乏清晰度。因此,C#将接口视为一种专门的类型,并提供语言级的支持是很有必要的。
3. C#中的接口
3.1 定义接口
要在C#中定义一个接口,需要使用interface关键字,并在接口中定义协定的方法,下面是一个接口定义:
interface IFlyable
{
void Fly();
}
(根据C#的编码建议,接口命名应该以大写字母I开头)
上述代码中,接口IFlyable协定了实现该接口的的类型都会有一个Fly方法可以调用,你可以把它想象成下面这样的抽象类:
abstract class IFlyable
{
public abstract void Fly();
}
对比两者,可以发现定义接口时除了将abstract class替换为了interface外,对方法也没有用public与abstract修饰,这一原因在前文已经提及过:由于接口本身就是用于对外协定行为,因此接口协定的方法自然应该是可以被外部访问的public,同时接口中的方法只是提供一种协定,因此无需提供实现。
当然,一个接口中可以协定多个方法:
interface IFlyable
{
void Prepare();
void Fly();
}
但是,接口中不能定义字段:
interface IFlyable
{
string Name; // 不允许定义字段
}
这是由于接口的作用是协定行为,而所谓的行为就是方法。但是,你可以在接口中定义属性甚至事件:
interface IFlayable
{
event Action Prepared;
string Name { get; set; }
string this[int index] { get; set; } // 索引器也是属性(有参数性)
}
这是由于属性本质上是方法,事件本质上是对多播委托的方法封装,因此上面接口定义的实际含义如下:
interface IFlayable
{
// event Action Prepared;
void add_PreparedHandler(Action action); // 注册委托
void remove_PreparedHandler(Action action); // 取消注册委托
// string Name { get; set; }
string get_Name(); // 获取Name属性
void set_Name(string val); // 设置Name属性
// string this[int index] { get; set; }
string get_Item(int index); // 获取Item属性
void set_Name(int index, string val); // 设置Item属性
}
3.2 使用接口
3.2.1 实现接口
接口定义后,就可以让类型去实现了。要让一个类型实现接口很简单,只需要‘继承’该接口,然后实现该接口中协定过的方法即可,例如下面用SuperCat实现IFlyable接口:
interface IFlyable
{
void Fly();
}
class Cat { }
class SuperCat : Cat, IFlayable
{
public void Fly()
{
Console.WriteLine("Flying");
}
}
上述代码中特地让SuperCat继承自Cat类,只是为了说明实现一个接口的语法和继承一个类型的语法相似,并且接口的位置要位于基类之后,所以下面这样是不行的:
class SuperCat : IFlayable, Cat { } // 错误,基类要在接口位置之前
实现接口的方法时只需要保证方法签名相同(返回类型+方法名+参数列表),而方法本身可以添加async、unsafe等方法修饰符,因此下面SuperCat中的Fly方法也可以实现IFlyable:
class SuperCat : Cat, IFlayable
{
public unsafe async void Fly()
{
Console.WriteLine("Flying");
}
}
另外,与继承类最大的不同在于,类型可以实现多个接口,只实现了各个接口协定的方法即可:
// SuperCat继承自Cat
class SuperCat : Cat, IFlyable, IWalkable, ISoundMaker
{
// 实现IFlyable
// 实现IWalkable
// 实现ISoundMaker
}
3.2.2 调用接口
可以像使用抽象类引用一样使用接口引用,如下面使用IFlyable:
void Call(IFlyable flyable)
{
flyable.Fly(); // IFlyable接口协定了实现该接口的方法都有一个Fly方法可以调用
}
SuperCat superCat = new SuperCat();
Call(superCat);
简而言之,将接口视为一个抽象类使用即可。
3.3 进阶接口操作
3.3.1 接口“继承”
接口与接口之间也可以继承,例如:
interface IMachine
{
void Launch();
}
interface IGameConsole : IMachine
{
void PlayGame();
}
上述代码中IGameConsole接口“继承”了IMachine接口,这意味着实现IGameConsole接口时除了要实现其协定的PlayGame方法外,也需要实现IMachine接口中协定的Launch方法:
class Xbox : IGameConsole
{
public void Launch() { ... } // IMachine协定的Launch方法
public void PlayGame() { ... } // IGameConsole协定的PlayGame方法
}
之所以要为“继承”添加引号,是因为接口间的继承行为从概念上来说应该称为‘组合’,也就是说,IGameConsole并不是继承了IMachine接口,而是对IMachine接口进行了组合。请记住这个概念,在后文中会提及原因。
3.3.2 显式实现接口
有时候多个接口协定的方法之间可能存在冲突,例如:
interface IGameConsole
{
void Launch(); // 启动游戏
}
interface IMachine
{
void Launch(); // 启动机器
}
class Xbox : IGameConsole, IMachine
{
public void Launch() { ... } // 只能定义一个Launch
}
上述代码中的接口IGameConsole与IMachine都协定了一个Launch方法,然而两个Launch方法所做的事并不一样,因此需要分别对其进行实现。但显然你只能定义一个Launch方法。要解决此类问题,就需要显式实现接口。以上述情况为例,显式实现接口的方法如下:
class Xbox : IGameConsole, IMachine
{
void IGameConsole.Launch() { ... } // 实现IGameConsole的Launch
void IMachine.Launch() { ... } // 实现IMachine的Launch
}
有两个需要点需要关注:
- 显式实现的方法没有访问修饰符
- 显式实现的方法名为
接口名.方法名
由于显式实现的方法没有访问修饰符,意味着其访问权限是默认的private,外部无法直接调用,但可以通过接口引用来调用相应的方法:
XBox xbox = new XBox();
// xbox.GetInformation(); // 不能直接调用
IGameConsole console = xbox; // 使用IGameConsole引用
ConsoleInfo a = console.GetInformation(); // 调用Xbox中的IGameConsole.GetInformation
IMachine machine = xbox; // 使用IMachine引用
MachineInfo b = machine.GetInformation(); // 调用Xbox中的IMachine.GetInformation
另外,对于类内部来说,也需要使用接口引用来调用:
class Xbox : IGameConsole, IMachine
{
void IGameConsole.Launch() { ... } // 实现IGameConsole的Launch
void IMachine.Launch() { ... } // 实现IMachine的Launch
void Test()
{
IGameConsole console = this; // 使用接口引用调用IGameConsole的Launch方法
console.Launch();
}
}
顺便一提,由于要通过接口引用来调用方法,因此对值类型来说此时将会面临装箱问题。一般情况下,应该尽可能选择默认的实现方式(即通过定义方法签名相同的方法)而非显式实现。
现在回过头来谈论一下为什么接口之间的“继承”实际是‘组合’,还是对于下述列子:
interface IMachine
{
void Launch(); // 这个Launch用来启动机器
}
interface IGameConsole : IMachine
{
void Launch(); // 这个Launc用来启动游戏
}
class Xbox : IGameConsole
{
void IGameConsole.Launch() { ... }
void IMachine.Launch() { ... }
}
你应该很快能理解上述代码的意图:IGameConsole组合了IMachine接口,Xbox实现IGameConsole接口,并显式实现了两个GetInformation方法。如果接口之间是继承,那么上述代码从概念上说不通:IGameConsole继承自IMachine,然后重写了其同名的Launch方法,然而我们在实现接口的时候却分别实现了IMachine和IGameConsole的Launch方法,那么IGameConsole到底继承了IMachine什么?另一方面来讲,协定的行为又如何用继承关系描述?总不能说‘可以启动游戏’继承了‘可以开机’吧。因此,应当认识到接口之间的“继承”实质是组合,即将协定进行组合。
3.3.3 接口方法的默认实现
尽管接口协定的方法默认是抽象方法,但是你确实可以在接口中为协定的方法提供实现,这种方法被称为默认接口方法:
interface IGameConsole
{
void Launch()
{
Console.WriteLine("Launched");
}
}
class Xbox : IGameConsole
{
// 此时不需要为IGameConsole协定的Launch方法提供实现
}
Xbox xbox = new Xbox();
// 由于Launch方法在Xbox中没有声明为public,因此需要通过接口引用来调用
IGameConsole console = xbox;
console.Launch();
(输出:Launched)
实现接口时可以不实现接口中提供了默认实现的方法,但是由于方法没有在实现该接口的类型中定义为public,因此此时该方法对外部来说无法访问,此时则同样需要通过接口引用来调用相应方法。另外,不必担心两个接口的默认实现出现冲突,如下:
interface IGameConsole
{
void Launch()
{
Console.WriteLine("GameConsole Launched");
}
}
interface IMachine
{
void Launch()
{
Console.WriteLine("Machine Launched");
}
}
class Xbox : IGameConsole, IMachine
{
}
这是因为如果XBox没有实现IGameConsole与IMachine的Launch方法,那么外部要使用这两个接口协定的方法就只能通过接口引用,那么这时候显然可以明确要调用的方法:
Xbox xbox = new Xbox();
IGameConsole console = xbox;
console.Launch(); // IGameConsole默认实现的Launch
IMachine machine = xbox;
machine.Launch(); // IMachine默认实现的Launch
此外,如果对接口组合时出现方法冲突,编译器会给出警告,此时可以使用new关键字来抑制警告:
interface IMachine
{
void Launch()
{
Console.WriteLine("Machine Launched");
}
}
interface IGameConsole : IMachine
{
new void Launch() // new的含义是:本类型中与基类中相似签名的成员没有关系
{
Console.WriteLine("GameConsole Launched");
}
}
(同样此时只能通过接口引用来调用接口方法,因此也不会出现冲突)
最后,如果类型中实现了接口的默认方法(无论是默认实现还是显式实现),那么就会覆盖默认实现:
interface IGameConsole
{
void Launch()
{
Console.WriteLine("Launched");
}
}
class Xbox : IGameConsole
{
public void Launch() // 实现IGameConsole的Launch方法
{
Console.WriteLine("Xbox Launched");
}
}
Xbox xbox = new Xbox();
IGameConsole console = xbox;
console.Launch(); // 此时调用的是Xbox中定义的Launch
(上述代码输出‘Xbox Launched’)
然而,不推荐使用接口默认实现,因为接口本身应该只提供协定功能,一般情况下如果需要有默认实现更应该考虑使用基类而不是接口,或者定义一个实现了该接口的类,并在这个类中提供实现:
interface IGameConsole { ... }
class GameConsoleBase : IGameConsole { ... }
通常真正需要默认实现的场合是需要更新某个接口,但又不希望影响之前已经使用了该接口的代码。
3.3.4 为接口添加静态方法
你可以在接口中定义静态方法:
interface IFoo
{
static void Hello()
{
Console.WriteLine("Hello");
}
}
其使用和使用一般类的静态方法没有区别:
IFoo.Hello();
静态方法不属于接口的协定,因此实现接口时不需要实现接口中的静态方法,你可以把接口中的静态方法理解为一个由该接口管理的方法。不过,C#目前有一项预览功能,可以像协定普通方法一样协定静态方法,这在后文会提到。
3.3.5 指定接口成员的访问修饰符
接口成员默认的访问修饰符为public,但你可以指定为其他修饰符:
interface IFoo
{
private void PrivateMethod() // private访问限制,必须提供默认实现
{
Console.WriteLine("Require defualt Implement");
}
protected void ProtectedMethod(); // protected访问限制,可在组合了该接口的接口中使用
internal void InternalMethod(); // internal访问限制,同一程序集范围内可用
public void PublicMethod(); // pubilc访问限制,默认的访问限制
// ... 还有一些很少用的组合访问修饰符,这里就不提了
}
而当访问限制为private时,由于这个方法只能用在接口内部访问,无法在接口外为其提供实现,所以必须为其提供默认实现,通常private访问级别是用于实现默认接口方法的辅助方法。另外再特别说明一下访问限制为protected时的情况。当接口中协定的方法的访问限制为protected时,如果要实现该方法,则必须显式实现,否则不会被视为该方法的实现:
class Foo : IFoo
{
void IFoo.ProtectedMethod() { ... } // 显式实现IFoo中的ProtectedMethod
protected void ProtectedMethod() { ... } // 只是定义了一个ProtectedMethod方法而已,与接口无关
}
另外,由于你只是‘实现’了该接口而不是‘继承’了该接口,所以你也无法调用其方法:
class Foo : IFoo
{
void IFoo.ProtectedMethod() { ... }
void Test()
{
IFoo foo = this;
foo.ProtectedMethod(); // Foo和接口IFoo之间不是继承关系,无法调用
}
}
这类方法只能在组合该接口的接口中调用:
interface IFoo
{
protected void ProtectedMethod();
}
interface Foo2 : IFoo
{
void DoSomething()
{
ProtectedMethod(); // 可以访问IFoo中的ProtectedMethod方法
}
}
你可能觉得这种既不能被外部访问也不能被内部访问的方法没什么用,不过下面是一个使用例子:
查看代码
interface ISpeaker
{
protected void Say(); // protected访问权限,只能在接口内以及组合了该接口的接口内使用
}
interface IRepeater : ISpeaker // IRepeater组合了ISpeaker
{
void Repeat() // Repeat的默认实现,调用三次ISpeaker协定的Say方法
{
Say();
Say();
Say();
}
}
class HelloRepeater : IRepeater
{
void ISpeaker.Say() // 显式实现了ISpeaker中协定访问等级为protected的Say方法
{
Console.WriteLine("Hello!");
}
}
class WorldRepeater : IRepeater
{
void ISpeaker.Say() // 显式实现了ISpeaker中协定访问等级为protected的Say方法
{
Console.WriteLine("World!");
}
}
void CallRepeater(IRepeater speaker)
{
speaker.Repeat();
}
CallRepeater(new HelloRepeater());
CallRepeater(new WorldRepeater());
(输出三次Hello!与三次World!)
尽管如此,通常来说访问级别为protected的接口方法确实没什么明显的作用,但在一些特殊的情况下可能对于封装和修改现有系统有帮助。
3.4 特殊接口
3.4.1 泛型接口
可以声明一个泛型接口:
interface IDataObject<T>
{
T GetData();
}
上面定义了一个泛型接口IDataObject<>,泛型接口并不神秘,就如可以像理解抽象类一样去理解接口,同样可以用理解泛型类的方式去理解泛型接口,由于泛型不是本文重点故不多做阐述。不过,为了提高泛型接口的实用性,泛型接口还支持将类型参数声明为协变量或逆变量,关于协变与逆变在另一篇文章里有所阐述,这里也不再赘述。
3.4.2 协定静态成员的接口
前文提到过接口允许协定静态成员,但截至目前这是C#的一项预览功能,需要在项目的csproj配置文件中向PropertyGroup
添加EnablePreviewFeatures
以启用语言的预览功能:
<EnablePreviewFeatures>True</EnablePreviewFeatures>
一个协定了静态方法的接口如下(注意由于接口中本身可以定义静态方法,所以需要添加abstract关键字来指示其为抽象方法):
interface IGameConsole
{
static abstract void PrintInfo();
}
以及一个实现该接口协定的静态方法的类:
class Xbox : IGameConsole
{
public static void PrintInfo()
{
Console.WriteLine("A famous game console");
}
}
咋一看好像协定静态方法的意义不大,因为调用静态方法需要直接使用类型名,无法通过接口引用来调用。但是,考虑以下代码:
interface IAddable<T> where T : IAddable<T>
{
static abstract T operator +(T left, T right);
}
class Math<T> where T : IAddable<T>
{
public static T Add(T left, T right)
{
return left + right;
}
}
由于运算符重载的本质是定义一个静态方法,因此协定静态方法意味着可以对类型允许使用的运算符进行协定,意味着可以在泛型中假定泛型类型可以进行+-*/等运算,这对于泛型约束来说非常有用,要知道在相当的一段时间里C#是没有办法假定泛型类型可以使用运算符的。需要说明的是你可能注意到上述代码中泛型接口IAddable<>的 类型参数T的泛型约束是where T : IAddable<T>
,看起来有点别扭,这是因为运算符重载要求其参数类型至少有一个是当前类型,因此需要T是IAddable<T>类型(即自己)。
4. 接口杂谈
4.1 接口在继承链中的传递
首先定义一个接口,基类以及其子类:
interface IGameConsole
{
public void Launch();
}
class Xbox : IGameConsole
{
public void Launch()
{
Console.WriteLine("Xbox Launched!");
}
}
class XboxOne : Xbox { }
既然基类实现了接口,那么其子类必然也满足接口的实现,因此下面的代码可以按预期运行:
IGameConsole console = new XboxOne(); // IGameConsole可以引用XboxOne
console.Launch(); // 输出Xbox Launched!
子类自然可以重新实现父类中实现过的接口协定,但是要分情况:
(1)父类中的方法不是虚方法,需要重新实现接口:
class XboxOne : Xbox, IGameConsole // 声明重新实现IGameConsole
{
public new void Launch()
{
Console.WriteLine("Xbox One Launched!");
}
}
(方法中的new修饰符不是必须的,但是加上会让语义更清晰)
这样在使用接口引用时才能调用到正确的方法:
IGameConsole console = new XboxOne();
console.Launch(); // 输出Xbox One Launched!
而下面的实现无法正确重新实现:
class XboxOne : Xbox
{
public void Launch()
{
Console.WriteLine("Xbox One Launched!");
}
}
毕竟Xbox的Launch方法只是一个普通非虚方法,只是它可以实现IGameConsole接口的协定而已。
(2)父类中的方法为虚方法,直接重写该虚方法即可:
class XboxOne : Xbox
{
public override void Launch() // 如果基类的Launch方法被修饰为虚方法,则直接重写即可
{
Console.WriteLine("Xbox One Launched!");
}
}
当然,严格来说这是多态的功劳(虽然接口本身的实现也依赖多态就是了)。
4.2 多重实现
可以同时对接口进行默认实现和显式实现:
interface IGameConsole
{
void Launch();
}
class Xbox : IGameConsole
{
public void Launch() // 默认实现
{
Console.WriteLine("Xbox Launched!");
}
void IGameConsole.Launch() // 显式实现
{
Console.WriteLine("A Game Console Launched");
}
}
不过从实际上来说,同时定义默认实现和显式实现后,真正实现接口的是显式实现,而默认实现此时就只是普通的方法而已。原因是因为使用接口无外乎通过下面两方式:
Xbox xbox = new Xbox();
xbox.Launch(); // 输出Xbox Launched!
IGameConsole console = xbox;
console.Launch(); // 输出A Game Console Launched
第一是直接通过对象使用,但其实这和接口无关,这本身就只是一个普通的方法调用。
第二是通过接口引用调用,这时候接口引用将调用显式实现,因此显式实现才是真正的实现。
这是合理的,因为显式实现的语义更明确。
5. 使用建议
5.1 基类 or 接口?
使用基类有时也可以做到接口能做的事,而另一方面在面向接口编程成为一种流行时可能让人在更应该使用基类的地方使用接口。关于选择基类还是接口,主要有如下几点可以考虑:
(1)IS-A还是CAN-DO。在OOP中继承主要是描述类型间的IS-A关系,例如Cat继承自Animal,因为Cat是一种(IS-A)Animal,而接口主要用于表示‘CAN-DO’,即表示类型‘可以做什么’。
(2)是否需要储存状态或定义复杂行为。基类可以定义字段,提供更丰富的方法实现,而接口不能定义字段并且方法通常都是抽象方法(不推荐使用默认实现)。
(3)行为是否比类型重要。例如某一位置只需要使用类型FileUtil的某个方法来获取文本数据,那么此时重要的是获取文本数据,至于数据是不是FileUtil提供的其实并不重要,此时若将获取数据这一行为抽象为接口可以获得提供更好的泛用性。
5.2 对接口的一些使用建议
(1)接口名以I开头,并且尽可能以‘CAN-DO’风格命名,或者合适时使用名词亦可。下面是良好的接口名例子:
interface IEnumerable { ... } // 表示可遍历
interface IList { ... } // 表示可以进行类似列表的操作
interface IFlyable { ... } // 表示能飞
interface IDataProvider { ... } // 表示能提供数据
(2)接口一旦定义后应该尽可能避免修改,因为接口是协定,类型之间基于接口的协定而做出假设进行交互,随意更改会使程序的维护变得复杂。
(3)接口的协定应该尽可能少,能满足其接口名描述的行为即可。多个简单的接口组合胜过一个所谓的“万能”接口。
(4)接口应该提供详细的注释,以阐述该接口的协定。