《C#本质论》 第7章 继承

第7章 继承

第6章讨论了类如何通过字段和属性来引用其他类,本章讨论如何利用类的继承关系建立类层次结构。
初学者主题:继承的定义
•派生/继承:对基类进行特化,添加额外成员或自定义基类成员。
•派生类型/子类型:继承了较常规类型的成员的特化类型。
•基/超/父类型:其成员由派生类型继承的常规类型。
•继承建立了”属于 is-a“关系,派生类型总是隐式属于基类型。

注意 代码中的继承用于定义两个类的”属于“关系,派生类是对基类的特化

7.1 派生

不是重新定义这两个类都适用的的方法和属性,而是进行重构,并让其派生于该类。

**注意 **通过继承,基类的每个成员都出现在派生类构成的链条中

**注意 **除非明确指定基类,否则所有类都默认从object派生

7.1.1 基类型和派生类型之间的转型

因为派生建立了属于关系,所以总是可以将派生类型的值直接赋给基类型的变量。
从基类型转换为派生类型需要执行显式转型,运行时可能失败。

注意 派生类型能隐式转型为它的基类,相反,基类向派生类转换要求显式的转型操作符,因为转换有可能失败。虽然编译器允许可能有效的显式转型。但”运行时“会坚持检查,无效转型将引发异常。

7.1.2 prvate修饰符

派生类继承除了构造函数和析构器的所有基类成员,但继承并不意味着一定能访问。因为私有成员只能在声明它们的类型中访问。
根据封装原则,派生类不能访问基类的private成员,这就强迫基类开发者决定一个成员是否能由派生类访问。
注意 派生类不能访问基类的私有成员

7.1.3 protected访问修饰符

public或private代表两种极端情况,中间还可进行更细致的封装,可在基类中定义只有派生类才能访问的成员。
注意 基类的受保护成员只能从基类及其派生链的其他类中访问

7.1.4 扩展方法

扩展方法从技术上说不是类型的成员,所以不可继承,但由于每个派生类都可作为它的基类的实例使用,所以对一个类型进行扩展方法也可扩展它的任何派生类型。如扩展基类,所有扩展方法在派生类中也能使用,但和所有扩展方法一样,实例方法有更高的签名,如果继承链中出现了一个兼容的签名,那么它将优先于扩展方法。
很少为基类写扩展方法,直接修改基类会更好。即使基类代码不可用,程序员也应考虑在基类或个别派生类实现的接口上添加扩展方法。

7.1.5 单继承

继承树中的类理论上数量无限,单C#是单继承语言,C#编译成的CIL语言也是一样。也意味着一个类不能直接从两个类派生。

C#的单继承是其面向对象方面与C++的主要区别之一
极少数需要多继承类结构的时候,一般的解决方案是使用聚合。换言之,不是一个类从另一个类继承,而是一个类包含另一个类的实例。

7.1.6 密封类

为正确设计类,使其它人能通过派生来扩展功能,需对它进行全面测试,验证派生能成功进行。密封类用sealed修饰符静止从其派生。string类型就是用sealed修饰符禁止派生类型。

7.2重写基类

某些情况下,一个成员可能在基类中没有得到最佳实现,这时候需要重写基类。

7.2.1 virtual修饰符

C#支持重写实例方法和属性,但不支持字段和任何静态成员的重写。为进行重写,要求在基类和派生类中都显式执行一个操作。基类将必须允许重写的每个成员都标记为virtual。

7.2.2 new修饰符

如果重写方法没有使用override关键字,编译器会生成警告信息。还可使用new修饰符,这种情形称为脆弱的基类
这种语义要用new修饰符实现,它在基类面前隐藏了派生类重新声明的成员,这时不是调用派生得最远的成员,相反,是搜索继承链,找到使用new修饰符的那个成员之前、派生最远的成员,然后调用该成员。但假如没有指定overrider也没有指定new,就默认为new,从而维持版本的安全性。

7.2.3 sealed修饰符

为类使用sealed修饰符可禁止从该类派生,类似的,虚成员也可密封,这会禁止子类重写基类的虚成员。

7.2.4 base成员

调用基类的实现需要使用base关键字,base语法和this几乎完全一样,也允许作为构造函数的一部分使用。
注意 用overrider修饰的任何方法自动为虚,只有基类的虚方法才能重写,所以重写后的方法还是虚方法。

7.2.5 调用基类的构造函数

实例化派生类时,“运行时”首先调用基类构造函数,防止绕过基类的初始化机制。但假如基类没有可访问的(非私有)默认构造函数,我们就不知道如何构造基类,C#编译器将会报错。为避免因为缺少可访问的默认构造函数而造成错误,程序员需要在派生类构造函数的头部显式指定要运行哪一个基类构造函数

using System.Diagnostics.CodeAnalysis;

namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter07.Listing07_14
{
    public class PdaItem
    {
        public PdaItem(string name)
        {
            Name = name;
        }

        // ...

        public virtual string Name { get; set; }
    }
    public class Contact : PdaItem
    {
        // Disable warning since FirstName&LastName set via Name property 
        #pragma warning disable CS8618 // Non-nullable field is uninitialized. 
        public Contact(string name) :
            base(name)
        {
        }
        #pragma warning restore CS8618

        public override string Name
        {
            get
            {
                return $"{ FirstName } { LastName }";
            }

            set
            {
                string[] names = value.Split(' ');
                // Error handling not shown
                FirstName = names[0];
                LastName = names[1];
            }
        }

        [NotNull] [DisallowNull]
        public string FirstName { get; set; }
        [NotNull] [DisallowNull]
        public string LastName { get; set; }

        // ...
    }
}

7.3 抽象类

该类本身不适合实例化,实例没有意义,只有作为基类,在从其派生的一系列数据类型之间共享默认的方法实现,才是该类真正的意义,这意味着该类应被设计为抽象类。不抽象、可直接实例化的类称为具体类
初学者主题:抽象类
抽象类代表抽象实体,抽象成员定义了从抽象实体派生的对象应该包含什么,但这种成员不包括实现。抽象类的大多数功能通常没有实现,一个类要从抽象类成功派生,必须为抽象基类中的抽象方法提供了具体的实现。

不能实例化还在其次,抽象类的主要特点在于它包含抽象成员抽象成员是没有实现的方法或属性,作用是强制所有派生类提供实现。

抽象方法不包含任何实现,由于我们将抽象成员设计为被重写,所以自动为虚此外,抽象成员不能声明为私有,否则派生类看不见它们。
开发具有良好设计的对象层次结构殊为不易,所以在编程抽象类型时,一档要自己实现至少一个或多个从抽象类型派生的具体类型,以检验自己的设计。
注意 抽象成员必须被重写,所以自动为虚,但不能用virtual关键字显式声明。
初学者主题:多态性

7.4 所有类都从SYstem.Object派生

所有这些方法都通过继承为所有对象所用,所有类都直接或间接从object派生,即使字面值也支持这种方法。

7.5 使用is操作符验证基础类型

由于C#允许在继承链中向下转型,因此有时需要在转换前判断基础类型是什么。

7.5 用is操作符进行模式匹配。

7.5.1 使用is操作符验证基础类型

由于C#允许在继承链中向下转型,因此有时需要在转换前判断基础类型是什么。此外,在没有实现多态性的情况下,一些要依赖特定类型的行为也可能要事先确定类型。C#用is操作符判断基础类型。

using System;

namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter07.Listing07_19
{
    public class Program
    {
        public static void Save(object data)
        {

            if (data is string)
            {
                string text = (string)data;
                if (text.Length > 0)
                {
                    data = Encrypt(text);
                    // ...
                }
            }
            // ...

            Console.WriteLine(data);
        }

        public static object Encrypt(string data)
        {
            // See Chapter 19 for actual encryption implementation
            return $"ENCRYPTED <{data}> ENCRYPTED";
        }
    }
}

注意通过显式转型,程序员宣布自己负责创建清晰的代码逻辑来避免无效的强制类型转换。如可能发生无效转型,应首选使用is操作符并完全避免异常。is操作符的好处是能创建一个显式转型可能失败但又没有异常处理开销的代码路径。从C# 7.0开始,is操作符除了用于类型检查,也可以用于声明变量并且赋值。

7.5.2 type、var和const的模式匹配

C# 7.0增强了is操作符来支持模式匹配(pattern matching)。上个例子核实数据是string后,仍然必须把它转型为string(前提是想把它作为string来访问)。更好的方案是执行类型的检查,如结果为true,就同时声明该类型的变量,并将结果赋给该变量。

C# 7.0引入了模式匹配特性后,我们可以对type、var和const进行模式匹配。而C# 8.0在此基础上又增加了元组(tuple)模式匹配、按序模式匹配、属性模式匹配,以及递归模式匹配。is操作符的这些新用法通常可以替代该操作符早期的基本用法。

在这里插入图片描述
在这里插入图片描述

7.5.3 元组模式匹配

7.5.4 顺序模式匹配

7.5.5 属性模式匹配

7.6 switch语句中的模式匹配

using System;

namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter07.Listing07_24
{
    public class Program
    {
        public static string? CompositeFormatDate(
                object input, string compositFormatString) =>
            input switch
            {
                DateTime { Year: int year, Month: int month, Day: int day } tempDate
                    when tempDate < DateTime.Now
                    => (year, month, day),
                DateTimeOffset
                { Year: int year, Month: int month, Day: int day }
                        => (year, month, day),
                string dateText => DateTime.TryParse(
                    dateText, out DateTime dateTime) ?
                        (dateTime.Year, dateTime.Month, dateTime.Day) :
                        // default ((int Year, int Month, int Day)?) preferable
                        // not covered until Chapter 12.
                        ((int Year, int Month, int Day)?)null,
                _ => null
            } is { } date ? string.Format(
                compositFormatString, date.Year, date.Month, date.Day) : null;
    }
}

上面的示例中,第一个case语句使用类型模式匹配来判断输入值是否为DateTime类型。如果判断值为true,则进而使用属性模式匹配来声明year、month和day变量并赋值;最后将这些变量包装为一个元组(year, month, day)并返回。DateTimeOffet类型的case用相同的方法处理。

string类型的case语句并没有使用模式识别,而是使用了前面章节里介绍过的TryParse方法。如果该方法调用不成功,则返回一个default((int Year, int Month, int Day)?),这将产生一个该类型的null值。

7.7 避免对多态类对象使用模式匹配

不过,多态(继承、封装、重写)也不总是能够更好地解决问题。当类的继承体系无法满足程序要求时,多态便无法运作。例如,如果需要处理来自不相关系统中的多个类,则无法将它们整合在同一个继承体系中,并用多态来解决问题。此外,假如正在使用来自第三方的,无法被修改的代码,则很可能也无法利用多态来重写代码的行为。

高级主题:使用as操作符进行转换

•使用as操作符可避免用额外的try-catch代码处理转换无效的情况,因为as操作符提供了尝试执行转型但转型失败后不抛出异常的一种方式。

•is操作符的优点是允许验证一个数据项是否属于特定类型。as操作符则更进一步,它会像一次转型所做的那样,尝试将对象转换为特定数据类型。但和转型不同的是,如对象不能转换,as操作符会返回null,这一点相当重要,因为它避免了因为转型而造成的异常。
•is操作符相较于as操作符的一个优点是后者不能成功判断基础类型。as能在继承链中向上或向下隐式转型,也提供了支持转型操作符的类型。但as不能判断基础类型。
•更重要的是,as操作符一般要求采取额外的步骤对被赋值的变量执行空检查。由于模式匹配is操作符自动包含该检查,所以几乎再也用不着as操作符了。

  • 16
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值