文章目录
C# 6.0 本质论学习笔记 —— 第 6 章 继承
继承关系建立了“属于”关系。派生类型总是隐式的“属于”基类型。
6.1 派生
继承是为了通过扩展现有类型来添加更多功能、行为和数据而设计的。继承的声明方法和java与cpp类似。
- 通过继承,只要没有其它限制,基类的所有成员都会出现在继承链中。
- 除非明确的制定了基类,否则所有类都默认从object派生。
6.1.1 基类型和派生类型之间的转型
因为派生类和基类建立了“属于”关系,所以大多数情况下可以将派生类的值直接赋给基类的变量。
- 派生类的实例直接赋给基类,称为隐式转型,不需要添加转型操作符,而且转换总是会成功,不会引发异常。
- 从基类型转换为派生类型,不一定总是成功,要求显式转型,而且不一定总是能够成功。
- c#编译器允许在可能兼容的类型之间显式转型,这代表程序员请求编译器信任它。编译器会在运行时检查,如果对象实例不属于真正的目标类型将引发错误。c#中显示转型的形式类似于c中的强制转换。
- 隐式转换为基类并不会实例化新实例,而是同一个实例被引用为基类型。
- 隐式转换为派生类要求被实例化的类型必须是要转换成的目标类型。
高级主题:定义自定义转换
类型间的转换并不是只限制于单一继承链中的类型。
– 只要提供了转型操作符,不相关的类型之间也能够进行转换。
– 在转型有可能失败的时候,开发者应该定义显式转型操作符,提醒别人只有在确认转型会成功的时候才能进行这样的转型。
– 转型操作符的形式和cpp中转型操作符类似,但只需要做简单的修改就可在显式转型和隐式转型之间变化。
1 2 3 4 5 6 7 8 9 10 |
class GPSCoordinates { //.. public static implicit operator UTMCoordinarates(GPSCoordinarates coordinates) { //.. } } //将implicit改变为explicit则为显式转型 |
6.1.2 private访问修饰符
类的私有成员被继承但是不能够被派生类访问,和cpp、java相同。
6.1.3 protected访问修饰符
基类中的受保护成员只能从基类中以及派生链中其他类中访问。
- 要从派生类中访问受保护成员,必须从编译时确定是从派生类(或者它的某子类)实例中访问受保护成员。
6.1.4 拓展方法
- 拓展方法从技术上说不是类型的成员,所以不能够继承。
- 由于派生类可以作为其基类使用,所以拓展方法在派生类中也可用。
6.1.5 单继承
继承链中的类理论上是无限的,但是c#是单继承语言,c#编译而成的CIL也是。
语言对比:cpp多继承,java单继承
c#和cpp的一大区别就是不同于后者,c#是单继承的语言。在极少数需要使用多继承类结构的时候,一般的解决方法是使用聚合(aggregation):
- 挑选一个类别作为主要基类,从中派生出一个新类。
- 其他希望的基类作为派生类的字段。
- 这些方法被委托给了字段:除了因为委托增加了复杂性,还体现在字段类上新增的任何功能都需要人工添加到派生类中,否则,派生类无法增加任何新的功能。
6.1.6 密封类
把类标记为sealed,可以避免非预期的派生,并避免出现因此产生的问题。
6.2 基类的重写
ps:基类除了构造器和终结器之外都会在派生类中继承。
对于并非非常合适的重基类之中继承的方法,override(重写\覆盖)机制提供了很好的帮助。
6.2.1 virtual修饰符
- c#支持重写实例方法和属性,但不支持重写字段或者任何静态成员。cpp支持重写成员变量,java不行。cpp和java都不能重写静态变量。
- c#必须将重写的每个类型声明为virtual;cpp的virtual强调在运行时的动态多态,不强制要求重写的必须是虚函数;java默认的方法都是虚方法,假如希望方法有非虚的行为,需要显式的密封它。
- 不允许将virtual方法声明为private的。
- c#应用于派生类的override关键字是必须的,override关键字意味着派生类的实现会替换掉基类的实现。在cpp和java中不强制要求override的存在。
- 不同于cpp的动态选择,c#“运行时”调用虚方法派生的最远的实现:在构造器中不要调用会影响所构造的对象的任何虚方法,如果当前要实例化的类型如果在派生类中重写,就会调用重写的实现,但在继承层次结构中,字段尚未初始化,可能发生未定义的行为。而cpp由于其动态绑定,在构造期间调用的是基类型的实现,不会发生这类型的问题。
- cpp中含有virtual =0的都是抽象类,c#中只有声明为abstract才是抽象的。
6.2.2 new修饰符
如果重写方法没有使用override关键字,编译器将会显示警告信息:
- 明显可以添加overide修饰符以解决问题。
- 和可以使用new修饰符,这种情形称之为脆弱的基类(brittle base class或者fragile base class)
- new不会被继承链搜索发现,除非是自身调用。
6.2.3 sealed修饰符
对类使用sealed修饰符可以禁止从该类继承。虚方法也可以密封,这样可以禁止子类重写继承链中高层基类的虚方法。一般除非对单个成员的密封开销大于对类的开销,更多倾向于对方法进行密封。
6.2.4 base成员
重写成员时,开发者经常要调用基类版本。
- 使用base.Method()的语法即可,调用的是直接基类的方法,类似于cpp中基类作用域的效果。
- 用override修饰的任何方法都自动成为虚方法,可以明显的声明。
6.2.5 构造器
关于构造器的构造规则,和cpp类似。
- 实例化派生类时,“运行时”首先调用基类的构造器,以避免对绕过读基类的初始化。
- 假如基类没有可访问的(非私有)默认构造器,就不清楚如何构造基类,编译器会报告错误。
- 为避免缺少可访问的默认构造器而造成错误,程序员需要在派生类的头部显式的指定运行哪一个基类的构造器,语法和cpp类似。
1 2 3 4 5 6 7 8 |
public class Sub:Base { public Sub(String name):Base(name) { //.. } } |
6.3 抽象类
不适合实例化的类,只有作为基类,才有意义,这种类应当声明为抽象类,对应cpp中纯虚类。
- 为类添加abstract修饰符则将类声明为抽象类。
- 抽象类包含抽象成员,它没有实现的方法或者属性,其作用是强制所有派生出的类提供对应的实现。cpp中也有这种,不过不是abstract,而是使用=0的语法,叫做纯虚函数,另外cpp可以在类外作用域为此函数提供实现,虽然基本上是然并卵。
- 抽象成员必须被重写,因此不能是private,也会自动成为虚函数,但不能用virtual关键字形容。
6.5 使用is操作符验证基础类型
- 由于c#中允许在继承链中向下转型,因此有时候需要在转换之前判断基本类型。
- 在没有实现多态性的情况之下,一些依赖特定类型的行为也可能要事先确定类型。
c#提供了is操作符来判断基础类型。
6.6 使用as操作符进行转换
as操作符会产生类似转型操作符的效果,但是在转型不成功时会返回null。
- 相比于is操作符,as也有缺点,不能判断基础类型。
- 避免了用try-catch进行错误处理。