【C#本质论 七】类-从设计的角度去认知(继承)

上一篇文章简要概述了为什么要有类,面向对象模式的好处,并且详细说明了三大特性之一封装的重要性,这一篇文章我们就继续学习下面向对象三大特性之二的继承。本章的结构如下:
【C#本质论 七】类-从设计的角度去认知(继承)_第1张图片

宏观认知

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 .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的实现。
【C#本质论 七】类-从设计的角度去认知(继承)_第2张图片

重写

重写的概念已经很熟了,就像上文列举的,为了扩展而准备,需要注意以下几点:

  • C#支持重写实例方法和属性、不支持字段和任何静态成员重写
  • C#重写的成员必须显示添加virtual关键字,C#默认方法非虚
  • 如果派生类要重写方法,必须显式的使用override关键字!
  • 重写后的override修饰的方法还是虚方法,还能被继承重写

还有一个重要的修饰符就是new,可以在派生类显式的隐藏从基类继承的成员! 说白了new的一个重要作用是,如果派生类使用了和基类相同的方法签名,不管这个方法是虚方法还是实例方法,都会隐藏基类的方法而用自己的实现。

脆弱的基类:new修饰符

还有一点值得注意:虽然运行时调用派生的最远的虚成员,但如果遇到new就会变为,new之前的派生的最远的虚成员

 public class Program
    {
        public class BaseClass
        {
            public void DisplayName()
            {
                Console.WriteLine("BaseClass");
            }
        }

        public class DerivedClass : BaseClass
        {
            // Compiler WARNING: DisplayName() hides inherited 
            // member. Use the new keyword if hiding was intended
            public virtual void DisplayName()
            {
                Console.WriteLine("DerivedClass");
            }
        }

        public class SubDerivedClass : DerivedClass
        {
            public override void DisplayName()
            {
                Console.WriteLine("SubDerivedClass");
            }
        }

        public class SuperSubDerivedClass : SubDerivedClass
        {
            public new void DisplayName()
            {
                Console.WriteLine("SuperSubDerivedClass");
            }
        }

        public static void Main()
        {
            SuperSubDerivedClass superSubDerivedClass
                = new SuperSubDerivedClass();

            SubDerivedClass subDerivedClass = superSubDerivedClass;
            DerivedClass derivedClass = superSubDerivedClass;
            BaseClass baseClass = superSubDerivedClass;

            superSubDerivedClass.DisplayName();  //SuperSubDerivedClass
            subDerivedClass.DisplayName(); //SubDerivedClass
            derivedClass.DisplayName();//SubDerivedClass
            baseClass.DisplayName();//BaseClass
        }
    }

这里最远的是SuperSubDerivedClass ,因为派生链最终指向了 SuperSubDerivedClass superSubDerivedClass= new SuperSubDerivedClass()产生的实例对象,如果为:

using System;

public class Program
{
    public class BaseClass
    {
        public virtual void DisplayName()
        {
            Console.WriteLine("BaseClass");
        }
    }

    public class DerivedClass : BaseClass
    {
        // Compiler WARNING: DisplayName() hides inherited
        // member. Use the new keyword if hiding wasb intended
        public override void DisplayName()
        {
            Console.WriteLine("DerivedClass");
        }
    }

    public class SubDerivedClass : DerivedClass
    {
        public override void DisplayName()
        {
            Console.WriteLine("SubDerivedClass");
        }
    }

    public class SuperSubDerivedClass : SubDerivedClass
    {
        public new void DisplayName()
        {
            Console.WriteLine("SuperSubDerivedClass");
        }
    }

    public static void Main()
    {
        SuperSubDerivedClass superSubDerivedClass
            = new SuperSubDerivedClass();
        DerivedClass derivedClass1 = new DerivedClass();
        SubDerivedClass subDerivedClass = superSubDerivedClass;
        DerivedClass derivedClass = subDerivedClass;
        BaseClass baseClass = derivedClass1;

        superSubDerivedClass.DisplayName();  //SuperSubDerivedClass
        subDerivedClass.DisplayName(); //SubDerivedClass
        derivedClass.DisplayName();//SubDerivedClass
        baseClass.DisplayName();//DerivedClass
    }
}
       
    }

baseClass.DisplayName(), 这里的最远就是DerivedClass里new出来的实例。

密封方法,禁止重写

和密封类一样,也有密封方法,就是子类继承实现后,不允许孙类继续继承,sealed和override是紧密配合使用的!

namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter07.Listing07_15
{
    class A
    {
        public virtual void Method()
        {
        }
    }

    class B : A
    {
        public override sealed void Method()
        {
        }
    }

    class C : B
    {
        // ERROR:  Cannot override sealed members
        //public override void Method()
        //{
        //}
    }
}

base修饰符

和this一样,base可以用于调用基类的成员,显式调用基类的实现,这个时候不用实现最远的派生,就是自己的方法实现(即使这个方法是虚方法)
【C#本质论 七】类-从设计的角度去认知(继承)_第3张图片
有了base修饰符,就能在代用派生类构造函数前确定要调用哪一个基类的构造函数。

 public class PdaItem
    {
        public PdaItem(string name)
        {
            Name = name;
        }

        // ...
        public string Name { get; set; }
    }
    public class Contact : PdaItem
    {
        public Contact(string name) :
            base(name)
        {
            Name = name;
        }

        public new string Name { get; set; }
        // ...
    }

抽象类

抽象类说白了,就是仅供派生的类,无法实例化,与之相反的是,不抽象可直接实例化的是具体类

 public abstract class PdaItem
    {
        public PdaItem(string name)
        {
            Name = name;
        }

        public virtual string Name { get; set; }  //虚成员已被基类实现,可以派生类被重写实现
        public abstract string GetSummary();     //抽象方法未被基类实现,必须被派生类实现!
    }

虚成员已被基类实现,可以派生类被重写实现,抽象成员未被基类实现,必须被派生类实现!并且**抽象成员(未被实现的方法或属性)**自动为虚。

System.Object

在万类始祖提供的方法里,
【C#本质论 七】类-从设计的角度去认知(继承)_第4张图片

模式匹配

这部分内容主要聊聊两个操作符:is和as

is操作符

is作用是检查一个对象是否兼容于其他指定的类型,并返回一个Bool值如果一个对象是某个类型或是其父类型的话就返回为true,否则的话就会返回为false,永远不会抛出异常,如果对象引用为null,那么is操作符总是返回为false,因为没有对象可以检查其类型。

 public static void Save(object data)
        {
            //is操作符的一般用法是向下显式转型之前,使用is来确定实例对象是否兼容于对应类型?
            if (data is string)    //如果data为null也返回false
            {
                string text = (string)data;
                if (text.Length > 0)  
                {
                    data = Encrypt(text);
                    // ...
                }
            }         
         
        }

即使这个对象是类型的子类型,转型也会失败哦,易错点,记住了(虽然编译的时候不会报错):
【C#本质论 七】类-从设计的角度去认知(继承)_第5张图片
从C#7.0开始,is增加了模式匹配:

 public static void Save(object data)
        {
            if (data is string text && text.Length > 0)  
            {
                data = Encrypt(text);
                // ...
            }
            else if (data is null)
            {
                throw new ArgumentNullException(nameof(data));
            }
            // ...

            Console.WriteLine(data);
        }

data is string text,如果data是string类型,则返回true,同时把值赋给text,这样就不需要两步操作了。主要用途就是判断一个数据项是否属于特定类型

switch对模式匹配的支持

之前的博客里简单聊过从C#7.0开始,支持模式匹配,但没有详细介绍过:

static public void Eject(Storage storage)
        {
            switch (storage)
            {
                case null: // The location of case null doesn't matter
                    throw new ArgumentNullException(nameof(storage));
                // ** Causes compile error because case statments below
                // ** are unreachable
                // case Storage tempStorage:
                //    throw new Exception();
                //    break;
                case UsbKey usbKey when usbKey.IsPluggedIn:
                    usbKey.Unload();
                    Console.WriteLine("USB Drive Unloaded!");
                    break;
                case Dvd dvd when dvd.IsInserted:
                    dvd.Eject();
                    Console.WriteLine("DVD Ejected!");
                    break;
                case Dvd dvd when !dvd.IsInserted:
                    throw new ArgumentException("There was no DVD present.");
                case HardDrive hardDrive:
                    throw new InvalidOperationException();
                default:   // The location of case default doesn't matter
                    throw new ArgumentException(nameof(storage));
            }
        }

有以下几点需要注意:

  • 和基本switch语句不同,模式匹配case子句不限于有常量值的类型(string,int,long,enum等)。相反,任何类型都可使用
  • 模式匹配case标签在类型后声明一个变量:case HardDrive hardDrive:该变量的作用域限于当前switch小节(始于case标签,中间是一个或多个语句,结束于跳转语句。)
  • 模式匹配case标签支持条件表达式,允许对条件进行额外筛选,例如:case UsbKey usbKey when usbKey.IsPluggedIn
  • 模式匹配switch小节的顺序变得重要。为基类写一个case标签,且不添加任何条件表达式(例如只写case Storage storage:),后面为派生类写的switch块都不会执行而且不写条件表达式,后边如果有case语句,编译器会报错。但如果基类的case标签写了条件表达式(编译时解析不了),之后的派生类标签都会被屏蔽
  • 为null写的switch小节可在任意位置,它解析为true的条件总是具有唯一性。(和基本switch语句一样,default标签位置还是随意。)
  • 允许针对相同类型写多个模式匹配case标签,前提是其中最多一个没有条件表达式,而且要想有条件表达式的执行,这个没有的要放到最后
  • 常量switch小节可以和模式匹配switch小节混合使用。当然优先考虑简化
  • 模式匹配switch小节仍然需要跳转语句。
  • case子句不允许使用可空类型(例如int?)。改为使用非可空版本。这是因为空值会匹配case null,永远不会匹配针对可空类型的case子句

暂时不需要太深入的去了解,在实践中验证吧。

as操作符

比起is操作符,as操作符的不同是:直接进行转型,转型成功则直接使用该类型的引用,如果转型失败返回null

 static object Print(IDocument document)
        {
            if(document != null)
            {
                // Print document...
            }
            else
            {
            }

            return null;
        }

        static void Main()
        {
            object data = new object();

            // ...

            Print(data as Document);
        }

as有个缺点就是不能判断基础类型,也就是不能用在值类型里:AS是引用类型类型的转换或者装箱转换,不能用与值类型的转换。如果是值类型只能结合is来强制转换

本篇博客详细介绍了继承这一特性,顺便梳理了下几个操作符的使用,对转型操作有了更深入的理解。

你可能感兴趣的:(C#学习系列)