c#高级编程 接口 (四)
前言
如果一个类派生自一个接口,声明这个类就会实现某些函数。
下面列出Microsoft预定义的一个接口System.IDisposable的完整定义。IDisposable包含一个方法Dispose(),该方法由类实现,用于清理代码:
public interface IDisposable { void Dispose(); }
上面的代码说明,声明接口在语法上与声明抽象类完全相同,但不允许提供接口中任何成员的实现方式。一般情况下,接口只能包含方法、属性、索引器和事件的声明。
比较接口和抽象类:抽象类可以有实现代码或没有实现代码的抽象成员。然而,接口不能有任何实现代码;它是纯粹抽象的。因为接口的成员总是抽象的,所以接口不需要abstract关键字。
类似于抽象类,永远不能实例化接口,它只能包含其成员的签名。此外,可以声明接口类型的变量。
接口既不能有构造函数也不能有字段。接口定义也不允许包含运算符重载,但设计语言时总是会讨论这个可能性,未来可能会改变。
在接口定义中还不允许声明成员的修饰符。接口成员总是隐式为public,不能声明为virtual。如果需要,就应由实现的类来声明,因此最好实现类来声明访问修饰符,就像本文的代码那样。
例如,IDisposable。如果类希望声明为公有类型,以便它实现方法Dispose(),该类就必须实现IDisposable。在C#中,这表示该类派生自IDisposable类。
//此类必须包含 IDisposable.Dispose()方法,否则你会得到一个编译错误。 class SomeClass: IDisposable { public void Dispose() { .... } }
在这个例子中,如果SomeClass派生自IDisposable类,但不包含与IDisposable类中签名相同的Dispose()实现代码,就会得到一个编译错误,因为该类破坏了实现IDisposable的一致协定。当然,编译器允许类有一个不派生自IDisposable类的Dispose()方法。问题是其他代码无法识别出SomeClass类,来支持IDisposable特性。
注意: IDisposable是一个相当简单的接口,它只定义了一个方法。大多数接口都包含许多成员。
1、定义和实现接口
下面开发一个遵循接口继承规范的例子来说明如何定义和使用接口。例子建立在银行账户的基础上。假定编写代码,最终允许在银行账户之间进行计算机转账业务。
功能
(1)许多公司可以实现银行账户,但它们一致认为,表示银行账户的所有类都实现接口IBankAccount。
(2)该接口包含一个用于存取款的方法和一个返回余额的属性。
(3)这个接口还允许外部代码识别由不同银行账户实现的各种银行账户类。
目标
允许银行账户彼此通信,以便在账户之间进行转账业务。
实现
为了使例子简单一些,所有代码都放在同一个源文件中,实际上不同的银行账户类不仅会编译到不同的程序集中,而且这些程序集位于不同银行的不同机器上。这些内容对于我们的目的过于复杂了,为了保留一定的真实性,我们为不同的公司定义不同的名称空间。
1.1、定义IBankAccount接口
//命名空间 namespace Wrox.ProCSharp { //接口 public interface IBankAccount { void PayIn(decimal amount); bool Withdraw(decimal amount); decimal Balance { get; } } }
注意,接口的名称为IBankAccount。接口名称通常以字母I开头,以便知道这是一个接口。
接口是少数几个推荐使用Hungarian表示法的例外之一。
1.2、编写表示银行账户的类
这些类不必彼此相关,它们可以是完全不同的类。但它们都表示银行账户,因为它们都实现了IBankAccount接口。
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("取款尝试失败。"); return false; } public decimal Balance => _balance; public override string ToString() => $"Venus Bank Saver:余额 = {_balance,6:C}"; } }
实现这个类的代码的作用一目了然。其中包含一个私有字段balance,当存款或取款时就调整这个字段。如果因为账户中的金额不足而取款失败,就会显示一条错误消息。(此处忽略其他相关信息)
SaverAccount派生自IBankAccount,表示它获得了IBankAccount的所有成员,但接口实际上并不实现其方法,所以SaverAccount必须提供这些方法的所有实现代码。
接口仅表示其成员的存在性,类负责确定这些成员是虚拟还是抽象的(但只有在类本身是抽象的,这些函数才能是抽象的)。
为了说明不同的类如何实现相同的接口,下面还实现一个类GoldAccount来表示其银行账户中的一个:
namespace Wrox.ProCSharp.JupiterBank { public class GoldAccount: IBankAccount { private decimal _balance;public void PayIn(decimal amount) => _balance += amount; public bool Withdraw(decimal amount) { if (_balance >= amount) { _balance -= amount; return true; } WriteLine("取款尝试失败。"); return false; } public decimal Balance => _balance; public override string ToString() => $"JupiterBank Saver:余额 = {_balance,6:C}"; } }
有了自己的类后,就可以测试它们了。首先需要一些using语句:
using Wrox.ProCSharp; using Wrox.ProCSharp.VenusBank; using Wrox.ProCSharp.JupiterBank; using static System.Console;
然后需要一个Main()方法:
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()); } } }
这段代码的执行结果如下:
Venus Bank Saver: 余额 = $100.00 取款尝试失败。 Jupiter Bank Saver: 余额 = $400.00
在这段代码中,要点是把两个引用变量声明为IBankAccount引用的方式。这表示它们可以指向实现这个接口的任何类的任何实例。但我们只能通过这些引用调用接口的一部分方法——如果要调用由类实现的但不在接口中的方法,就需要把引用强制转换为合适的类型。
接口引用完全可以看成类引用——但接口引用的强大之处在于,它可以引用任何实现该接口的类。例如,我们可以构造接口数组,其中数组的每个元素都是不同的类:
IBankAccount[] accounts = new IBankAccount[2]; accounts[0] = new SaverAccount(); accounts[1] = new GoldAccount();
2、派生接口
接口可以彼此继承,其方式与类的继承方式相同。下面通过定义一个新的ITransferBankAccount接口来说明这个概念,该接口的功能与IBankAccount相同,只是又定义了一个方法,把资金直接转到另一个账户上:
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的实现代码相同:
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; } //重构tostring()方法 public override string ToString() => $"Jupiter Bank Current Account: 余额 = {_balance,6:C}"; }
这段代码的结果如下所示,可以验证,其中说明了正确的转账金额:
Venus Bank Saver: 余额= $300.00 Jupiter Bank Current Account: 余额 = $400.00
源代码:
拓展:is和as运算符