2-C# 继承(随笔)
1. 继承的类型
1.1 实现继承和接口继承
- 实现继承:表示一个类型派生于一个基类型,它拥有该基类的所有成员字段和函数。在实现继承中,派生类采用基类型的每个函数的实现代码,除非针对某个功能进行重写。在需要给现有的类添加功能,或许多相关的类型共享一组重要的公共功能时,这种类型的继承非常有用。
- 接口继承:表示一个类型只继承了函数的签名,没有继承任何代码。在需要指定该类型具有某些可用的特性时,最好使用这种类型的继承。
C#不支持多重继承,但支持多重接口继承。
结构不支持实现继承,但支持接口继承。
2. 实现继承
2.1 虚方法
把一个基类函数声明为virtual,就可以在任何派生类中重写该函数。
class MyBaseClass
{
public virtual string VirtualMethod()
{
return "This method is virtual and defined in MyBaseClass.";
}
}
对于虚属性或重写属性,语法与非虚属性相同,但要在定义中添加关键字virtual.
public virtual string ForeName
{
get {return foreName;}
set {foreName = value;}
}
private string foreName;
c#中,函数在默认的情况下不是虚拟的,但(除了构造函数以外)可以显式的声明为virtual.在Java中,所有的函数都是虚拟的。
C#要求在派生类的函数重写另一个函数时,要使用override关键字显示声明:
class MyDerivedClass:MyBaseClass
{
public override string VirtualMethod()
{
return "This method is an override defined in MyDerivedClass";
}
}
成员字段和静态函数都不能声明为virtual,因为这个概念只对类中的实例函数成员有意义。
2.2 隐藏方法
如果签名相同的方法在基类和派生类中都进行了声明,但该方法没有分别声明为virtual和override,派生类方法就会隐藏基类方法。
在大多数情况下,是要重写方法,而不是隐藏方法,因为隐藏方法会造成对于给定类的实例调用错误方法的危险。如果开发人员未使用new显式声明,则C#会对这个潜在错误进行警告。
2.3 调用函数的基类版本
使用方法:base<MethodName>()
class CustomerAccount
{
public virtual decimal CalculatePrice()
{
return 0.0M;
}
}
class GoldAccount:CustomerAccount
{
public override decimal CalculatePrice()
{
return base.CaculatePrice() * 0.9M;
}
}
2.4 抽象类和抽象函数
C#允许把类和函数声明为abstract。抽象类不能实例化,而抽象函数不能直接实现,必须在非抽象的派生类中重写。抽象函数本身也是虚拟的。如果类包含抽象函数,则该类也是抽象的,也必须声明为抽象的。
abstract class Building
{
public abstract decimal CalculateHeatingCost();
}
2.5 密封类和密封方法
C#允许把类和方法声明为sealed。对于类,这表示不能继承该类。对于方法,则表示不能重写该方法。
适用环境:
- 重写某些功能会导致代码混乱
- 因商业原因把类或方法标记为sealed,以防第三方以违反授权协议的方式扩展该类。
.Net基类库大量使用了密封类,使希望从这些类中派生出自己的类的第三方开发人员无法访问这些类。
class MyClass : MyClassBase
{
public sealed override void FinalMethod()
{
//.....
}
}
class DerivedClass : MyClass
{
public override void FinalMethod() // Wrong
{
//....
}
}
要在方法或属性上使用sealed关键字,必须先从基类上把它声明为要重写的方法或属性。如果基类上不希望有重写的方法或属性,就不要把它声明为virtual.
2.6 派生类的构造函数
在创建派生类的实例时,实际上会有多个构造函数起作用。要实例化的类的构造函数本身不能初始化类,还必须调用基类中的构造函数,这就是为什么要通过层次结构进行构造的原因。
所有操作的最终结果是,构造函数的调用顺序是先调用System.Object,再按照层次结构由上向下进行,直到达到编译器要实例化的类为止。构造函数的执行顺序:最先调用的总是基类的构造函数。如果派生类不喜欢初始化基类的方式,那么只要它能访问基类的数据,就可以改变数据的初始值,但是,好的编程方式几乎总是应尽可能避免这种情况,让基类构造函数来处理其字段。
2.6.1 在层次结构中添加无参数的构造函数
public abstract class GenericCustomer
{
private string name;
public GenericCustomer()
: base()
{
name = "<no name>";
}
}
注意,在定制的构造函数中,执行GenericCustomer构造函数前,添加了一个对基类构造函数的调用。唯一的区别是,这次使用的是关键字base,而不是this,表示这是基类的构造函数,而不是要调用当前类的构造函数。在base关键字后面的圆括号中没有参数,这非常重要,因为没有给基类构造函数传送任何参数,所以编译器必须调用无参数的构造函数。其结果是编译器会插入要调用System.Object构造函数的代码,这正好与默认情况相同。
如果编译器没有在左花括号的前面找到对另一个构造函数的任何引用,它就回假定我们要调用基类的构造函数-这符合默认构造函数的工作方式。
基类的构造函数不能为private,否则会报错,因为派生类需要访问基类的构造函数。
2.6.2 在层次结构中添加带参数的构造函数
首先构造一个带参数的GenericCustomer构造函数,它仅在顾客提供其姓名时才实例化顾客:
abstract class GenericCustomer
{
private string name;
public GenericCustomer(string name)
{
this.name = name;
}
}
此时编译器在位GenericCustomer的派生类创建默认构造函数时,会产生编译错误,因为编译器为派生类生成的默认构造函数会试图调用一个无参数的GenericCustomer构造函数,但GenericCustomer没有这样的构造函数。因此,需要为派生类提供一个构造函数,来避免这个编译错误:
class Nevermore60Customer:GenericCustomer
{
private uint highCostMinutesUsed;
public Nevermore60Customer(string name) : base(name)
{
}
}
现在,Nevermore60Customer对象的实例化只有在提供了包含顾客姓名的字符串时才能进行。派生类Nevermore60Customer本身不能初始化name字段,因为它不能访问基类中的私有字段,但可以把顾客姓名传送给基类,以便GenericCustomer构造函数处理。
下面讨论如果要处理不同的重载构造函数和一个类的层次结构,会发生什么情况。最终,假定NeverMore60的顾客通过朋友联系到MortimerPhones,即该公司中有一个人是朋友,因此通过与朋友签约可以获得折扣。这表示在构造一个Nevermore60Customer室,还需要传递联系人的姓名。在现实生活中,构造函数必须利用该姓名去完成更加复杂的工作,如处理折扣等,但这里只是把联系人的姓名存储到另一个字段中。
class Nevermore60Customer:GenericCustomer
{
private string referrerName;
private uint highCostMinutesUsed;
public Nevermore60Customer(string name, string referrerName) : base(name)
{
this.referrerName = referrerName;
}
}
该构造函数将姓名作为参数,把它传递给基类构造函数进行处理。referrerName是一个需要声明的变量,这样构造函数才能在其主体中处理这个函数。但是,并不是所有Nevermore60Customer都有联系人,因此还需要有一个不需要此参数的构造函。实际上,我们指定如果没有联系人,referrerName字段就设置为“”,使用如下带一个参数的构造函数:
public Nevermore60Customer(string name) : this(name, "<None>")
{
}
这样就正确建立了所有的构造函数。
当执行如下代码时:
GenericCustomer customer = new Nevermore60Customer("Tom");
编译器实例化customer时的过程如下:
- 调用 public Nevermore69CUstomer(string name) 这个构造函数
- 将控制权传递给对应的带两个参数的构造函数,分别是“Tom”与“”
- 将控制权传递给GenericCusromer构造函数;
- 将控制权传递给System.Object默认构造函数;
- 执行System.Object构造函数
- 执行GenericCustomer构造函数,初始化name字段;
- 执行Nevermore60Customer带两个参数的构造函数,把referrerName初始化为“”
- 执行Nevemore60Customer带一个参数的构造函数,这个构造函数什么也没做
这个过程非常简洁,设计也很合理。每个构造函数都负责处理相应变量的初始化。在这个过程中,正确的实例化了类,以备使用。如果在为类编写自己的构造函数时遵循同样的规则,就会发现,即便是最复杂的类也可以顺利的初始化,并且不会出现任何问题。
3. 修饰符
修饰符,即用于修饰类型或成员的关键字。
3.1 可见性修饰符
修饰符 | 应用于 | 说明 |
---|---|---|
public | 所有类型和成员 | 任何代码均可以访问该项 |
protected | 类型和内嵌类型的所有成员 | 只有派生的类能够访问该项 |
internal | 所有类型和成员 | 只能够在包含它的程序集内访问该项 |
private | 类型和内嵌类型的所有成员 | 只能在它所属的类型中访问该项 |
protected internal | 类型和内嵌类型的所有成员 | 只能在包含它的程序集内和派生类的任何代码可以访问该项 |
不能把类型定义为protected,private,protected internal, 因为对于包含在名称空间中的类型来说,没有任何意义,因此这些修饰符只能应用于成员。但是,可以用这些修饰符定义嵌套的类型,即包含在其他类型中的类型,因为这时类型也具有成员的状态。
下面的代码是合法的:
public class OuterClass
{
private class InnerClass
{
//etc
}
}
注意:如果有嵌套的类型,则内部类型总是可以访问外部类型的所有成员。
3.2 其他修饰符
修饰符 | 应用于 | 说明 |
---|---|---|
new | 函数成员 | 成员用相同的签名隐藏继承的成员 |
static | 所有成员 | 成员不作用于类的具体实例 |
virtual | 仅函数成员 | 成员可以由派生类重写 |
abstract | 仅函数成员 | 成员只定义函数签名,不写实现代码 |
override | 仅函数成员 | 成员重写了继承的虚拟或抽象成员 |
sealed | 类、方法、属性 | 对于类,不能继承自密封类。对于属性和方法,成员(类型B)重写已继承的虚拟成员(类型A),且该修饰符必须与override一起使用(sealed override M(), 重写的时候),但任何派生类(类型C)中的任何成员都不能重写该成员, |
extern | 静态[DllImport]方法 | 成员在外部用另一种语言实现 |
4. 接口
如果一个类派生自一个接口,声明这个类就会实现某些函数。并不是所有的面向对象语言都支持接口。
public interface IDisposable
{
void Dispose();
}
声明接口在语法上与声明抽象类完全相同,但不允许提供接口中任何成员的实现方式。一般情况下,接口只包含方法、属性、索引器和事件的声明。
不能实例化接口,它只能包含其成员的签名。接口既不能有构造函数,也不能有字段,因为这隐含了某些内部的实现方式。接口定义也不允许包含运算符重载,尽管这不是因为声明他们在原则上有什么问题,而是因为接口通常是公共协定,包含运算符重载会引起一些与其他.Net语言不兼容的问题。
在接口定义中还不允许声明关于成员的修饰符。接口成员总是共有的,不能声明为虚拟或静态。如果需要,就应由实现的类声明。
class SomeClass:IDisposable
{
public void Dispose()
{
// implementtation of Dispose() method
}
}
如果上述类,不包含与IDisposable类中签名相同的Dispose()实现代码,就会得到一个编译错误,以为该类破坏了实现IDisposable的一致协定。
4.1 定义与实现接口
下面开发一个遵循接口继承规范的小例子来说明如何定义和使用接口。这个例子建立在银行账户的基础上。该接口包含一个用于存取款的方法和一个返回余额的属性。这个接口还允许外部代码识别由不同银行账户实现的各种银行账户类。
首先,需要定义IBankAccount接口:
namespace Wrox.ProCSharp
{
public interface IBankAccount
{
void PayIn(decimal amount); // 存储
void Withdraw(decimal amount); // 提取
decimal Balance {get;} //余额
}
}
现在可以编写表示银行的账户的类了,这些类不必彼此相关,他们可以是完全不同的类。但他们都表示银行账号,因为他们都实现了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 ture;
}
Console.WriteLine("Withdrawal attempt failed.");
return false;
}
public decimal Balance
{
get {return this.balance;}
}
public override string ToString()
{
return String.Format("Venus Bank Saver: Balance = {0, 6:C}", this.balance);
}
}
}
SaverAccount派生自一个接口IBankAccount, 表示获得了这个接口的所有成员,但接口实际上并不实现其方法,所以SaverAccount必须提供这些方法的所有实现代码。如果缺少实现代码,编译器就会产生错误。接口仅表示其成员的存在性,类负责确定这些成员是虚拟还是抽象的,但只有在类本身是抽象的,这些函数才能是抽象的。
为了说明不同的类如何实现相同的接口,下面假定Jupiter还实现一个类GoldAccount来表示其银行账户中的一个:
namespace Wrox.PrcCSharp.Jupiter
{
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 ture;
}
Console.WriteLine("Withdrawal attempt failed.");
return false;
}
public decimal Balance
{
get {return this.balance;}
}
public override string ToString()
{
return String.Format("Venus Bank Saver: Balance = {0, 6:C}", this.balance);
}
}
}
下面我们进行测试:
using System;
using Wrox.ProCSharp;
using Wrox.ProCSharp.Venus;
using Wrox.ProCSharp.Jupiter;
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(jupiter.ToString());
}
}
}
在这段代码中,要点是把两个引用变量声明为IBankAccount引用的方式。这表示他们可以指向实现这个接口的任何类的任何实例。但我们只能通过这些引用调用接口的一部分方法,如果要调用由类实现的但不在接口中的方法,就需要把引用强制转换为合适的类型。
接口引用完全可以看成类引用,但接口引用的强大之处在于,它可以引用任何实现该接口的类。
IBankAccount[] accounts = new IBankAccount[2];
accounts[0] = new SaverAccount();
accounts[1] = new GoldAccount();
4.2 派生的接口
接口可以彼此继承,其方式与类的继承方式相同。
下面定义一个接口IBankTransferAccount来说明这个概念,该接口的功能与IBankAccount相同,只是定义了一个方法,用来将资金转移到另一个账户上:
namespace Wrox.ProCSharp
{
public interface IBankTransferAccount:IBankAccount
{
bool TransferTo(IBankAccount destination, decimal amount);
}
}
该接口继承了IBankAccount的所有成员,所以在实现该接口的派生类时,必须要实现IBankAccount中的所有方法,以及新增加的方法。