使用英语阅读

通过


C# 和 .NET 中的继承

本教程介绍如何在 C# 中继承。 继承是面向对象的编程语言的一项功能,可用于定义提供特定功能的基类(数据和行为),并定义继承或替代该功能的派生类。

先决条件

安装说明

在 Windows 上,使用此 WinGet 配置文件 来安装所有必备组件。 如果已安装某些内容,WinGet 将跳过此步骤。

  1. 下载该文件,然后双击以运行它。
  2. 阅读许可协议,键入 y,并在系统提示接受时按下 Enter
  3. 请在 Windows 任务栏中查找闪烁的用户帐户控制(UAC)提示符,可能需要管理员级权限才能安装。

在其他平台上,需要单独安装其中每个组件。

  1. .NET SDK 下载页面下载建议的安装程序,然后双击以运行它。 下载页可检测平台,并推荐平台的最新安装程序。
  2. Visual Studio Code 主页下载最新的安装程序,然后双击以运行它。 该页还会检测平台,并且链接应该适合你的系统。
  3. 单击 C# DevKit 扩展页上的“安装”按钮。 这将打开 Visual Studio 代码,并询问是否要安装或启用扩展。 选择“安装”。

运行示例

若要在本教程中创建并运行示例,请使用命令行中的 dotnet 实用工具。 针对每个示例,请按照以下步骤作:

  1. 创建用于存储示例的目录。

  2. 在命令提示符处输入 dotnet new console 命令以创建新的 .NET Core 项目。

  3. 将示例中的代码复制并粘贴到代码编辑器中。

  4. 从命令行输入 dotnet restore 命令以加载或还原项目的依赖项。

    无需运行 dotnet restore,因为它由所有需要还原的命令隐式运行,如 dotnet newdotnet builddotnet rundotnet testdotnet publishdotnet pack。 若要禁用隐式还原,请使用 --no-restore 选项。

    在某些显式还原有意义的场景中,dotnet restore 命令仍然有用,比如在 Azure DevOps Services 中进行持续集成构建时,或在需要显式控制还原发生时间的生成系统中。

    有关如何管理 NuGet 源的信息,请参阅 dotnet restore 文档

  5. 输入 dotnet run 命令以编译和执行示例。

背景:什么是继承?

继承 是面向对象的编程的基本属性之一。 它允许你定义可重用(继承)、扩展或修改父类行为的子类。 其成员继承的类称为 基类。 继承基类成员的类称为 派生类

C# 和 .NET 只支持单一继承。 也就是说,类只能继承自单个类。 但是,继承是可传递的,它允许为一组类型定义继承层次结构。 换句话说,类型 D 可以继承自类型 C,该类型继承自类型 B,该类型继承自基类类型 A。 由于继承是可传递的,因此类型 A 的成员可用于类型 D

并非基类的所有成员都由派生类继承。 以下成员未被继承:

  • 静态构造函数,用于初始化类的静态数据。

  • 实例构造函数,调用该构造函数以创建新类实例。 每个类必须定义其自己的构造函数。

  • 终结器:由运行时的垃圾回收器调用,用于销毁类实例。

虽然基类的所有其他成员都由派生类继承,但它们是否可见取决于它们的可访问性。 一个成员的可访问性会影响其在派生类中的可见性,具体如下:

  • 只有在基类中嵌套的派生类中,私有成员才可见。 否则,此类成员在派生类中不可见。 在以下示例中,A.B 是派生自 A的嵌套类,C 派生自 A。 专用 A._value 字段在 A.B 中可见。但是,如果从 C.GetValue 方法中删除注释并尝试编译该示例,则会生成编译器错误 CS0122:“由于其保护级别,”'A._value'不可访问”。

    C#
    public class A
    {
        private int _value = 10;
    
        public class B : A
        {
            public int GetValue()
            {
                return _value;
            }
        }
    }
    
    public class C : A
    {
        //    public int GetValue()
        //    {
        //        return _value;
        //    }
    }
    
    public class AccessExample
    {
        public static void Main(string[] args)
        {
            var b = new A.B();
            Console.WriteLine(b.GetValue());
        }
    }
    // The example displays the following output:
    //       10
    
  • 受保护成员仅在派生类中可见。

  • 内部成员仅在与基类同属一个程序集的派生类中可见, 在与基类属于不同程序集的派生类中不可见。

  • 公共成员在派生类中可见,并且属于派生类的公共接口。 可以调用继承的公共成员,就像它们是在派生类中定义一样。 在下面的示例中,类 A 定义一个名为 Method1的方法,类 B 继承自类 A。 然后,该示例调用 Method1,就好像它是 B上的实例方法。

    C#
    public class A
    {
        public void Method1()
        {
            // Method implementation.
        }
    }
    
    public class B : A
    { }
    
    public class Example
    {
        public static void Main()
        {
            B b = new ();
            b.Method1();
        }
    }
    

派生类还可以通过提供重写实现代码来重写继承的成员。 为了能够替代成员,基类中的成员必须用 虚拟 关键字进行标记。 默认情况下,基类成员不会被标记为 virtual,也无法被重写。 尝试替代非虚拟成员,如以下示例所示,将生成编译器错误 CS0506:“<成员> 无法替代继承的成员 <成员>,因为它未标记为虚拟、抽象或重写。

C#
public class A
{
    public void Method1()
    {
        // Do something.
    }
}

public class B : A
{
    public override void Method1() // Generates CS0506.
    {
        // Do something else.
    }
}

在某些情况下,派生类必须重写基类实现代码。 标记有 abstract 关键字的基类成员要求派生类必须重写它们。 尝试编译以下示例将生成编译器错误 CS0534“<类> 不实现继承的抽象成员 <成员>”,因为类 B 不提供 A.Method1的实现。

C#
public abstract class A
{
    public abstract void Method1();
}

public class B : A // Generates CS0534.
{
    public void Method3()
    {
        // Do something.
    }
}

继承仅适用于类和接口。 其他类型类别(结构、委托和枚举)不支持继承。 由于这些规则,尝试编译类似于以下示例的代码将生成编译器错误 CS0527:“接口列表中的类型'ValueType'不是接口。错误消息指示,尽管可以定义结构实现的接口,但不支持继承。

C#
public struct ValueStructure : ValueType // Generates CS0527.
{
}

隐式继承

在 .NET 类型系统中,除了它们可能通过单一继承获得的任何类型外,所有类型都隐式继承自 Object 或其派生类型。 Object 的常见功能可用于任何类型。

若要查看隐式继承的含义,让我们定义一个新类,SimpleClass,这只是一个空类定义:

C#
public class SimpleClass
{ }

然后,可以使用反射(这允许检查类型的元数据以获取有关该类型的信息)来获取属于 SimpleClass 类型的成员的列表。 虽然尚未在 SimpleClass 类中定义任何成员,但示例中的输出指示它实际上有 9 个成员。 其中一个成员是 C# 编译器为 SimpleClass 类型自动提供的无参数(或默认)构造函数。 其余八个是 Object的成员,这是 .NET 类型系统中所有类和接口最终隐式继承的类型。

C#
using System.Reflection;

public class SimpleClassExample
{
    public static void Main()
    {
        Type t = typeof(SimpleClass);
        BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
                             BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
        MemberInfo[] members = t.GetMembers(flags);
        Console.WriteLine($"Type {t.Name} has {members.Length} members: ");
        foreach (MemberInfo member in members)
        {
            string access = "";
            string stat = "";
            var method = member as MethodBase;
            if (method != null)
            {
                if (method.IsPublic)
                    access = " Public";
                else if (method.IsPrivate)
                    access = " Private";
                else if (method.IsFamily)
                    access = " Protected";
                else if (method.IsAssembly)
                    access = " Internal";
                else if (method.IsFamilyOrAssembly)
                    access = " Protected Internal ";
                if (method.IsStatic)
                    stat = " Static";
            }
            string output = $"{member.Name} ({member.MemberType}): {access}{stat}, Declared by {member.DeclaringType}";
            Console.WriteLine(output);
        }
    }
}
// The example displays the following output:
//	Type SimpleClass has 9 members:
//	ToString (Method):  Public, Declared by System.Object
//	Equals (Method):  Public, Declared by System.Object
//	Equals (Method):  Public Static, Declared by System.Object
//	ReferenceEquals (Method):  Public Static, Declared by System.Object
//	GetHashCode (Method):  Public, Declared by System.Object
//	GetType (Method):  Public, Declared by System.Object
//	Finalize (Method):  Internal, Declared by System.Object
//	MemberwiseClone (Method):  Internal, Declared by System.Object
//	.ctor (Constructor):  Public, Declared by SimpleClass

Object 类的隐式继承使这些方法可用于 SimpleClass 类:

  • SimpleClass 对象转换为其字符串表示形式的公共 ToString 方法返回完全限定的类型名称。 在这种情况下,ToString 方法返回字符串“SimpleClass”。

  • 测试两个对象的相等性的三种方法:公共实例 Equals(Object) 方法、公共静态 Equals(Object, Object) 方法和公共静态 ReferenceEquals(Object, Object) 方法。 默认情况下,这些方法测试引用相等性;也就是说,要相等,两个对象变量必须引用同一对象。

  • 公共 GetHashCode 方法,它计算一个值,使得类型的实例可以在哈希集合中使用。

  • 公共 GetType 方法,该方法返回表示 SimpleClass 类型的 Type 对象。

  • 受保护的 Finalize 方法,用于在垃圾回收器回收对象的内存之前释放非托管资源。

  • 受保护的 MemberwiseClone 方法,该方法创建当前对象的浅表克隆。

由于隐式继承,可以从 SimpleClass 对象调用任何继承的成员,就好像它实际上是 SimpleClass 类中定义的成员一样。 例如,以下示例调用 SimpleClass.ToString 方法,该方法 SimpleClass 继承自 Object

C#
public class EmptyClass
{ }

public class ClassNameExample
{
    public static void Main()
    {
        EmptyClass sc = new();
        Console.WriteLine(sc.ToString());
    }
}
// The example displays the following output:
//        EmptyClass

下表列出了可以在 C# 中创建的类型类别及其隐式继承的类型。 每个基类型通过继承向隐式派生类型提供一组不同的成员。

类型类别 隐式继承自
class Object
结构体 ValueTypeObject
enum EnumValueType、、 Object
delegate MulticastDelegateDelegate、、 Object

继承和“is a”关系

通常,继承用于表示基类与一个或多个派生类之间的“是”关系,其中派生类是基类的专用版本;派生类是基类的类型。 例如,Publication 类表示任何类型的发布,BookMagazine 类表示特定类型的发布。

备注

类或结构可以实现一个或多个接口。 虽然接口实现通常显示为单个继承的解决方法,或者作为一种将继承与结构一起使用的方式,但它旨在表达接口与其实现类型之间的不同关系(“可以执行”关系),而不是继承。 接口定义了一部分功能(例如测试相等性、比较或排序对象的能力,或支持文化相关的解析和格式化),这些功能可以由实现该接口的类型使用。

请注意,“is a”还表示类型与该类型的特定实例化之间的关系。 在下面的示例中,Automobile 是一个具有三个唯一只读属性的类:Make,汽车制造商;Model,汽车类型:和 Year,它的制造年。 Automobile 类还有一个自变量被分配给属性值的构造函数,并将 Object.ToString 方法重写为生成唯一标识 Automobile 实例(而不是 Automobile 类)的字符串。

C#
public class Automobile
{
    public Automobile(string make, string model, int year)
    {
        if (make == null)
            throw new ArgumentNullException(nameof(make), "The make cannot be null.");
        else if (string.IsNullOrWhiteSpace(make))
            throw new ArgumentException("make cannot be an empty string or have space characters only.");
        Make = make;

        if (model == null)
            throw new ArgumentNullException(nameof(model), "The model cannot be null.");
        else if (string.IsNullOrWhiteSpace(model))
            throw new ArgumentException("model cannot be an empty string or have space characters only.");
        Model = model;

        if (year < 1857 || year > DateTime.Now.Year + 2)
            throw new ArgumentException("The year is out of range.");
        Year = year;
    }

    public string Make { get; }

    public string Model { get; }

    public int Year { get; }

    public override string ToString() => $"{Year} {Make} {Model}";
}

在这种情况下,不应依赖继承来表示具体的汽车品牌和型号。 例如,无需定义 Packard 类型来表示 Packard 汽车公司制造的汽车。 相反,可以通过使用传递给其类构造函数的相应值创建一个 Automobile 对象来表示它们,如以下示例所示。

C#
using System;

public class Example
{
    public static void Main()
    {
        var packard = new Automobile("Packard", "Custom Eight", 1948);
        Console.WriteLine(packard);
    }
}
// The example displays the following output:
//        1948 Packard Custom Eight

基于继承的 is-a 关系最适合应用于基类和派生类,这些类将附加成员添加到基类或需要基类中不存在的其他功能。

设计基类和派生类

让我们看看设计基类及其派生类的过程。 在本部分中,你将定义一个基类,Publication,它表示任何类型的出版物,如书籍、杂志、报纸、日记、文章等。你还将定义派生自 PublicationBook 类。 可以轻松扩展示例以定义其他派生类,例如 MagazineJournalNewspaperArticle

Publication 基类

设计 Publication 类时,需要做出下面几项设计决策:

  • 要确定在基类 Publication 中包含哪些成员,以及Publication 成员是否提供方法实现,或者Publication 是否是用作其派生类模板的抽象基类。

    在这种情况下,Publication 类将提供方法实现。 设计抽象基类及其派生类部分中的示例就使用抽象基类定义派生类必须重写的方法。 派生类可以自由提供适合派生类型的任何实现。

    重用代码(即多个派生类共享基类方法的声明和实现,不需要重写它们)是非抽象基类的优点。 因此,如果成员的代码可能由某些或大多数专用 Publication 类型共享,则应将其添加到 Publication。 如果无法有效地提供基类实现,最终必须在派生类中提供基本相同的成员实现,而不是基类中的单个实现。 需要在多个位置维护重复的代码是一个潜在的 bug 源。

    为了最大化代码重用和创建逻辑且直观的继承层次结构,你需要确保 Publication 类中只包含所有或大多数出版物通用的数据和功能。 然后,派生类可以实现所表示的特定出版物种类的唯一成员。

  • 类层次结构的扩展空间大小。 是否要开发三个或多个类的层次结构,而不仅仅是基类和一个或多个派生类? 例如,Publication 可以是 Periodical的基类,而 Periodical又是 MagazineJournalNewspaper的基类。

    对于示例,你将使用 Publication 类的简易层次结构和单个派生类 Book。 可以轻松扩展示例,以创建派生自 Publication的其他类,例如 MagazineArticle

  • 实例化基类是否有意义。 如果不可以,则应向类应用 abstract 关键字。 否则,您可以通过调用其类构造函数来实例化 Publication 类。 如果尝试通过直接调用其类构造函数来实例化用 abstract 关键字标记的类,则 C# 编译器将生成错误 CS0144,“无法创建抽象类或接口的实例”。如果尝试使用反射实例化类,反射方法将引发 MemberAccessException

    默认情况下,可以通过调用基类构造函数来实例化基类。 无需显式定义类构造函数。 如果基类的源代码中不存在一个,C# 编译器会自动提供默认的(无参数)构造函数。

    在此示例中,将把 Publication 类标记为 abstract,使其无法实例化。 没有任何 abstract 方法的 abstract 类表示在多个具体类(如 BookJournal)之间共享的抽象概念。

  • 派生类是否必须继承特定成员的基类实现、是否有权替代基类实现,还是必须提供实现。 使用 abstract 关键字来强制派生类提供实现代码。 使用 virtual 关键字来允许派生类重写基类方法。 默认情况下,可重写基类中定义的方法。

    Publication 类不具备任何 abstract 方法,不过类本身是 abstract

  • 派生类是否表示继承层次结构中的最终类,不能将其本身用作其他派生类的基类。 默认情况下,任何类都可以用作基类。 可以应用 密封 关键字来表明某个类不能作为其他类的基类。 尝试从密封类派生会导致编译器错误 CS0509:“无法从密封类型 <typeName>派生。”

    在此示例中,将把派生类标记为 sealed

以下示例显示了 Publication 类的源代码,以及 Publication.PublicationType 属性返回的 PublicationType 枚举。 除了继承自 Object 的成员之外,Publication 类还定义了以下唯一成员和成员重写:

C#

public enum PublicationType { Misc, Book, Magazine, Article };

public abstract class Publication
{
    private bool _published = false;
    private DateTime _datePublished;
    private int _totalPages;

    public Publication(string title, string publisher, PublicationType type)
    {
        if (string.IsNullOrWhiteSpace(publisher))
            throw new ArgumentException("The publisher is required.");
        Publisher = publisher;

        if (string.IsNullOrWhiteSpace(title))
            throw new ArgumentException("The title is required.");
        Title = title;

        Type = type;
    }

    public string Publisher { get; }

    public string Title { get; }

    public PublicationType Type { get; }

    public string? CopyrightName { get; private set; }

    public int CopyrightDate { get; private set; }

    public int Pages
    {
        get { return _totalPages; }
        set
        {
            if (value <= 0)
                throw new ArgumentOutOfRangeException(nameof(value), "The number of pages cannot be zero or negative.");
            _totalPages = value;
        }
    }

    public string GetPublicationDate()
    {
        if (!_published)
            return "NYP";
        else
            return _datePublished.ToString("d");
    }

    public void Publish(DateTime datePublished)
    {
        _published = true;
        _datePublished = datePublished;
    }

    public void Copyright(string copyrightName, int copyrightDate)
    {
        if (string.IsNullOrWhiteSpace(copyrightName))
            throw new ArgumentException("The name of the copyright holder is required.");
        CopyrightName = copyrightName;

        int currentYear = DateTime.Now.Year;
        if (copyrightDate < currentYear - 10 || copyrightDate > currentYear + 2)
            throw new ArgumentOutOfRangeException($"The copyright year must be between {currentYear - 10} and {currentYear + 1}");
        CopyrightDate = copyrightDate;
    }

    public override string ToString() => Title;
}
  • 构造函数

    由于 Publicationabstract,因此无法直接从代码实例化它,如以下示例所示:

    C#
    var publication = new Publication("Tiddlywinks for Experts", "Fun and Games",
                                      PublicationType.Book);
    

    但是,其实例构造函数可以直接从派生类构造函数调用,如 Book 类的源代码所示。

  • 两个与出版物相关的属性

    Title 是一个只读 String 属性,其值是通过调用 Publication 构造函数提供的。

    Pages 是一个读写 Int32 属性,指示出版物的总页数。 该值存储在名为 totalPages的专用字段中。 值必须为正数,否则会抛出 ArgumentOutOfRangeException

  • 与出版商相关的成员

    两个只读属性,PublisherType。 这些值最初由对 Publication 类构造函数的调用提供。

  • 与出版相关的成员

    两种方法,PublishGetPublicationDate,设置并返回发布日期。 Publish 方法在调用时将私有 published 标志设置为 true,并将传递给它的日期赋值给私有 datePublished 字段。 如果 published 标志 false,则 GetPublicationDate 方法返回字符串“NYP”,如果 true,则返回 datePublished 字段的值。

  • 与版权相关的成员

    Copyright 方法采用版权持有者的姓名和版权年份作为参数,并将其分配给 CopyrightNameCopyrightDate 属性。

  • 重写 ToString 方法

    如果某个类型没有重写 Object.ToString 方法,它返回该类型的完全限定名称,这在区分不同实例时几乎没有用处。 Publication 类将 Object.ToString 重写为返回 Title 属性值。

下图说明了基础 Publication 类与其隐式继承的 Object 类之间的关系。

对象类和发布类

Book

Book 类将书籍表示为一种特殊类型的出版物。 以下示例显示了 Book 类的源代码。

C#
using System;

public sealed class Book : Publication
{
    public Book(string title, string author, string publisher) :
           this(title, string.Empty, author, publisher)
    { }

    public Book(string title, string isbn, string author, string publisher) : base(title, publisher, PublicationType.Book)
    {
        // isbn argument must be a 10- or 13-character numeric string without "-" characters.
        // We could also determine whether the ISBN is valid by comparing its checksum digit
        // with a computed checksum.
        //
        if (!string.IsNullOrEmpty(isbn))
        {
            // Determine if ISBN length is correct.
            if (!(isbn.Length == 10 | isbn.Length == 13))
                throw new ArgumentException("The ISBN must be a 10- or 13-character numeric string.");
            if (!ulong.TryParse(isbn, out _))
                throw new ArgumentException("The ISBN can consist of numeric characters only.");
        }
        ISBN = isbn;

        Author = author;
    }

    public string ISBN { get; }

    public string Author { get; }

    public decimal Price { get; private set; }

    // A three-digit ISO currency symbol.
    public string? Currency { get; private set; }

    // Returns the old price, and sets a new price.
    public decimal SetPrice(decimal price, string currency)
    {
        if (price < 0)
            throw new ArgumentOutOfRangeException(nameof(price), "The price cannot be negative.");
        decimal oldValue = Price;
        Price = price;

        if (currency.Length != 3)
            throw new ArgumentException("The ISO currency symbol is a 3-character string.");
        Currency = currency;

        return oldValue;
    }

    public override bool Equals(object? obj)
    {
        if (obj is not Book book)
            return false;
        else
            return ISBN == book.ISBN;
    }

    public override int GetHashCode() => ISBN.GetHashCode();

    public override string ToString() => $"{(string.IsNullOrEmpty(Author) ? "" : Author + ", ")}{Title}";
}

除了继承自 Publication 的成员之外,Book 类还定义了以下唯一成员和成员重写:

  • 两个构造函数

    这两个 Book 构造函数共享三个通用参数。 其中两个参数(titlepublisher)对应于 Publication 构造函数的相应参数。 第三个参数是 author,存储在不可变的 Author 属性中。 一个构造函数包括 isbn 参数,该参数存储在 ISBN 自动属性中。

    第一个构造函数使用 这个 关键字来调用另一个构造函数。 构造函数链接是定义构造函数的常见模式。 调用具有最大参数数量的构造函数时,具有较少参数的构造函数提供默认值。

    第二个构造函数使用 base 关键字将标题和发布者名称传递给基类构造函数。 如果不在源代码中显式调用基类构造函数,C# 编译器会自动提供对基类的默认或无参数构造函数的调用。

  • 只读 ISBN 属性,返回 Book 对象的国际标准书号(唯一的 10 位或 13 位数字)。 ISBN 作为 Book 构造函数之一的参数提供。 ISBN 存储在由编译器自动生成的专用后盾字段中。

  • 只读 Author 属性。 作者名称作为参数提供给 Book 构造函数,并存储在属性中。

  • 两个与价格相关的只读属性(PriceCurrency)。 它们在 SetPrice 方法调用中作为参数提供。 Currency 属性是三位数 ISO 货币符号(例如美元)。 可以从 ISOCurrencySymbol 属性检索 ISO 货币符号。 这两个属性都是外部只读的,但两者都可以由 Book 类中的代码设置。

  • SetPrice 方法,用于设置 PriceCurrency 属性的值。 这些值由这些相同的属性返回。

  • 重写 ToString 方法(继承自 Publication)、Object.Equals(Object)GetHashCode 方法(继承自 Object)。

    除非重写,否则 Object.Equals(Object) 方法测试的是引用相等性。 也就是说,如果两个对象变量引用同一对象,则它们被视为相等。 另一方面,在 Book 类中,如果两个 Book 对象具有相同的 ISBN,则应相等。

    重写 Object.Equals(Object) 方法时,还必须重写 GetHashCode 方法,该方法返回运行时用来将项存储在哈希集合中的值,以便高效检索。 哈希代码应返回与测试相等性一致的值。 由于已将 Object.Equals(Object) 重写为在两个 Book 对象的 ISBN 属性相等时返回 true,因此返回的哈希代码是通过调用 ISBN 属性返回的字符串的 GetHashCode 方法计算得出。

下图说明了 Book 类与其基类 Publication之间的关系。

出版物和书籍类

现在可以实例化 Book 对象,同时调用其唯一成员和继承成员,并将其作为参数传递给需要 Publication 或类型 Book类型的参数的方法,如以下示例所示。

C#
public class ClassExample
{
    public static void Main()
    {
        var book = new Book("The Tempest", "0971655819", "Shakespeare, William",
                            "Public Domain Press");
        ShowPublicationInfo(book);
        book.Publish(new DateTime(2016, 8, 18));
        ShowPublicationInfo(book);

        var book2 = new Book("The Tempest", "Classic Works Press", "Shakespeare, William");
        Console.Write($"{book.Title} and {book2.Title} are the same publication: " +
              $"{((Publication)book).Equals(book2)}");
    }

    public static void ShowPublicationInfo(Publication pub)
    {
        string pubDate = pub.GetPublicationDate();
        Console.WriteLine($"{pub.Title}, " +
                  $"{(pubDate == "NYP" ? "Not Yet Published" : "published on " + pubDate):d} by {pub.Publisher}");
    }
}
// The example displays the following output:
//        The Tempest, Not Yet Published by Public Domain Press
//        The Tempest, published on 8/18/2016 by Public Domain Press
//        The Tempest and The Tempest are the same publication: False

设计抽象基类及其派生类

在前面的示例中,你定义了一个基类,该基类为许多方法提供了实现,以允许派生类共享代码。 然而,在许多情况下,基类通常不被期望提供实现。 相反,基类是一个 抽象类,它声明 抽象方法;它充当一个模板,用于定义每个派生类必须实现的成员。 通常,在抽象基类中,每个派生类型的实现对该类型是唯一的。 你用抽象关键字标记了该类,因为实例化 Publication 对象毫无意义,尽管该类确实提供了出版物共有功能的实现。

例如,每个封闭的二维几何形状包括两个属性:区域、形状的内部盘区;和外围,或沿形状边缘的距离。 但是,计算这些属性的方式完全取决于特定形状。 例如,用于计算圆的外围(或周长)的公式与正方形的边界不同。 Shape 类是具有 abstract 方法的 abstract 类。 这表示派生类共享相同的功能,但这些派生类以不同的方式实现该功能。

以下示例定义一个名为 Shape 的抽象基类,该类定义两个属性:AreaPerimeter。 除了使用 抽象 关键字标记类之外,每个实例成员还使用 抽象 关键字进行标记。 在此示例中,Shape 还将 Object.ToString 方法重写为返回类型的名称,而不是其完全限定的名称。 它定义了两个静态成员,GetAreaGetPerimeter,允许调用方轻松检索任何派生类实例的区域和外围。 将派生类实例传递给两个方法中的任意一个时,运行时调用的是派生类重写的方法。

C#
public abstract class Shape
{
    public abstract double Area { get; }

    public abstract double Perimeter { get; }

    public override string ToString() => GetType().Name;

    public static double GetArea(Shape shape) => shape.Area;

    public static double GetPerimeter(Shape shape) => shape.Perimeter;
}

然后,可以从表示特定形状的 Shape 派生某些类。 以下示例定义三个类:SquareRectangleCircle。 每个公式都使用该特定形状唯一的公式来计算区域和外围。 某些派生类还定义属性,如 Rectangle.DiagonalCircle.Diameter,这些属性对于它们所表示的形状是唯一的。

C#
using System;

public class Square : Shape
{
    public Square(double length)
    {
        Side = length;
    }

    public double Side { get; }

    public override double Area => Math.Pow(Side, 2);

    public override double Perimeter => Side * 4;

    public double Diagonal => Math.Round(Math.Sqrt(2) * Side, 2);
}

public class Rectangle : Shape
{
    public Rectangle(double length, double width)
    {
        Length = length;
        Width = width;
    }

    public double Length { get; }

    public double Width { get; }

    public override double Area => Length * Width;

    public override double Perimeter => 2 * Length + 2 * Width;

    public bool IsSquare() => Length == Width;

    public double Diagonal => Math.Round(Math.Sqrt(Math.Pow(Length, 2) + Math.Pow(Width, 2)), 2);
}

public class Circle : Shape
{
    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double Area => Math.Round(Math.PI * Math.Pow(Radius, 2), 2);

    public override double Perimeter => Math.Round(Math.PI * 2 * Radius, 2);

    // Define a circumference, since it's the more familiar term.
    public double Circumference => Perimeter;

    public double Radius { get; }

    public double Diameter => Radius * 2;
}

以下示例使用派生自 Shape的对象。 它实例化派生自 Shape 的一组对象,然后调用 Shape 类的静态方法,用于包装返回的 Shape 属性值。 运行时从派生类型的重写属性检索值。 该示例还会将数组中的每个 Shape 对象强制转换为其派生类型,如果强制转换成功,则检索该特定子类 Shape的属性。

C#
using System;

public class Example
{
    public static void Main()
    {
        Shape[] shapes = { new Rectangle(10, 12), new Square(5),
                    new Circle(3) };
        foreach (Shape shape in shapes)
        {
            Console.WriteLine($"{shape}: area, {Shape.GetArea(shape)}; " +
                              $"perimeter, {Shape.GetPerimeter(shape)}");
            if (shape is Rectangle rect)
            {
                Console.WriteLine($"   Is Square: {rect.IsSquare()}, Diagonal: {rect.Diagonal}");
                continue;
            }
            if (shape is Square sq)
            {
                Console.WriteLine($"   Diagonal: {sq.Diagonal}");
                continue;
            }
        }
    }
}
// The example displays the following output:
//         Rectangle: area, 120; perimeter, 44
//            Is Square: False, Diagonal: 15.62
//         Square: area, 25; perimeter, 20
//            Diagonal: 7.07
//         Circle: area, 28.27; perimeter, 18.85

其他资源

文档

培训

模块

实现类继承 - Training

了解如何使用基类和派生类创建类层次结构,以及如何使用“new”、“virtual”、“abstract”和“override”关键字隐藏或替代派生类的成员。