前言
今天, 我们开始进入第四章的解读. 本章讲的是继承. 要做稍微复杂一些的开发, 便不可避免的会使用到继承. 本篇文章我们主要解读 "实现继承".
另外, 从本文开始, 我开始使用 Markdown 格式来书写文章, 它可以提供更好的布局风格和代码样式.
继承的类型
我们可能都知道, C++ 支持多继承, 但 Java 和 C# 都不支持多继承. 多继承非常繁琐和复杂, 但现实世界中却又有很多多继承的情况, 很简单的比如我们每个人都继承自父母, 那么怎么用对象化的视角来对现实世界建模呢?
对于 C# 来说, 就是实现继承和接口继承.
实现继承
表示一个类型派生于一个基类型, 它拥有该基类型的所有成员字段和方法. 在实现继承中, 派生类型采用基类型的每个方法的实现代码, 除非在派生类型的定义中指定重写某个方法的实现代码.
接口继承
表示一个类型只继承了方法的签名, 没有继承任何实现代码. 虽然 C# 不支持多继承, 却支持多接口继承, 从而相当程度上满足了多继承的现实要求.
另外, 我们也多次提到, 结构和类的不同. 对于继承这个概念来说, 结构和类的不同之处是, 结构不能继承. 但我们也知道结构继承自 System.ValueType, 所以更准确的说法是:
结构总是派生自 system.ValueType, 它们还可以派生自任意多个接口.
类总是派生自某个用户定义的类, 它们还可以派生自任意多个接口.
实现继承
一个典型的 C# 的继承的案例, 例如:
- public class HJD100Reader : RFReader, IRFReader, IDevice
- {
- // 主体代码
- }
我们也可以对比下 Java 的继承案例, 本质是一样的, 但在关键字上不同:
- public class HJD100Reader extends RFReader implements IRFReader,IDevice{
- // 主体代码
- }
我们可以明显的看出, C# 更精简, 它将类继承和接口继承的概念统一起来了, 都用 ":" 来标识继承关系. 而 Java 则很有规范性, 代码的可读性很强, 比如它的声明语法可以 "读出来":
类 HJD100Reader"继承" 自 RFReader 类, 并 "实现" 了 IRFReader 和 IDevice 接口.
说句题外话, 如果放三年前, 我会认为 C# 的写法真棒, 简洁! 但我现在的看法更多维. 我觉得未来更高级的语言, 在这一点上, 可能更像 Java 的表达, 而不是 C#. 如果我们讲, C# 在高级语言里做到了极致, 那么在未来 "超级语言" 里, 语言的 "表达能力" 可能更重要. 只有表达能力和表达方式更接近于普通 "人" 的表达, 才是未来 "人人编程" 时代的 "超级语言".
如果类定义中没有指定继承的基类, 并不是没有基类, 而是编译器会默认指定 System.Object 作为基类.
虚方法
把一个基类方法声明为 virtual, 就可以在任何派生类中重写该方法:
- public virtual string VirtualMethod()
- {
- // 虚方法可以有自己的实现代码, 并可以在其派生类中被重写
- }
这里讲到方法, 你有没有想起我们之前讲过的一个概念: 方法成员. 比如属性就是方法成员, 索引器也是, 等等. 所以, 对于虚方法的概念, 在这里应该可以推而广之, 虚方法适用所有方法成员, 而不仅仅是方法.
对于虚方法的应用, 很多人可能并不关注. 因为不用也妥妥的 "没问题", 系统编译时只是一个警告. 我自己写 C# 了开头几年其实都不是很严谨的使用 virtual.
为什么是这样, 其实溯源起来, 还 Java 和 C++ 都有关系. Java 的虚方法概念是: 所有方法都是虚方法, 用一句土话说, 就是 "所有方法都应该可以被继承, 这是所有方法的福利". 但 C# 就觉得这个不行, 太粗放了, 需要更严谨. 它采用了 C++ 的做法. 据说语言性能上会更加优异. 而我则更看重从语言设计的角度出发的考虑: 一个方法是否应该被重写, 这个权力要交给程序员.
程序员内心 OS:
程序员:"嗯, 我认为这个方法, 后代很可能会重写以扩展功能, 但我也会提供一个基本的通过功能".
这个时候, 加上 virtual.
程序员:"这个方法我开放出来的目的, 是作为公开调用的接口使用的, 派生类不可能有对其进行重写的场景".
那么, 不加 virtual.
以上这些, 都表明了书写类库的程序员的态度, 这个态度传达给客户端程序员, 从而双方达成更细致的沟通.
名词解释:
类库程序员: 是写基础类库的程序员
客户端程序员: 调用类库程序员写的类库, 实现具体业务方法的程序员
而对应虚方法的 virtual, 在派生类中重写方法时, 需要显式的加 override:
- public class Base
- {
- /// <summary>
- /// 类库程序员认为: 不需要重写
- /// </summary>
- public void Method1()
- {
- }
- /// <summary>
- /// 类库程序员认为: 可能重写
- /// </summary>
- public virtual void Method2()
- {
- }
- }
- public class Child : Base
- {
- /// <summary>
- /// 客户端程序员: 不小心覆盖了基类方法
- /// </summary>
- public void Method1()
- {
- // 如上书写, 在 IDE 中会将方法名以波浪号提示 代码存在隐患
- }
- /// <summary>
- /// 客户端程序员: 规范的书写
- /// </summary>
- public override void Method2()
- {
- base.Method2();
- }
- }
隐藏方法
如上节所写, 如果客户端程序员不小心写了覆盖基类的方法 Method1(), 编译器会提示 "你隐藏了基类的方法". 但这个时候, 这个隐藏可能不是程序员主动的行为, 而是一种无意中造成的隐患. 客户端程序员收到类库程序员发出的这个 "信号", 就可以是纠正错误, 即:
如果确实要覆盖基类的方法, 那么应该显式的用 new 关键字
- public new void Method1()
- {
- // 加上 new, 显式的表示, 客户端程序员已经知晓, 是有意隐藏基类方法
- }
如果不是要覆盖基类方法, 就说明客户端程序员定义的方法签名无意中和基类的方法签名重复了, 应该更改派生类中的方法签名.
在本书的这部分章节, 讲解基类方法如何重写, 是否隐藏等机制分析上, C# 语言创始人真的是有很多内心 OS, 通过各种关键字的使用, 解决了开发中团队协作, 程序的多版本部署, 客户版本升级等等各种场景下, 很好的使用这几个关键字, 就能实现一个强壮的系统. 非常精彩. 大家有兴趣可以自己去翻翻.
重申一下, 这几个关键字, 真的很重要, 大家平时要多想多用:
virtual, override, new
调用基类版本
前面讲 override 的时候, 就有提到调用基类版本, 即 base.< 方法 >, 那是 IDE 智能带入的:
- public override void Method2()
- {
- base.Method2(); // 调用基类方法
- base.Method3(); // 调用基类的其他方法
- }
抽象类和抽象方法
- C# 允许把类和方法声明为 abstract. 抽象类不能实例化, 而抽象方法不能直接实现, 必须在非抽象的派生类中重写. 显然, 抽象方法本身也是虚拟的 (尽管也不需要提供 virtual 关键字, 实际上, 如果提供了该关键字, 就会产生一个语法错误). 如果类包含抽象方法, 则该类也是抽象的, 也必须声明为抽象的.
- public abstract class AbstractClass
- {
- /// <summary>
- /// 抽象方法: 只有声明, 没有方法体; 要求必须所在 class 也是抽象的
- /// </summary>
- public abstract void Method1();
- /// <summary>
- /// 抽象类中可以声明 非抽象方法
- /// </summary>
- public void Method2()
- {
- // 基类代码
- }
- }
抽象类和抽象方法, 是不是很 "抽象"! 我在一年前其实都还没用过抽象类和抽象方法, 因为用不上. 更普遍的一些语言特性已经能用的够好了. 我个人觉得, 什么情况下, 才会够格用上抽象类和抽象方法呢? 写第三方类库的程序员们.
如果我们实现一个程序, 要封装一些业务场景下要用的基础类库, 一般也都用不上抽象类和抽象方法. 但对于第三方类库的开发者, 他们面对的是成千上万的客户端程序员, 设计上一点点瑕疵, 一点点不便, 都会客户端程序员们带来巨大的不便 (绝对量上). 因此仔细的设计变得非常有必要和有意义.
比如, 对照着以上的代码案例, 我可以试着解说一下:
为什么要定义抽象方法?
类库程序员 OS:
类库程序员: 我需要声明一个方法, 这个方法是类设计的重要成员, 但它确实无法在基类中实现任何代码.
提问者: 那么你可以定义一个声明为 virtual 的空方法吗?
类库程序员: 当然可以, 但作为一个追求卓越的程序员, 搞一个空方法, 这不严谨呀, 万一别人以为我空方法里忘写代码了呢! 你说加个注释说明这就是空方法? 你不觉得很累赘吗? 所以, 我需要抽象方法.
为什么要在抽象类中定义非抽象方法?
类库程序员 OS:
类库程序员: 我需要定义一个方法, 提供一些基础功能, 这些功能需要在派生类中被重写并调用, 然后加上派生类自己的逻辑.
提问者: 很好的想法. 你可以使用 vitual,override,base 关键字实现你的想法.
类库程序员: 你说的对. 但我这个基类提供的方法, 它有用, 但确实是不能独立使用的, 它要通过派生类的重写和调用才能产生真正的效果. 我担心小白程序员们, 随意的 new 这个基类, 而导致他们白白浪费精力.
提问者: 嗯, 你想的周全, 那么应该给 class 也加上 abstract, 这样小白程序员们就无法 new 这个基类了, 他们只能 new 派生类.
类库程序员: 完美!
因为继承非常重要, 所以我也写的非常细致. 时间不够, 今天就写到这里, 下一篇我们继续.
觉得文章有意义的话, 请动动手指, 分享给朋友一起来共同学习进步.
来源: https://www.cnblogs.com/holyknight17/p/10305062.html