上一篇文章简要概述了为什么要有类,面向对象模式的好处,并且详细说明了三大特性之一封装的重要性,这一篇文章我们就继续学习下面向对象三大特性之二的继承。本章的结构如下:
宏观认知
1,为什么要有继承?(爹给儿子的,有些儿子可以修改,有些不可以修改)
上一篇博客我提到,面向对象编程的好处:可以不必从头创建新程序,而是用现有的一个模板去复制、去扩展,或添加更多,而且整个代码还非常有条理,可以控制访问权限、组织起来不会乱,我们从一个模板new一个类出来就可以具备该类的全部功能是不是很方便,但有时候有些类不能满足我们的需求,但也不至于再因为一个小的功能就重新搞个类出来,没有必要,这个时候继承的好处就体现出来了,不仅全部基类代码可以复用,而且还可以在派生类上添加额外成员或修改(重写)基类成员。
2,如何实现只让自己的派生类访问自己的成员?
用protected,除了自己和自己的儿子,别人休想用我的成员实现!
3,为什么要重写?(儿子用爹可修改的方法或属性并修改)
当基类的方法或者属性实现在不同的派生类中有不同的使用方式的时候,把基类的方法声明为虚方法,这样可以实现面向对象思想的另一个重要特性:多态。重写能让派生类自定义自己的实现,而不是统一使用父类的方法。
4,啥时候用new操作符?(儿子不用爹的,儿子自己实现)
简单而言就是如果从基类继承的某个方法和自己想要实现的方法同名(同一个方法签名:包括参数量、参数类型、方法名),这个时候要显示的用new来表明自己要使用自己的方法实现,而不是基类的,不然的话会报警告。
5,如何防止方法被重写?(儿子用爹的可修改方法或属性并修改,孙子自己实现)
当派生类重写完基类的方法后,不想让该方法进一步被重写,所以使用sealed配合override使用重写能让派生类自定义自己的实现,而不是统一使用父类的方法。
6,抽象类是干啥的?(所有儿子必须用爹的抽象成员并修改和实现)
抽象类就是不能new对象,只能被其它类继承的类就是一个不可能拥有对象的可怜类,,但主要特点是强制所有派生类型提供对它定义的抽象方法的实现,当年我没考上大学,对孩子们寄予厚望,你可一定要考上啊!。
7,万物始祖Object(所有爹的爹,众爹之爹)
所有类都隐式的从Object派生出来,
8,如何判断类型并尝试转换为特定类型(如何证明你爹是你爹)
使用is和as可以显式的判断在转换前该对象是否属于基础类型。
派生
继承建立了“属于”(is-a)关系。派生类型总是隐式属于基类型。如同硬盘属于存储设备,从存储设备类型派生的其他任何类型都属于存储设备。反之则不成立。存储设备不一定是硬盘
- 派生/继承:对基类进行特化,添加额外成员或自定义基类成员。
- 派生类型/子类型:继承了较常规类型的成员的特化类型。
- 基/超/父类型:其成员由派生类型继承的常规类型。
派生中有以下几点需要注意:
1,基类的每个成员都出现在派生类构成的链条中,也就是祖宗的东西每一代都在进行增量增加。
2,除非明确指定基类,否则所有类都默认从object派生,但是同时注意,如果明确指定了基类,就不算object直接派生了,因为:C#是单继承的!
3, 要想绕路实现多继承,可以通过聚合的方式,即派生类一方面继承自己基类的一切,又把另一个类当成自己的成员。
namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter07.Listing07_08 { public class PdaItem { // ... } public class Person { public string FirstName { get; set; } public string LastName { get; set; } // ... } public class Contact : PdaItem { private Person InternalPerson { get; set; } public string FirstName { get { return InternalPerson.FirstName; } set { InternalPerson.FirstName = value; } } public string LastName { get { return InternalPerson.LastName; } set { InternalPerson.LastName = value; } } // ... } }
这样看起来就好像继承了Person的属性一样,这种方式显然不靠谱
4,派生类继承除构造函数和析构器之外的所有基类的成员,但继承并不意味着一定能访问!
public class PdaItem { private string _Name; //private修饰,当然访问不了 // ... } public class Contact : PdaItem { // ... } public class Program { public static void Main() { Contact contact = new Contact(); // ERROR: 'PdaItem. _Name' is inaccessible // due to its protection level //contact._Name = "Inigo Montoya"; //uncomment this line and it will not compile } }
5,基类的受保护成员只能从基类及其派生链的其他类中访问:
namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter07.Listing07_07 { using System; using System.IO; public class Program { public static void Main() { Contact contact = new Contact(); contact.Name = "Inigo Montoya"; // ERROR: 'PdaItem.ObjectKey' is inaccessible // due to its protection level //contact.ObjectKey = Guid.NewGuid(); //uncomment this line and it will not compile } } public class PdaItem { protected Guid ObjectKey { get; set; } // ... } public class Contact : PdaItem { void Save() { // Instantiate a FileStream using <ObjectKey>.dat // for the filename FileStream stream = System.IO.File.OpenWrite( ObjectKey + ".dat"); } void Load(PdaItem pdaItem) { // ERROR: 'pdaItem.ObjectKey' is inaccessible // due to its protection level //pdaItem.ObjectKey =... Contact contact = pdaItem as Contact; if(contact != null) { contact.ObjectKey = new Guid();//... } } // ... public string Name { get; set; } } }
//Contact.Load()方法有一个容易被忽视的细节:即使Contact从PdaItem派生,从Contact类内部也无法访问一个PdaItem实例的受保护ObjectKey。这是由于万一PdaItem是一个Address,Contact不应访问Address的受保护成员。所以,封装成功阻止了Contact修改Address的ObjectKey。成功转型为Contact可绕过该限制。基本规则是,要从派生类中访问受保护成员,必须能在编译时确定它是派生类(或者它的某个子类)中的实例。
6,扩展方法理论上不属于类型的成员,所以不可继承,但其实扩展方法也可以在派生类中用,所以如果继承链中有同名的方法,优先调用实例方法。
类型密封
如果不希望自己的类被派生,则可以使用sealed关键字对类进行密封:
namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter07.Listing07_09 { public sealed class CommandLineParser { // ... } // ERROR: Sealed classes cannot be derived from public sealed class DerivedCommandLineParser //: CommandLineParser //uncomment this line and it will not compile { // ... } }
类型转换
派生类型可以隐式转为基类型,而基类型则需要显式转换。
public class Test { public void TestMethod(Father data) { Child c = (Child)data; //向下转型,显式 } public void TestMethod2(Child data) { Father c = data; //向上转型,隐式 } }
需要注意以下几点:
- 隐式转型为基类不会实例化新实例,而是将同一个实例引用为基类型,它现在提供的功能(可访问的成员)是基类型的。这类似于将CD-ROM驱动器说成是一种存储设备。由于并非所有存储设备都支持弹出操作,所以CDROM转型为存储设备后不再支持弹出
- 类似地,将基类向下转型为派生类会引用更具体的类型,类型可用的操作也会得到扩展。但这种转换有限制,被转换的必须确实是目标类型(或者它的派生类型)的实例。
public class BaseClass { public virtual void DisplayName() { Console.WriteLine("BaseClass"); } } public class DerivedClass : BaseClass { public override void DisplayName() { Console.WriteLine("DerivedClass"); } public void Set() { Console.WriteLine("不可能"); } } public static void Main() { BaseClass baseClass1 = new BaseClass(); DerivedClass dr = (DerivedClass)baseClass1; //这里虽然编译时没有问题,但运行时会报错,转换不成功, //因为该实例是基类的,即使转为派生类引用,也不能调用派生类自己的方法 dr.Set(); }
- 显式转型一定会在允运行时被CLR检查,即使侥幸能躲过编译检查,最终还是逃脱不了运行时检查
类型转型时应该严格注意以上几点。向上转为基类,调用的方法是引用者本身的方法,例如Father c =data,c调用的非虚成员为自己的,但是如果该方法或属性为虚,则调用派生的最远的(这个最远的是派生链上最终指向的实例对象),因为遵循原则:运行时调用派生的最远的虚成员<\font>
public class Program { public static void Main() { Contact contact; //派生类 PdaItem item; //基类 contact = new Contact(); item = contact; //向上转型,隐式 // Set the name via PdaItem variable item.Name = "Inigo Montoya"; //属性调用的为Contact.Name的实现 // Display that FirstName & LastName // properties were set Console.WriteLine( $"{ contact.FirstName } { contact.LastName}"); } }
调用如下,father调用的实例方法还是自己的,但是虚方法实现是child的实现。