1.继承的类型:
在面向对象编程中,有两种截然不同的继承类型:
- 实现继承:表示一个类型派生于一个基类型,它拥有该及类型的所有成员字段和函数。派生类型采用基类型的每个函数的实现代码,除非指定重写某个函数的实现代码。在需要给现有的类型添加功能,或许多相同的类型共享一组重要的公共功能是,这种继承非常有用。
- 接口继承:表示一个类型只继承了函数的签名,没有继承任何实现代码。在需要指定该类型具有某些可用的特性时,最好使用这种类型的继承。
2.多重继承:
C#不支持多重继承。但允许类型派生自多个接口—多重接口继承。因为多重继承的优点是有争议的:
- 一方面:可以编写非常复杂、但很紧凑的代码,如C++ ATL库。
- 另一方面:代码很难理解和调试。
3.私有继承:
C#不支持私有继承,因此在基类名上没有public或private限定符,但C++支持。支持私有继承只会大大增加语言的复杂性。
4.虚方法:
- 把属性或方法声明为virtual,就可以在任何派生类中重写。
- 在派生类中重写时,要使用override关键字显式声明。此举避免了C++中很容易发生的潜在运行错误,即派生类的方法签名与基类略有差别,则方法不能重写。在C#中会出现编译错误,因为函数已标记为override,但没有重写基类的方法。
- 成员字段和静态函都不能声明为virtual,因为只对类中的实例函数成员有意义。
5.隐藏方法:
如果签名相同的方法在基类和派生类中都进行了声明,但该方法没有分别声明为virtual和override,派生类方法就会隐藏基类方法。
在C#中,要隐藏方法应使用new关键字声明,可以不显式声明,如:
class MyDerivedClass:HisBaseClass { public new int MyGroovyMethod() { //some groovy implementation return 0; } }
6.调用函数的基类版本:
从派生类调用方法的基类版本:base<MethodName>(),可以调用基类的任何方法,不必从同一方法的重载中调用它。
7.抽象类和抽象函数:
abstract:
- 抽象类不能实例化,而抽象函数不能直接实现,必须在非抽象的派生类中重写。显然,抽象函数本身也是虚拟的。
- 如果类包含抽象函数,则该类也是抽象的,也必须声明为抽象的。
8.密封类和密封方法:
sealed:
对于类,这表示不能继承该类;对于方法,这表示不能重写该方法。
要在方法和属性上使用sealed关键字,必须先从基类上把它声明为要重写的方法或属性。如:
class MyClass: MyClassBase { public sealed override void FinalMethod() ( // etc. ) } class DerivedClass: MyClass { public override void FinalMethod() //wrong. Will give compilation error ( ) }
适用于:
- 如果要对库、类或自己编写的其他类作用域之外的类或方法进行操作,则重写某些功能会导致代码混乱。
- 商业原因可以防第三方以违反授权协议的方式扩展该类。
9:派生类的构造函数
构造函数的调用顺序是先调用System.Object,再按照层次结构由上向下进行,直到到达编译器要实例化的类为止。
也就是说,派生类的构造函数可以在执行过程中调用它可以访问的任何基类方法、属性和任何其他成员,因为基类已经构造出来了,字段也初始化了。
在层次结构中添加无参数的构造函数:
对基类构造函数的调用,使用语法与调用不同重载版本的构造函数相同,区别是使用base而不是this。在base后圆括号没有参数,因为必须调用无参数的构造函数。
public abstract class GenericCustomer { private string name; public GenericCustomer() : base() //we could omit this line without affecting the compiled code/ { name = "<no name>"; } }
注意:如果编译器没有在左花括号前找到对另一个构造函数的任何引用,它就会假定我们要调用基类的构造函数—这符合默认构造函数的工作方式。
在层次结构中添加带参数的构造函数:
class Nevermore60Customer:GenericCustomer { //基类的字段交由基类的构造函数初始化,派生类无权限 public Nevermore60Customer(string name,string referrerName) : base(name) { this.referrerName = referrerName; } private string referrerName;//派生类本身需初始化的字段 // 添加构造函数重载满足未提供联系人的情况 public Nevermore60Customer(string name) : this(name,"<None>") { } }
当实例化customer时:
GenericCustomer customer = new Nevermore60Customer("cArabel Jones");
会通过以下流程:
调用单参数的构造函数—>把控制权传递给this(name,"<None>")调用的构造函数—>把控制权传递给GenericCustomer构造函数—>把控制权传递给Systerm.Object默认构造函数—>开始执行System.Object构造函数—>执行GenericCustomer构造函数,初始化name字段—>执行双参的自身构造函数,初始化联系人姓名—>执行单参构造函数,但什么也不做。
10.修饰符:
可见性修饰符
修饰符 | 应用于 | 说明 |
public | 所有类型或成员 | 任何代码均可以访问此项 |
protected | 类型和内嵌类型的所有成员 | 只有派生的类型能访问该项 |
internal | 所有类型或成员 | 只能在包含它的程序集中访问该项 |
private | 类型和内嵌类型的所有成员 | 只能在它所属的类型中访问该项 |
protected internal | 类型和内嵌类型的所有成员 | 只能在包含它的程序集和派生类型的任何代码中访问该项 |
注意,类型定义可以是内部的或公有的,这取决于是否希望在类型包含的程序集外部访问它
不能把类型定义为protected、private和protected internal,因为这些修饰符对于包含在命名空间的类型没有意义,这些只能应用与成员。也可以是嵌套的类型,因为在这种情况下类型也具有成员的状态。
如果有嵌套的类型,则内部的类型总是可以访问外部类型的所有成员。
其他修饰符
修饰符 | 应用于 | 说明 |
new | 函数成员 | 成员用相同的签名隐藏继承的成员 |
static | 所有成员 | 成员不作用于类的具体事例 |
virtual | 仅函数成员 | 成员可以由派生类重写 |
abstract | 仅函数成员 | 虚拟成员定义了成员的签名,但没有提供实现代码 |
override | 仅函数成员 | 成员重写了继承的虚拟和抽象成员 |
scaled | 类、方法和属性 |
对于类,不能继承自密封类。对于属性和方法,成员重写已继承的虚拟成员,但任何 派生类中的任何成员都不能重写该成员。该修饰符必须与override一起使用 |
extern | 仅静态[DllImport]方法 | 成员在外部用另一种语言实现 |
11.接口:
声明接口在语法上与声明抽象类完全相同,但不允许提供接口中任何成员的实现方式。
一般情况下,接口只能包含方法、属性、索引器和事件的声明。
- 不能实例化接口,它只能包含其成员的签名。
- 不能有构造函数也不能有字段。
- 不允许包含运算符重载。会引起与其它语言不兼容问题,如VB,VB不支持运算符重载。
大多数情况下,.Net的用法规则不鼓励采用Hungarian(即在前面增加字母表示定义的对象类型),接口是推荐采用此表示法的几种名称之一。如:IBankAccount
接口仅表示其成员的存在性,类负责确定这些成员是虚拟还是抽象的。
public interface IBankAccount { void PayIn(decimal amount); bool Withdraw(decimal amount); decimal Balance { get; } }
如果引用变量声明为接口引用的方式,则表示它们可以指向实现这个接口的任何类的任何实例。当我们只能通过这些引用调用接口的一部分方法—如果要调用由类实现的但不再接口中的方法,就需要把引用强制转换为合适的类型。但从任何接口到System.Object的数据类型强制转换是隐式的,如ToString()
接口引用完全可以看作是类引用—但接口引用的强大之处在于,它可以引用任何实现该接口的类。因此可以构造接口数组,其中数据的每个元素都是不同的类。
IBankAccount[] accounts = new IBankAccount[2]; accounts[0] = new SaverAccount(); accounts[1] = new GoldAccount();
12.派生的接口
接口可以彼此继承,其方式与类的继承方式相同。