1.0继承的类型
1.0.1 实现继承和接口继承
在面向对象的编程中,有两种截然不同的继承类型:实现继承和接口继承。
● 实现继承:表示一个类型派生于一个基类型,拥有该基类型的所有成员字段和函数。在实现继承中,派生类型的每个函数采用基类型的实现代码,除非在派生类型的定义中指定重写该函数的实现代码。在需要给现有的类型添加功能,或许多相关的类型共享一组重要的公共功能时,这种类型的继承是非常有效的。
● 接口继承:表示一个类型只继承了函数的签名,没有继承任何实现代码。在需要指定该类型具有某些可用的特性时,最好使用这种类型的继承。例如,某些类型可以指定从接口System.IDisposable(详见第12章)中派生,从而提供一种清理资源的方法Dispose()。由于某种类型清理资源的方式可能与另一种类型的完全不同,所以定义通用的实现代码是没有意义的,此时就适合使用接口继承。接口继承常常被看做提供了一种契约:让类型派生于接口,来保证为客户提供某个功能。
1.0.2 多重继承
一些语言如C++支持所谓的“多重继承”,即一个类派生于多个类。使用多重继承的优点是有争议的:一方面,毫无疑问,可以使用多重继承编写非常复杂、但很紧凑的代码,如C++ ATL库。另一方面,使用多重实现继承的代码常常很难理解和调试(这也可以从C++ ATL库中看出)。如前所述,使健壮代码的编写容易一些,是开发C#的重要设计目标。因此,C#不支持多重实现继承。而C#又允许类型派生于多个接口。这说明,C#类可以派生于另一个类和任意多个接口。更准确地说,因为System.Object是一个公共的基类,所以每个C#类(除了Object类之外)都有一个基类,还可以有任意多个基接口。
4.1.3 结构和类
第3章区分了结构(值类型)和类(引用类型)。使用结构的一个限制是结构不支持继承,但每个结构都自动派生于System.ValueType。实际上还应更仔细一些:不能建立结构的类型层次,但结构可以实现接口。换言之,结构并不支持实现继承,但支持接口继承。事实上,定义结构和类可以总结为:
● 结构总是派生于System.ValueType,它们还可以派生于任意多个接口。
● 类总是派生于用户选择的另一个类,它们还可以派生于任意多个接口。
1.1 实现继承
C++程序员习惯于使用公共和私有继承的概念,要注意C#不支持私有继承,因此基类名上没有public或private限定符。支持私有继承会大大增加语言的复杂性,实际上私有继承在C++中也很少使用。
如果在类定义中没有指定基类,C#编译器就假定System.Object是基类。因此下面的两段代码生成相同的结果:
class MyClass : Object //derives from System.Object
{
}
和
class MyClass //derives from System.Object
{
}
第二种形式比较常用,因为它较简单。
C#支持object关键字,它用作System.Object类的假名,所以也可以编写下面的代码:
class MyClass : object //derives from System.Object
{
}
如果要引用Object类,可以使用object关键字,智能编辑器(如Visual Studio)会识别它,因此便于编辑代码。
1.1.1 虚方法
把一个基类函数声明为virtual,该函数就可以在派生类中重写了:
class MyBaseClass
{
public virtual string VirtualMethod()
{
return "This method is virtual and defined in MyBaseClass";
}
}
也可以把属性声明为virtual。对于虚属性或重写属性,语法与非虚属性是相同的,但要在定义中加上关键字virtual,其语法如下所示:
public virtual string ForeName
{
get { return foreName; }
set { foreName = value; }
}
private string foreName;
C#中虚函数的概念与标准OOP概念相同:可以在派生类中重写虚函数。在调用方法时,会调用对象类型的合适方法。在C#中,函数在默认情况下不是虚拟的,但(除了构造函数以外)可以显式地声明为virtual。这遵循C++的方式,即从性能的角度来看,除非显式指定,否则函数就不是虚拟的。而在Java中,所有的函数都是虚拟的。但C#的语法与C++的语法不同,因为C#要求在派生类的函数重写另一个函数时,要使用override关键字显式声明:
class MyDerivedClass : MyBaseClass
{
public override string VirtualMethod()
{
return "This method is an override defined in MyDerivedClass";
}
}
方法重写的语法避免了C++中很容易发生的潜在运行错误:当派生类的方法签名无意中与基类版本略有差别时,派生类方法就不能重写基类方法了。在C#中,这会出现一个编译错误,因为编译器会认为函数已标记为override,但没有重写它的基类方法。
成员字段和静态函数都不能声明为virtual,因为这个概念只对类中的实例函数成员有意义。
1.1.2 隐藏方法
如果签名相同的方法在基类和派生类中都进行了声明,但该方法没有声明为virtual 和 override,派生类方法就会隐藏基类方法。
在大多数情况下,是要重写方法,而不是隐藏方法,因为隐藏方法会存在为给定类的实例调用错误方法的危险。但是,如下面的例子所示,C#语法可以确保开发人员在编译时收到这个潜在错误的警告,使隐藏方法更加安全。这也是类库开发人员得到的版本方面的好处。
假定有人编写了类HisBaseClass:
class HisBaseClass
{
// various members
}
在将来的某一刻,要编写一个派生类,给HisBaseClass添加某个功能,特别是要添加一个目前基类中没有的方法MyGroovyMethod():
class MyDerivedClass: HisBaseClass
{
public int MyGroovyMethod()
{
// some groovy implementation
return 0;
}
}
一年后,基类的编写者决定扩展基类的功能。为了保持一致,他也添加了一个名为MyGroovyMethod()的方法,该方法的名称和签名与前面添加的方法相同,但并不完成相同的工作。在使用基类的新方法编译代码时,程序在应该调用哪个方法上就会有潜在的冲突。这在C#中完全合法,但因为我们的MyGroovyMethod()与基类的MyGroovyMethod()不相关,运行这段代码的结果就可能不是我们希望的结果。C#已经为此设计了一种方式,可以很好地处理这种情况。
首先,系统会发出警告。在C#中,应使用new关键字声明我们要隐藏一个方法,如下所示:
class MyDerivedClass : HisBaseClass
{
public new int MyGroovyMethod()
{
// some groovy implementation
return 0;
}
}
但是,我们的MyGroovyMethod()没有声明为new,所以编译器会认为它隐藏了基类的方法,但没有显式声明,因此发出一个警告(这也适用于把MyGroovyMethod()声明为 virtual)。如果愿意,可以给我们的方法重命名。这么做,是最好的情形,因为这会避免许多冲突。但是,如果觉得重命名方法是不可能的(例如,已经为其他公司把软件发布为一个库,所以无法修改方法的名称),则所有的已有客户机代码仍能正确运行,选择我们的MyGroovyMethod()。这是因为访问这个方法的已有代码必须通过对MyDerivedClass(或进一步派生的类)的引用进行选择。
已有的代码不能通过对HisBaseClass的引用访问这个方法,因为在对HisBaseClass的早期版本进行编译时,会产生一个编译错误。这个问题只会发生在将来编写的客户机代码上。C#会发出一个警告,告诉用户在将来的代码中可能会出问题——用户应注意这个警告,不要试图在将来的代码中通过对HisBaseClass的引用调用MyGroovyMethod()方法,但所有已有的代码仍会正常工作。这是比较微妙的,但很好地说明了C#如何处理类的不同版本。
4.2.3 调用函数的基类版本
C#有一种特殊的语法用于从派生类中调用方法的基类版本:base.<MethodName>()。
注意,可以使用base.<MethodName>()语法调用基类中的任何方法,不必在同一个方法的重载中调用它。
4.2.4 抽象类和抽象函数
C#允许把类和函数声明为abstract,抽象类不能实例化,而抽象函数没有执行代码,必须在非抽象的派生类中重写。显然,抽象函数也是虚拟的(但也不需要提供virtual关键字,实际上,如果提供了该关键字,就会产生一个语法错误)。如果类包含抽象函数,该类将也是抽象的,也必须声明为抽象的:
abstract class Building
{
public abstract decimal CalculateHeatingCost(); // abstract method
}
C++开发人员要注意C#中的一些语法区别。C#不能采用=0语法来声明抽象函数。在C#中,这个语法有误导作用,因为可以在类声明的成员字段上使用=<value>,提供初始值:
abstract class Building
{
private bool damaged = false; // field
public abstract decimal CalculateHeatingCost(); // abstract method
}
注意:
C++开发人员还要注意术语上的细微差别:在C++中,抽象函数常常描述为纯虚函数,而在C#中,仅使用抽象这个术语。
4.2.5 密封类和密封方法
C#允许把类和方法声明为sealed。对于类来说,这表示不能继承该类;对于方法来说,这表示不能重写该方法。
sealed class FinalClass
{
// etc
}
class DerivedClass : FinalClass // wrong. Will give compilation error
{
// etc
}
注意:
Java开发人员可以把C#中的sealed当作Java中的final。
在把类或方法标记为sealed时,最可能的情形是:如果要对库、类或自己编写的其他类进行操作,则重写某些功能会导致错误。也可以因商业原因把类或方法标记为sealed,以防第三方以违反注册协议的方式扩展该类。但一般情况下,在把类或方法标记为sealed时要小心,因为这么做会严重限制它的使用。即使不希望它能继承一个类或重写类的某个成员,仍有可能在将来的某个时刻,有人会遇到我们没有预料到的情形。.NET基类库大量使用了密封类,使希望从这些类中派生出自己的类的第三方开发人员无法访问这些类。例如string就是一个密封类。
把方法声明为sealed也可以实现类似的目的,但很少这么做。
class MyClass
{
public sealed override void FinalMethod()
{
// etc.
}
}
class DerivedClass : MyClass
{
public override void FinalMethod() // wrong. Will give compilation error
{
}
}
要在方法或属性上使用sealed关键字,必须先在基类上把它声明为重写。如果基类上不希望有重写的方法或属性,就不要把它声明为virtual。
4.2.6 派生类的构造函数
第3章介绍了单个类的构造函数是如何工作的。这样,就产生了一个有趣的问题,在开始为层次结构中的类(这个类继承了其他类,也可能有定制的构造函数)定义自己的构造函数时,会发生什么情况?
假定没有为类定义任何显式的构造函数,这样编译器就会为所有的类提供默认的构造函数,在后台会进行许多操作,编译器可以很好地解决层次结构中的所有问题,每个类中的每个字段都会初始化为默认值。但在添加了一个我们自己的构造函数后,就要通过派生类的层次结构高效地控制构造过程,因此必须确保构造过程顺利进行,不要出现不能按照层次结构进行构造的问题。
为什么派生类会有某些特殊的问题?原因是在创建派生类的实例时,实际上会有多个构造函数起作用。要实例化的类的构造函数本身不能初始化类,还必须调用基类中的构造函数。这就是为什么要通过层次结构进行构造的原因。
为了说明为什么必须调用基类的构造函数,下面是手机公司MortimerPhones开发的一个例子。这个例子包含一个抽象类GenericCustomer,它表示顾客。还有一个(非抽象)类Nevermore60Customer,它表示采用特定付费方式(称为Nevermore60付费方式)的顾客。所有的顾客都有一个名字,由一个私有字段表示。在Nevermore60付费方式中,顾客前几分钟的电话费比较高,需要一个字段highCostMinutesUsed,它详细说明了每个顾客该如何支付这些较高的电话费。抽象类GenericCustomer的定义如下所示:
abstract class GenericCustomer
{
private string name;
// lots of other methods etc.
}
class Nevermore60Customer : GenericCustomer
{
private uint highCostMinutesUsed;
// other methods etc.
}
不要担心在这些类中执行的其他方法,因为这里仅考虑构造过程。如果下载了本章的示例代码,就会发现类的定义仅包含构造函数。
下面看看使用new运算符实例化Nevermore60Customer时,会发生什么情况:
GenericCustomer customer = new Nevermore60Customer();
显然,成员字段name和highCostMinutesUsed都必须在实例化customer时进行初始化。如果没有提供自己的构造函数,而是仅依赖默认的构造函数,name就会初始化为null引用,highCostMinutesUsed初始化为0。下面详细讨论其过程。
highCostMinutesUsed字段没有问题:编译器提供的默认Nevermore60Customer构造函数会把它初始化为0。
那么name呢?看看类定义,显然,Nevermore60Customer构造函数不能初始化这个值。字段name声明为private,这意味着派生的类不能访问它。默认的Nevermore60Customer构造函数甚至不知道存在这个字段。唯一知道这个字段的是GenericCustomer的其他成员,即如果对name进行初始化,就必须在GenericCustomer的某个构造函数中进行。无论类层次结构有多大,这种情况都会一直延续到最终的基类System.Object上。
理解了上面的问题后,就可以明白实例化派生类时会发生什么样的情况了。假定默认的构造函数在整个层次结构中使用:编译器首先找到它试图实例化的类的构造函数,在本例中是Nevermore60Customer,这个默认Nevermore60Customer构造函数首先要做的是为其直接基类GenericCustomer运行默认构造函数,然后GenericCustomer构造函数为其直接基类System.Object运行默认构造函数,System. Object没有任何基类,所以它的构造函数就执行,并把控制权返回给GenericCustomer构造函数。现在执行GenericCustomer构造函数,把name初始化为null,再把控制权返回给Nevermore60Customer构造函数,接着执行这个构造函数,把highCostMinutesUsed初始化为0,并退出。此时,Nevermore60Customer实例就已经成功地构造和初始化了。
构造函数的调用顺序是先调用System.Object,再按照层次结构由上向下进行,直到到达编译器要实例化的类为止。还要注意在这个过程中,每个构造函数都初始化它自己的类中的字段。这是它的一般工作方式,在开始添加自己的构造函数时,也应尽可能遵循这个规则。
注意构造函数的执行顺序。基类的构造函数总是最先调用。也就是说,派生类的构造函数可以在执行过程中调用它可以访问的基类方法、属性和其他成员,因为基类已经构造出来了,其字段也初始化了。如果派生类不喜欢初始化基类的方式,但要访问数据,就可以改变数据的初始值,但是,好的编程方式应尽可能避免这种情况,让基类构造函数来处理其字段。
1.2 修饰符
前面已经遇到许多所谓的修饰符,即应用于类型或成员的关键字。修饰符可以指定方法的可见性,例如public或private,还可以指定一项的本质,例如方法是virtual或abstract。C#有许多访问修饰符,下面讨论完整的修饰符列表。
1.2.1 可见性修饰符
表中的修饰符确定了是否允许其他代码访问某一项
修 饰 符 | 应 用 于 | 说 明 |
public | 所有的类型或成员 | 任何代码均可以访问该方法 |
protected | 类型和内嵌类型的所有成员 | 只有派生的类型能访问该方法 |
internal | 类型和内嵌类型的所有成员 | 只能在包含它的程序集中访问该方法 |
private | 所有的类型或成员 | 只能在它所属的类型中访问该方法 |
protected internal | 类型和内嵌类型的所有成员 | 只能在包含它的程序集和派生类型的代码中访问该方法 |
protected internal合并了protected和internal,但这是一种OR合并,而不是AND合并。protected internal成员在同一个程序集的任何代码中都可见,在派生类中也可见,甚至在其他程序集中也可见。
不能把类型定义为protected、private和protected internal,因为这些修饰符对于包含在命名空间中的类型来说是没有意义的。因此这些修饰符只能应用于成员。但是,可以用这些修饰符定义嵌套的类型(即包含在其他类型中的类型),因为在这种情况下,类型也具有成员的状态。下面的代码是合法的:
public class OuterClass
{
protected class InnerClass
{
//etc.
}
//etc.
}
如果有嵌套的类型,内部的类型总是可以访问外部类型的所有成员,所以在上面的代码中,InnerClass中的代码可以访问OuterClass的所有成员,甚至可以访问OuterClass的私有成员。
1.2.2 其他修饰符
表中的修饰符可以应用于类型的成员,而且有不同的用途。在应用于类型时,其中的几个修饰符也是有意义的。
修 饰 符 | 应 用 于 | 说 明 |
new | 函数成员 | 成员用相同的签名隐藏继承的成员 |
static | 所有的成员 | 成员不在类的具体实例上执行 |
virtual | 仅类和函数成员 | 成员可以由派生类重写 |
abstract | 仅函数成员 | 虚拟成员定义了成员的签名,但没有提供实现代码 |
override | 仅函数成员 | 成员重写了继承的虚拟或抽象成员 |
sealed | 类,方法和属性 | 密封类不能继承。对于属性和方法,成员重写了继承的虚拟成员,但继承该类的任何类都不能重写该成员。该修饰符必须与override一起使用 |
extern | 仅静态[DllImport]方法 | 成员在外部用另一种语言实现 |
1.3 接口
如前所述,如果一个类派生于一个接口,它就会执行某些函数。并不是所有的面向对象语言都支持接口,所以本节将详细介绍C#接口的实现。
注意:
熟悉COM的开发人员应注意,尽管在概念上C#接口类似于COM接口,但它们是不同的,底层的结构不同,例如,C#接口并不派生于IUnknown。C#接口根据.NET函数提供了一个契约。与COM接口不同,C#接口不代表任何类型的二进制标准。
下面列出Microsoft预定义的一个接口System.IDisposable的完整定义。IDisposable包含一个方法Dispose(),该方法由类执行,用于清理代码:
public interface IDisposable
{
void Dispose();
}
上面的代码说明,声明接口在语法上与声明抽象类完全相同,但不允许提供接口中任何成员的执行方式。一般情况下,接口中只能包含方法、属性、索引器和事件的声明。
不能实例化接口,它只能包含其成员的签名。接口不能有构造函数(如何构建不能实例化的对象?)或字段(因为这隐含了某些内部的执行方式)。接口定义也不允许包含运算符重载,但这不是因为声明它们在原则上有什么问题,而是因为接口通常是公共契约,包含运算符重载会引起一些与其他.NET语言不兼容的问题,例如与VB的不兼容,因为VB不支持运算符重载。
在接口定义中还不允许声明成员上的修饰符。接口成员总是公共的,不能声明为虚拟或静态。如果需要,就应由执行的类来声明,因此最好通过执行的类来声明访问修饰符。
接口的另一个例子是C#中的foreach循环。实际上,foreach循环的内部工作方式是查询对象,看看它是否实现了System.Collections.IEnumerable接口。如果是,C#编译器就插入IL代码,使用这个接口上的方法迭代集合中的成员,否则,foreach就会引发一个异常。第10章将详细介绍IEnumerable接口。但应注意,IEnumerable和IDisposable在某种程度上都是有点特殊的接口,因为它们都可以由C#编译器识别,在C#编译器生成的代码中会考虑它们。显然,自己定义的接口就没有这个特权。
1.3.1 定义和实现接口
下面开发一个遵循接口继承规范的小例子来说明如何定义和使用接口。这个例子建立在银行账户的基础上。假定编写代码,最终允许在银行账户之间进行计算机转账业务。许多公司可以实现银行账户,但它们都是彼此赞同表示银行账户的所有类都实现接口IBankAccount。该接口包含一个用于存取款的方法和一个返回余额的属性。这个接口还允许外部代码识别由不同银行账户执行的各种银行账户类。我们的目的是允许银行账户彼此通信,以便在账户之间进行转账业务,但还没有介绍这个功能。
为了使例子简单一些,我们把例子的所有代码都放在同一个源文件中,但实际上不同的银行账户类会编译到不同的程序集中,而这些程序集位于不同银行的不同机器上。但那些内容对于这里的例子来说过于复杂了。为了保留一定的真实性,我们为不同的公司定义不同的命名空间。
首先,需要定义IBank接口:
namespace Wrox.ProCSharp
{
public interface IBankAccount
{
void PayIn(decimal amount);
bool Withdraw(decimal amount);
decimal Balance
{
get;
}
}
}
注意,接口的名称为IBankAccount。接口名称传统上以字母I开头,以便知道这是一个接口。
现在可以编写表示银行账户的类了。这些类不必彼此相关,它们可以是完全不同的类。但它们都表示银行账户,因为它们都实现了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,当存款或取款时就调整这个字段。如果因为账户中的金额不足而取款失败,就会显示一个错误消息。还要注意,因为我们要使代码尽可能简单,所以不实现额外的属性,例如账户持有人的姓名。在现实生活中,这是最基本的信息,但对于本例来说,这是不必要的。
为了说明不同的类如何实现相同的接口,下面假定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();
1.3.2 派生的接口
接口可以彼此继承,其方式与类的继承相同。下面通过定义一个新接口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