如前所述,如果一个类派生于一个接口,它就会执行某些函数。并不是所有的面向对象语言都支持接口,所以本节将详细介绍C#接口的实现。
注意:
熟悉COM的开发人员应注意,尽管在概念上C#接口类似于COM接口,但它们是不同的,底层的结构不同,例如,C#接口并不派生于IUnknown。C#接口根据.NET函数提供了一个契约。与COM接口不同,C#接口不代表任何类型的二进制标准。
下面列出Microsoft预定义的一个接口System.IDisposable的完整定义。IDisposable包含一个方法Dispose(),该方法由类执行,用于清理代码:
public interface IDisposable
{
void Dispose();
}
上面的代码说明,声明接口在语法上与声明抽象类完全相同,但不允许提供接口中任何成员的执行方式。一般情况下,接口中只能包含方法、属性、索引器和事件的声明。
不能实例化接口,它只能包含其成员的签名。接口不能有构造函数(如何构建不能实例化的对象?)或字段(因为这隐含了某些内部的执行方式)。接口定义也不允许包含运算符重载,但这不是因为声明它们在原则上有什么问题,而是因为接口通常是公共契约,包含运算符重载会引起一些与其他.NET语言不兼容的问题,例如与VB.NET的不兼容,因为VB.NET不支持运算符重载。
在接口定义中还不允许声明成员上的修饰符。接口成员总是公共的,不能声明为虚拟或静态。如果需要,就应由执行的类来声明,因此最好通过执行的类来声明访问修饰符,就像上面的代码那样。
例如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是一个相当简单的接口,它只定义了一个方法。大多数接口都包含许多成员。
接口的另一个例子是C#中的foreach循环。实际上,foreach循环的内部工作方式是查询对象,看看它是否实现了System.Collections.IEnumerable接口。如果是,C#编译器就插入IL代码,使用这个接口上的方法迭代集合中的成员,否则,foreach就会引发一个异常。第10章将详细介绍IEnumerable接口。但应注意,IEnumerable和IDisposable在某种程度上都是有点特殊的接口,因为它们都可以由C#编译器识别,在C#编译器生成的代码中会考虑它们。显然,自己定义的接口就没有这个特权。
下面开发一个遵循接口继承规范的小例子来说明如何定义和使用接口。这个例子建立在银行账户的基础上。假定编写代码,最终允许在银行账户之间进行计算机转账业务。许多公司可以实现银行账户,但它们都是彼此赞同表示银行账户的所有类都实现接口IBankAccount。该接口包含一个用于存取款的方法和一个返回余额的属性。这个接口还允许外部代码识别由不同银行账户执行的各种银行账户类。我们的目的是允许银行账户彼此通信,以便在账户之间进行转账业务,但还没有介绍这个功能。
为了使例子简单一些,我们把例子的所有代码都放在同一个源文件中,但实际上不同的银行账户类会编译到不同的程序集中,而这些程序集位于不同银行的不同机器上。第37章在讨论远程通信时,将介绍位于不同机器上的.NET程序集如何通信。但那些内容对于这里的例子来说过于复杂了。为了保留一定的真实性,我们为不同的公司定义不同的命名空间。
首先,需要定义IBank接口:
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运行的存款账户:
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;
}
Console.WriteLine("Withdrawal attempt failed.");
return false;
}
public decimal Balance
{
get
{
return balance;
}
}
public override string ToString()
{
return String.Format("Venus Bank Saver: Balance = {0,6:C}", balance);
}
}
}
这个类的实现代码的作用一目了然。其中包含一个私有字段balance,当存款或取款时就调整这个字段。如果因为账户中的金额不足而取款失败,就会显示一个错误消息。还要注意,因为我们要使代码尽可能简单,所以不实现额外的属性,例如账户持有人的姓名。在现实生活中,这是最基本的信息,但对于本例来说,这是不必要的。
在这段代码中,唯一有趣的是类的声明:
public class SaverAccount : IBankAccount
SaverAccount派生于一个接口IbankAccount,我们没有明确指出任何其他基类(当然这表示SaverAccount直接派生于System.Object)。另外,从接口中派生完全独立于从类中派生。
SaverAccount派生于IBankAccount,表示它获得了IBankAccount的所有成员,但接口并不实际实现其方法,所以SaverAccount必须提供这些方法的所有实现代码。如果没有提供实现代码,编译器就会产生错误。接口仅表示其成员的存在性,类负责确定这些成员是虚拟还是抽象的(但只有在类本身是抽象的,这些成员才能是抽象的)。在本例中,接口方法不必是虚拟的。
为了说明不同的类如何实现相同的接口,下面假定Planetary Bank of Jupiter还实现一个类Gold Account来表示其银行账户:
namespace Wrox.ProCSharp.JupiterBank
{
public class GoldAccount : IBankAccount
{
// etc
}
}
这里没有列出GoldAccount类的细节,因为在本例中它基本上与SaverAccount的实现代码相同。GoldAccount与VenusAccount没有关系,它们只是碰巧实现相同的接口而已。
有了自己的类后,就可以测试它们了。首先需要一些using语句:
using System;
using Wrox.ProCSharp;
using Wrox.ProCSharp.VenusBank;
using Wrox.ProCSharp.JupiterBank;
然后需要一个Main()方法:
namespace Wrox.ProCSharp
{
class MainEntryPoint
{
static void Main()
{
IBankAccount venusAccount = new SaverAccount();
IBankAccount jupiterAccount = new GoldAccount();
venusAccount.PayIn(200);
venusAccount.Withdraw(100);
Console.WriteLine(venusAccount.ToString());
jupiterAccount.PayIn(500);
jupiterAccount.Withdraw(600);
jupiterAccount.Withdraw(100);
Console.WriteLine(jupiterAccount.ToString());
}
}
}
这段代码(如果下载本例子,它在BankAccounts.cs文件中)的执行结果如下:
C:>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的数据类型转换是隐式的)。第6章将介绍强制转换的语法。
接口引用完全可以看做是类引用—— 但接口引用的强大之处在于,它可以引用任何实现该接口的类。例如,我们可以构造接口数组,其中的每个元素都是不同的类:
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'
接口可以彼此继承,其方式与类的继承相同。下面通过定义一个新接口ITransferBank Account来说明这个概念,该接口的功能与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;
}
Console.WriteLine("Withdrawal attempt failed.");
return false;
}
public decimal Balance
{
get
{
return balance;
}
}
public bool TransferTo(IBankAccount destination, decimal amount)
{
bool result;
if ((result = Withdraw(amount)) == true)
destination.PayIn(amount);
return result;
}
public override string ToString()
{
return String.Format("Jupiter Bank Current Account: Balance = {0,6:C}",
balance);
}
}
可以用下面的代码验证该类:
static void Main()
{
IBankAccount venusAccount = new SaverAccount();
ITransferBankAccount jupiterAccount = new CurrentAccount();
venusAccount.PayIn(200);
jupiterAccount.PayIn(500);
jupiterAccount.TransferTo(venusAccount, 100);
Console.WriteLine(venusAccount.ToString());
Console.WriteLine(jupiterAccount.ToString());
}
这段代码(CurrentAccount.cs)的结果如下所示,其中显示转账后正确的资金数:
C:>CurrentAccount
Venus Bank Saver: Balance = £300.00
Jupiter Bank Current Account: Balance = £400.00