4.5 接口
如前所述,如果一个类派生自一个接口,声明这个类就会实现某些函数。并不是所有的面向对象语言都支持接口,所以本节将详细介绍C#接口的实现。下面列出Microsoft预定义的一个接口System.IDisposable的完整定义。IDisposable包含一个方法Dispose(),该方法由类实现,用于清理代码:
public interface IDisposable { void Dispose(); }
上面的代码说明,声明接口在语法上与声明抽象类完全相同,但不允许提供接口中任何成员的实现方式。一般情况下,接口只能包含方法、属性、索引器和事件的声明。
比较接口和抽象类:抽象类可以有实现代码或没有实现代码的抽象成员。然而,接口不能有任何实现代码;它是纯粹抽象的。因为接口的成员总是抽象的,所以接口不需要abstract关键字。
类似于抽象类,永远不能实例化接口,它只能包含其成员的签名。此外,可以声明接口类型的变量。
接口既不能有构造函数(如何构建不能实例化的对象?)也不能有字段(因为这隐含了某些内部的实现方式)。接口定义也不允许包含运算符重载,但设计语言时总是会讨论这个可能性,未来可能会改变。
在接口定义中还不允许声明成员的修饰符。接口成员总是隐式为public,不能声明为virtual。如果需要,就应由实现的类来声明,因此最好实现类来声明访问修饰符,就像本节的代码那样。
例如,IDisposable。如果类希望声明为公有类型,以便它实现方法Dispose(),该类就必须实现IDisposable。在C#中,这表示该类派生自IDisposable类。
class SomeClass: IDisposable { // This class MUST contain an implementation of the // IDisposable.Dispose() method, otherwise // you get a compilation error. public void Dispose() { // implementation of Dispose() method } // rest of class }
在这个例子中,如果SomeClass派生自IDisposable类,但不包含与IDisposable类中签名相同的Dispose()实现代码,就会得到一个编译错误,因为该类破坏了实现IDisposable的一致协定。当然,编译器允许类有一个不派生自IDisposable类的Dispose()方法。问题是其他代码无法识别出SomeClass类,来支持IDisposable特性。
注意:IDisposable是一个相当简单的接口,它只定义了一个方法。大多数接口都包含许多成员。IDisposable的正确实现代码没有这么简单,参见第5章。
4.5.1 定义和实现接口
下面开发一个遵循接口继承规范的小例子来说明如何定义和使用接口。这个例子建立在银行账户的基础上。假定编写代码,最终允许在银行账户之间进行计算机转账业务。许多公司可以实现银行账户,但它们一致认为,表示银行账户的所有类都实现接口IBankAccount。该接口包含一个用于存取款的方法和一个返回余额的属性。这个接口还允许外部代码识别由不同银行账户实现的各种银行账户类。我们的目的是允许银行账户彼此通信,以便在账户之间进行转账业务,但还没有介绍这个功能。
为了使例子简单一些,我们把本例子的所有代码都放在同一个源文件中,但实际上不同的银行账户类不仅会编译到不同的程序集中,而且这些程序集位于不同银行的不同机器上。但这些内容对于我们的目的过于复杂了。为了保留一定的真实性,我们为不同的公司定义不同的名称空间。
首先,需要定义IBankAccount接口(代码文件UsingInterfaces/IBankAccount.cs):
namespace Wrox.ProCSharp { public interface IBankAccount { void PayIn(decimal amount); bool Withdraw(decimal amount); decimal Balance { get; } } }
注意,接口的名称为IBankAccount。接口名称通常以字母I开头,以便知道这是一个接口。
注意:如第2章所述,在大多数情况下,.NET的用法规则不鼓励采用所谓的Hungarian表示法,在名称的前面加一个字母,表示所定义对象的类型。接口是少数几个推荐使用Hungarian表示法的例外之一。
现在可以编写表示银行账户的类了。这些类不必彼此相关,它们可以是完全不同的类。但它们都表示银行账户,因为它们都实现了IBankAccount接口。
下面是第一个类,一个由Royal Bank of Venus运行的存款账户(代码文件UsingInterfaces/VenusBank.cs ):
namespace Wrox.ProCSharp.VenusBank { public class SaverAccount: IBankAccount { private decimal _balance; public void PayIn(decimal amount) => _balance += amount; public bool Withdraw(decimal amount) { if (_balance >= amount) { _balance -= amount; return true; } WriteLine("Withdrawal attempt failed."); return false; } public decimal Balance => _balance; public override string ToString() => $"Venus Bank Saver: Balance = {_balance,6:C}"; } }
实现这个类的代码的作用一目了然。其中包含一个私有字段balance,当存款或取款时就调整这个字段。如果因为账户中的金额不足而取款失败,就会显示一条错误消息。还要注意,因为我们要使代码尽可能简单,所以不实现额外的属性,如账户持有人的姓名。在现实生活中,这是最基本的信息,但对于本例不必要这么复杂。
在这段代码中,唯一有趣的一行是类的声明:
public class SaverAccount: IBankAccount
SaverAccount派生自一个接口IBankAccount,我们没有明确指出任何其他基类(当然这表示SaverAccount直接派生自System.Object)。另外,从接口中派生完全独立于从类中派生。
SaverAccount派生自IBankAccount,表示它获得了IBankAccount的所有成员,但接口实际上并不实现其方法,所以SaverAccount必须提供这些方法的所有实现代码。如果缺少实现代码,编译器就会产生错误。接口仅表示其成员的存在性,类负责确定这些成员是虚拟还是抽象的(但只有在类本身是抽象的,这些函数才能是抽象的)。在本例中,接口的任何函数不必是虚拟的。
为了说明不同的类如何实现相同的接口,下面假定Planetary Bank of Jupiter还实现一个类GoldAccount来表示其银行账户中的一个(代码文件UsingInterfaces/JupiterBank.cs ):
namespace Wrox.ProCSharp.JupiterBank { public class GoldAccount: IBankAccount { // etc } }
这里没有列出GoldAccount类的细节,因为在本例中它基本上与SaverAccount的实现代码相同。GoldAccount与SaverAccount没有关系,它们只是碰巧实现相同的接口而已。
有了自己的类后,就可以测试它们了。首先需要一些using语句:
using Wrox.ProCSharp; using Wrox.ProCSharp.VenusBank; using Wrox.ProCSharp.JupiterBank; using static System.Console;
然后需要一个Main()方法(代码文件UsingInterfaces/Program.cs ):
namespace Wrox.ProCSharp { class Program { static void Main() { IBankAccount venusAccount = new SaverAccount(); IBankAccount jupiterAccount = new GoldAccount(); venusAccount.PayIn(200); venusAccount.Withdraw(100); WriteLine(venusAccount.ToString()); jupiterAccount.PayIn(500); jupiterAccount.Withdraw(600); jupiterAccount.Withdraw(100); WriteLine(jupiterAccount.ToString()); } } }
这段代码的执行结果如下:
> BankAccounts Venus Bank Saver: Balance = $100.00 Withdrawal attempt failed. Jupiter Bank Saver: Balance = $400.00
在这段代码中,要点是把两个引用变量声明为IBankAccount引用的方式。这表示它们可以指向实现这个接口的任何类的任何实例。但我们只能通过这些引用调用接口的一部分方法——如果要调用由类实现的但不在接口中的方法,就需要把引用强制转换为合适的类型。在这段代码中,我们调用了ToString()(不是IBankAccount实现的),但没有进行任何显式的强制转换,这只是因为ToString()是一个System.Object()方法,因此C#编译器知道任何类都支持这个方法(换言之,从任何接口到System.Object的数据类型强制转换是隐式的)。第8章将介绍强制转换的语法。
接口引用完全可以看成类引用——但接口引用的强大之处在于,它可以引用任何实现该接口的类。例如,我们可以构造接口数组,其中数组的每个元素都是不同的类:
IBankAccount[] accounts = new IBankAccount[2]; accounts[0] = new SaverAccount(); accounts[1] = new GoldAccount();
但注意,如果编写了如下代码,就会生成一个编译器错误:
accounts[1] = new SomeOtherClass(); // SomeOtherClass does NOT implement // IBankAccount: WRONG! !
这会导致一个如下所示的编译错误:
Cannot implicitly convert type 'Wrox.ProCSharp. SomeOtherClass' to 'Wrox.ProCSharp.IBankAccount'
4.5.2 派生的接口
接口可以彼此继承,其方式与类的继承方式相同。下面通过定义一个新的ITransferBankAccount接口来说明这个概念,该接口的功能与IBankAccount相同,只是又定义了一个方法,把资金直接转到另一个账户上(代码文件UsingInterfaces/ ITransferBankAccount ):
namespace Wrox.ProCSharp { public interface ITransferBankAccount: IBankAccount { bool TransferTo(IBankAccount destination, decimal amount); } }
因为ITransferBankAccount派生自IBankAccount,所以它拥有IBankAccount的所有成员和它自己的成员。这表示实现(派生自)ITransferBankAccount的任何类都必须实现IBankAccount的所有方法和在ITransferBankAccount中定义的新方法TransferTo()。没有实现所有这些方法就会产生一个编译错误。
注意,TransferTo()方法对于目标账户使用了IBankAccount接口引用。这说明了接口的用途:在实现并调用这个方法时,不必知道转账的对象类型,只需要知道该对象实现IBankAccount即可。
下面说明ITransferBankAccount:假定Planetary Bank of Jupiter还提供了一个当前账户。CurrentAccount类的大多数实现代码与SaverAccount和GoldAccount的实现代码相同(这仅是为了使例子更简单,一般是不会这样的),所以在下面的代码中,我们仅突出显示了不同的地方(代码文件UsingInterfaces/ JupiterBank.cs ):
public class CurrentAccount: ITransferBankAccount { private decimal _balance; public void PayIn(decimal amount) => _balance += amount; public bool Withdraw(decimal amount) { if (_balance >= amount) { _balance -= amount; return true; } WriteLine("Withdrawal attempt failed."); return false; } public decimal Balance => _balance; public bool TransferTo(IBankAccount destination, decimal amount) { bool result = Withdraw(amount); if (result) { destination.PayIn(amount); } return result; } public override string ToString() => $"Jupiter Bank Current Account: Balance = {_balance,6:C}"; }
可以用下面的代码验证该类:
static void Main() { IBankAccount venusAccount = new SaverAccount(); ITransferBankAccount jupiterAccount = new CurrentAccount(); venusAccount.PayIn(200); jupiterAccount.PayIn(500); jupiterAccount.TransferTo(venusAccount, 100); WriteLine(venusAccount.ToString()); WriteLine(jupiterAccount.ToString()); }
这段代码的结果如下所示,可以验证,其中说明了正确的转账金额:
> CurrentAccount Venus Bank Saver: Balance = $300.00 Jupiter Bank Current Account: Balance = $400.00