C# 和 .NET 中的继承Inheritance in C# and .NET

此教程将介绍 C# 中的继承。This tutorial introduces you to inheritance in C#. 继承是面向对象的编程语言的一项功能,可方便你定义提供特定功能(数据和行为)的基类,并定义继承或重写此功能的派生类。Inheritance is a feature of object-oriented programming languages that allows you to define a base class that provides specific functionality (data and behavior) and to define derived classes that either inherit or override that functionality.

系统必备Prerequisites

本教程假定你已安装 .NET Core SDK。This tutorial assumes that you've installed the .NET Core SDK. 请访问 .NET Core 下载页进行下载。Visit the .NET Core Downloads page to download it. 还需要安装代码编辑器。You also need a code editor. 此教程使用 Visual Studio Code,但你可以选择使用任何代码编辑器。This tutorial uses Visual Studio Code, although you can use any code editor of your choice.

运行示例Running the examples

若要创建并运行此教程中的示例,请通过命令行使用 dotnet 实用工具。To create and run the examples in this tutorial, you use the dotnet utility from the command line. 对于每个示例,请按照以下步骤操作:Follow these steps for each example:

  1. 创建用于存储示例的目录。Create a directory to store the example.
  2. 在命令提示符处,输入 dotnet new console 命令,新建 .NET Core 项目。Enter the dotnet new console command at a command prompt to create a new .NET Core project.
  3. 将示例中的代码复制并粘贴到代码编辑器中。Copy and paste the code from the example into your code editor.
  4. 在命令行处输入 dotnet restore 命令,加载或还原项目的依赖项。Enter the dotnet restore command from the command line to load or restore the project's dependencies.

备注

从 .NET Core 2.0 SDK 开始,无需运行 dotnet restore,因为它由所有需要还原的命令隐式运行,如 dotnet newdotnet builddotnet runStarting with .NET Core 2.0 SDK, you don't have to run dotnet restore because it's run implicitly by all commands that require a restore to occur, such as dotnet new, dotnet build and dotnet run. 在执行显式还原有意义的某些情况下,例如 Azure DevOps Services 中的持续集成生成中,或在需要显式控制还原发生时间的生成系统中,它仍然是有效的命令。It's still a valid command in certain scenarios where doing an explicit restore makes sense, such as continuous integration builds in Azure DevOps Services or in build systems that need to explicitly control the time at which the restore occurs.

  1. 输入 dotnet run 命令,编译并执行示例。Enter the dotnet run command to compile and execute the example.

背景:什么是继承?Background: What is inheritance?

继承是面向对象的编程的一种基本特性。Inheritance is one of the fundamental attributes of object-oriented programming. 借助继承,能够定义可重用(继承)、扩展或修改父类行为的子类。It allows you to define a child class that reuses (inherits), extends, or modifies the behavior of a parent class. 成员被继承的类称为基类The class whose members are inherited is called the base class. 继承基类成员的类称为派生类The class that inherits the members of the base class is called the derived class.

C# 和 .NET 只支持单一继承C# and .NET support single inheritance only. 也就是说,类只能继承自一个类。That is, a class can only inherit from a single class. 不过,继承是可传递的。这样一来,就可以为一组类型定义继承层次结构。However, inheritance is transitive, which allows you to define an inheritance hierarchy for a set of types. 换言之,类型 D 可继承自类型 C,其中类型 C 继承自类型 B,类型 B 又继承自基类类型 AIn other words, type D can inherit from type C, which inherits from type B, which inherits from the base class type A. 由于继承是可传递的,因此类型 D 继承了类型 A 的成员。Because inheritance is transitive, the members of type A are available to type D.

并非所有基类成员都可供派生类继承。Not all members of a base class are inherited by derived classes. 以下成员无法继承:The following members are not inherited:

  • 静态构造函数:用于初始化类的静态数据。Static constructors, which initialize the static data of a class.

  • 实例构造函数:在创建类的新实例时调用。Instance constructors, which you call to create a new instance of the class. 每个类都必须定义自己的构造函数。Each class must define its own constructors.

  • 终结器:由运行时的垃圾回收器调用,用于销毁类实例。Finalizers, which are called by the runtime's garbage collector to destroy instances of a class.

虽然基类的其他所有成员都可供派生类继承,但这些成员是否可见取决于它们的可访问性。While all other members of a base class are inherited by derived classes, whether they are visible or not depends on their accessibility. 成员的可访问性决定了其是否在派生类中可见,如下所述:A member's accessibility affects its visibility for derived classes as follows:

  • 只有在基类中嵌套的派生类中,私有成员才可见。Private members are visible only in derived classes that are nested in their base class. 否则,此类成员在派生类中不可见。Otherwise, they are not visible in derived classes. 在以下示例中,A.B 是派生自 A 的嵌套类,而 C 则派生自 AIn the following example, A.B is a nested class that derives from A, and C derives from A. 私有 A.value 字段在 A.B 中可见。The private A.value field is visible in A.B. 不过,如果从 C.GetValue 方法中删除注释并尝试编译示例,则会生成编译器错误 CS0122:“'A.value' 不可访问,因为它具有一定的保护级别。”However, if you remove the comments from the C.GetValue method and attempt to compile the example, it produces compiler error CS0122: "'A.value' is inaccessible due to its protection level."

    using System;
    
    public class A 
    {
       private int value = 10;
    
       public class B : A
       {
           public int GetValue()
           {
               return this.value;
           }     
       }
    }
    
    public class C : A
    {
    //    public int GetValue()
    //    {
    //        return this.value;
    //    }
    }
    
    public class Example
    {
        public static void Main(string[] args)
        {
            var b = new A.B();
            Console.WriteLine(b.GetValue());
        }
    }
    // The example displays the following output:
    //       10
    
  • 受保护成员仅在派生类中可见。Protected members are visible only in derived classes.

  • 内部成员仅在与基类同属一个程序集的派生类中可见,Internal members are visible only in derived classes that are located in the same assembly as the base class. 在与基类属于不同程序集的派生类中不可见。They are not visible in derived classes located in a different assembly from the base class.

  • 公共成员在派生类中可见,并且属于派生类的公共接口。Public members are visible in derived classes and are part of the derived class' public interface. 可以调用继承的公共成员,就像它们是在派生类中定义一样。Public inherited members can be called just as if they are defined in the derived class. 在以下示例中,类 A 定义 Method1 方法,类 B 继承自类 AIn the following example, class A defines a method named Method1, and class B inherits from class A. 然后,以下示例调用 Method1,就像它是 B 中的实例方法一样。The example then calls Method1 as if it were an instance method on B.

public class A
{
    public void Method1()
    {
        // Method implementation.
    }
}

public class B : A
{ }

public class Example
{
    public static void Main()
    {
        B b = new B();
        b.Method1();
    }
}

派生类还可以通过提供重写实现代码来重写继承的成员。Derived classes can also override inherited members by providing an alternate implementation. 基类成员必须标记有 virtual 关键字,才能重写继承的成员。In order to be able to override a member, the member in the base class must be marked with the virtual keyword. 默认情况下,基类成员没有 virtual 标记,因此无法被重写。By default, base class members are not marked as virtual and cannot be overridden. 如果尝试重写非虚成员(如以下示例所示),则会生成编译器错误 CS0506:“<member> 无法重写继承的成员 <member>,因为继承的成员没有 virtual、abstract 或 override 标记。”Attempting to override a non-virtual member, as the following example does, generates compiler error CS0506: "<member> cannot override inherited member <member> because it is not marked virtual, abstract, or override.

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

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

在某些情况下,派生类必须重写基类实现代码。In some cases, a derived class must override the base class implementation. 标记有 abstract 关键字的基类成员要求派生类必须重写它们。Base class members marked with the abstract keyword require that derived classes override them. 如果尝试编译以下示例,则会生成编译器错误 CS0534:“<class> 不实现继承的抽象成员 <member>”,因为类 B 没有提供 A.Method1 的实现代码。Attempting to compile the following example generates compiler error CS0534, "<class> does not implement inherited abstract member <member>", because class B provides no implementation for A.Method1.

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

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

继承仅适用于类和接口。Inheritance applies only to classes and interfaces. 其他各种类型(结构、委托和枚举)均不支持继承。Other type categories (structs, delegates, and enums) do not support inheritance. 因为这些规则的存在,如果尝试编译以下代码,则会生成编译器错误 CS0527:“接口列表中的类型“ValueType”不是接口。”Because of these rules, attempting to compile code like the following example produces compiler error CS0527: "Type 'ValueType' in interface list is not an interface." 此错误消息指明,尽管可以定义结构实现的接口,但不支持继承。The error message indicates that, although you can define the interfaces that a struct implements, inheritance is not supported.

using System;

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

隐式继承Implicit inheritance

.NET 类型系统中的所有类型除了可以通过单一继承进行继承之外,还可以隐式继承自 Object 或其派生的类型。Besides any types that they may inherit from through single inheritance, all types in the .NET type system implicitly inherit from Object or a type derived from it. Object 的常用功能可用于任何类型。The common functionality of Object is available to any type.

为了说明隐式继承的具体含义,让我们来定义一个新类 SimpleClass,这只是一个空类定义:To see what implicit inheritance means, let's define a new class, SimpleClass, that is simply an empty class definition:

public class SimpleClass
{ }

然后可以使用反射(便于检查类型的元数据,从而获取此类型的相关信息),获取 SimpleClass 类型的成员列表。You can then use reflection (which lets you inspect a type's metadata to get information about that type) to get a list of the members that belong to the SimpleClass type. 尽管没有在 SimpleClass 类中定义任何成员,但示例输出表明它实际上有九个成员。Although you haven't defined any members in your SimpleClass class, output from the example indicates that it actually has nine members. 这些成员的其中之一是由 C# 编译器自动为 SimpleClass 类型提供的无参数(或默认)构造函数。One of these members is a parameterless (or default) constructor that is automatically supplied for the SimpleClass type by the C# compiler. 剩余八个是 Object(.NET 类型系统中的所有类和接口最终隐式继承自的类型)的成员。The remaining eight are members of Object, the type from which all classes and interfaces in the .NET type system ultimately implicitly inherit.

using System;
using System.Reflection;

public class Example
{
    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 (var 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";
            }
            var 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 类可以使用下面这些方法:Implicit inheritance from the Object class makes these methods available to the SimpleClass class:

  • 公共 ToString 方法将 SimpleClass 对象转换为字符串表示形式,返回完全限定的类型名称。The public ToString method, which converts a SimpleClass object to its string representation, returns the fully qualified type name. 在这种情况下,ToString 方法返回字符串“SimpleClass”。In this case, the ToString method returns the string "SimpleClass".

  • 三个用于测试两个对象是否相等的方法:公共实例 Equals(Object) 方法、公共静态 Equals(Object, Object) 方法和公共静态 ReferenceEquals(Object, Object) 方法。Three methods that test for equality of two objects: the public instance Equals(Object) method, the public static Equals(Object, Object) method, and the public static ReferenceEquals(Object, Object) method. 默认情况下,这三个方法测试的是引用相等性;也就是说,两个对象变量必须引用同一个对象,才算相等。By default, these methods test for reference equality; that is, to be equal, two object variables must refer to the same object.

  • 公共 GetHashCode 方法:计算允许在经哈希处理的集合中使用类型实例的值。The public GetHashCode method, which computes a value that allows an instance of the type to be used in hashed collections.

  • 公共 GetType 方法:返回表示 SimpleClass 类型的 Type 对象。The public GetType method, which returns a Type object that represents the SimpleClass type.

  • 受保护 Finalize 方法:用于在垃圾回收器回收对象的内存之前释放非托管资源。The protected Finalize method, which is designed to release unmanaged resources before an object's memory is reclaimed by the garbage collector.

  • 受保护 MemberwiseClone 方法:创建当前对象的浅表复制。The protected MemberwiseClone method, which creates a shallow clone of the current object.

由于是隐式继承,因此可以调用 SimpleClass 对象中任何继承的成员,就像它实际上是 SimpleClass 类中定义的成员一样。Because of implicit inheritance, you can call any inherited member from a SimpleClass object just as if it was actually a member defined in the SimpleClass class. 例如,下面的示例调用 SimpleClassObject 继承而来的 SimpleClass.ToString 方法。For instance, the following example calls the SimpleClass.ToString method, which SimpleClass inherits from Object.

using System;

public class SimpleClass
{}

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

下表列出了可以在 C# 中创建的各种类型及其隐式继承自的类型。The following table lists the categories of types that you can create in C# and the types from which they implicitly inherit. 每个基类型通过继承向隐式派生的类型提供一组不同的成员。Each base type makes a different set of members available through inheritance to implicitly derived types.

类型类别Type category 隐式继承自Implicitly inherits from
classclass Object
structstruct ValueTypeObjectValueType, Object
enumenum Enum, ValueType, ObjectEnum, ValueType, Object
委托delegate MulticastDelegate, Delegate, ObjectMulticastDelegate, Delegate, Object

继承和“is a”关系Inheritance and an "is a" relationship

通常情况下,继承用于表示基类和一个或多个派生类之间的“is a”关系,其中派生类是基类的特定版本;派生类是基类的具体类型。Ordinarily, inheritance is used to express an "is a" relationship between a base class and one or more derived classes, where the derived classes are specialized versions of the base class; the derived class is a type of the base class. 例如,Publication 类表示任何类型的出版物,BookMagazine 类表示出版物的具体类型。For example, the Publication class represents a publication of any kind, and the Book and Magazine classes represent specific types of publications.

备注

一个类或结构可以实现一个或多个接口。A class or struct can implement one or more interfaces. 虽然接口实现代码通常用作单一继承的解决方法或对结构使用继承的方法,但它旨在表示接口与其实现类型之间的不同关系(即“can do”关系),而不是继承关系。While interface implementation is often presented as a workaround for single inheritance or as a way of using inheritance with structs, it is intended to express a different relationship (a "can do" relationship) between an interface and its implementing type than inheritance. 接口定义了其向实现类型提供的一部分功能(如测试相等性、比较或排序对象,或支持区域性敏感的分析和格式设置)。An interface defines a subset of functionality (such as the ability to test for equality, to compare or sort objects, or to support culture-sensitive parsing and formatting) that the interface makes available to its implementing types.

请注意,“is a”还表示类型与其特定实例化之间的关系。Note that "is a" also expresses the relationship between a type and a specific instantiation of that type. 在以下示例中,Automobile 类包含三个唯一只读属性:Make(汽车制造商)、Model(汽车型号)和 Year(汽车出厂年份)。In the following example, Automobile is a class that has three unique read-only properties: Make, the manufacturer of the automobile; Model, the kind of automobile; and Year, its year of manufacture. Automobile 类还有一个自变量被分配给属性值的构造函数,并将 Object.ToString 方法重写为生成唯一标识 Automobile 实例(而不是 Automobile 类)的字符串。Your Automobile class also has a constructor whose arguments are assigned to the property values, and it overrides the Object.ToString method to produce a string that uniquely identifies the Automobile instance rather than the Automobile class.

using System;

public class Automobile
{
    public Automobile(string make, string model, int year)
    {
        if (make == null)
           throw new ArgumentNullException("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("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}";
}

在这种情况下,不得依赖继承来表示特定汽车品牌和型号。In this case, you shouldn't rely on inheritance to represent specific car makes and models. 例如,不需要定义 Packard 类型来表示帕卡德制造的汽车。For example, you don't need to define a Packard type to represent automobiles manufactured by the Packard Motor Car Company. 相反,可以通过创建将相应值传递给其类构造函数的 Automobile 对象来进行表示,如以下示例所示。Instead, you can represent them by creating an Automobile object with the appropriate values passed to its class constructor, as the following example does.

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”关系最适用于基类和向基类添加附加成员或需要基类没有的其他功能的派生类。An is-a relationship based on inheritance is best applied to a base class and to derived classes that add additional members to the base class or that require additional functionality not present in the base class.

设计基类及其派生类Designing the base class and derived classes

让我们来看看如何设计基类及其派生类。Let's look at the process of designing a base class and its derived classes. 在此部分中,将定义一个基类 Publication,用于表示任何类型的出版物,如书籍、杂志、报纸、期刊、文章等。还将定义一个从 Publication 派生的 Book 类。In this section, you'll define a base class, Publication, which represents a publication of any kind, such as a book, a magazine, a newspaper, a journal, an article, etc. You'll also define a Book class that derives from Publication. 可以将示例轻松扩展为定义其他派生类,如 MagazineJournalNewspaperArticleYou could easily extend the example to define other derived classes, such as Magazine, Journal, Newspaper, and Article.

Publication 基类The base Publication class

设计 Publication 类时,需要做出下面几项设计决策:In designing your Publication class, you need to make several design decisions:

  • 要在 Publication 基类中添加哪些成员、Publication 成员是否提供方法实现或 Publication 是否是用作派生类模板的抽象基类。What members to include in your base Publication class, and whether the Publication members provide method implementations or whether Publication is an abstract base class that serves as a template for its derived classes.

    在此示例中,Publication 类提供方法实现代码。In this case, the Publication class will provide method implementations. 设计抽象基类及其派生类部分中的示例就使用抽象基类定义派生类必须重写的方法。The Designing abstract base classes and their derived classes section contains an example that uses an abstract base class to define the methods that derived classes must override. 派生类可以随时提供适合派生类型的任意实现代码。Derived classes are free to provide any implementation that is suitable for the derived type.

    能够重用代码(即多个派生类共用基类方法的声明和实现代码,无需重写它们)是非抽象基类的优势所在。The ability to reuse code (that is, multiple derived classes share the declaration and implementation of base class methods and do not need to override them) is an advantage of non-abstract base classes. 因此,如果代码可能由某些或大多数特定 Publication 类型共用,则应向 Publication 添加成员。Therefore, you should add members to Publication if their code is likely to be shared by some or most specialized Publication types. 如果无法有效地提供基类实现,则最终将不得不在派生类中提供基本相同的成员实现代码,而不是共用基类中的同一实现代码。If you fail to provide base class implementations efficiently, you'll end up having to provide largely identical member implementations in derived classes rather a single implementation in the base class. 如果需要在多个位置保留重复的代码,可能会导致 bug 出现。The need to maintain duplicated code in multiple locations is a potential source of bugs.

    为了最大限度地提高代码重用性并创建合乎逻辑的直观继承层次结构,需要确保在 Publication 类中只添加所有或大多数出版物通用的数据和功能。Both to maximize code reuse and to create a logical and intuitive inheritance hierarchy, you want to be sure that you include in the Publication class only the data and functionality that is common to all or to most publications. 然后,派生类可以实现所表示的特定出版物种类的唯一成员。Derived classes then implement members that are unique to the particular kinds of publication that they represent.

  • 类层次结构的扩展空间大小。How far to extend your class hierarchy. 是否要开发包含三个或更多类的层次结构,而不是仅包含一个基类和一个或多个派生类?Do you want to develop a hierarchy of three or more classes, rather than simply a base class and one or more derived classes? 例如,Publication 可以是 Periodical 的基类,而后者又是 MagazineJournalNewspaper 的基类。For example, Publication could be a base class of Periodical, which in turn is a base class of Magazine, Journal and Newspaper.

    在示例中,将使用包含 Publication 类和一个派生类 Book 的小型层次结构。For your example, you'll use the small hierarchy of a Publication class and a single derived class, Book. 可以将此示例轻松扩展为创建其他许多派生自 Publication 的类,如 MagazineArticleYou could easily extend the example to create a number of additional classes that derive from Publication, such as Magazine and Article.

  • 能否实例化基类。Whether it makes sense to instantiate the base class. 如果不可以,则应向类应用 abstract 关键字。If it does not, you should apply the abstract keyword to the class. 否则,可通过调用类构造函数来实例化 Publication 类。Otherwise, your Publication class can be instantiated by calling its class constructor. 如果尝试通过直接调用类构造函数来实例化标记有 abstract 关键字的类,则 C# 编译器会生成错误 CS0144:“无法创建抽象类或接口的实例。”If an attempt is made to instantiate a class marked with the abstract keyword by a direct call to its class constructor, the C# compiler generates error CS0144, "Cannot create an instance of the abstract class or interface." 如果尝试使用反射来实例化类,则反射方法会抛出 MemberAccessExceptionIf an attempt is made to instantiate the class by using reflection, the reflection method throws a MemberAccessException.

    默认情况下,可以通过调用类构造函数来实例化基类。By default, a base class can be instantiated by calling its class constructor. 无需显式定义类构造函数。You do not have to explicitly define a class constructor. 如果基类的源代码中没有类构造函数,C# 编译器会自动提供默认的(无参数)构造函数。If one is not present in the base class' source code, the C# compiler automatically provides a default (parameterless) constructor.

    在此示例中,将把 Publication 类标记为 abstract,使其无法实例化。For your example, you'll mark the Publication class as abstract so that it cannot be instantiated. 一个不具备任何 abstract 方法的 abstract 类表示该类代表一个在几个具体类(例如 BookJournal)之间共享的抽象概念。An abstract class without any abstract methods indicates that this class represents an abstract concept that is shared among several concrete classes (like a Book, Journal).

  • 派生类是否必须继承特定成员的基类实现代码、是否能选择重写基类实现代码或者是否必须提供实现代码。Whether derived classes must inherit the base class implementation of particular members, whether they have the option to override the base class implementation, or whether they must provide an implementation. 使用 abstract 关键字来强制派生类提供实现代码。You use the abstract keyword to force derived classes to provide an implementation. 使用 virtual 关键字来允许派生类重写基类方法。You use the virtual keyword to allow derived classes to override a base class method. 默认情况下,可重写基类中定义的方法。By default, methods defined in the base class are not overridable.

Publication 类不具备任何 abstract 方法,不过类本身是 abstractThe Publication class does not have any abstract methods, but the class itself is abstract.

  • 派生类是否表示继承层次结构中的最终类,且本身不能用作其他派生类的基类。Whether a derived class represents the final class in the inheritance hierarchy and cannot itself be used as a base class for additional derived classes. 默认情况下,任何类都可以用作基类。By default, any class can serve as a base class. 可以应用 sealed 关键字来指明类不能用作其他任何类的基类。You can apply the sealed keyword to indicate that a class cannot serve as a base class for any additional classes. 如果尝试从密封类派生,则会生成编译器错误 CS0509:“无法从密封类型 <typeName> 派生。”Attempting to derive from a sealed class generated compiler error CS0509, "cannot derive from sealed type <typeName>".

    在此示例中,将把派生类标记为 sealedFor your example, you'll mark your derived class as sealed.

以下示例展示了 Publication 类的源代码,以及 Publication.PublicationType 属性返回的 PublicationType 枚举。The following example shows the source code for the Publication class, as well as a PublicationType enumeration that is returned by the Publication.PublicationType property. 除了继承自 Object 的成员之外,Publication 类还定义了以下唯一成员和成员重写:In addition to the members that it inherits from Object, the Publication class defines the following unique members and member overrides:

using System;

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("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;
      this.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;
}
  • 构造函数A constructor

    由于 Publication 类标记有 abstract,因此无法直接通过以下代码进行实例化:Because the Publication class is abstract, it cannot be instantiated directly from code like the following example:

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

    不过,它的实例构造函数可以直接通过派生类构造函数进行调用,如 Book 类的源代码所示。However, its instance constructor can be called directly from derived class constructors, as the source code for the Book class shows.

  • 两个与出版物相关的属性Two publication-related properties

    Title 是只读 String 属性,其值通过调用 Publication 构造函数提供。Title is a read-only String property whose value is supplied by calling the Publication constructor.

    Pages 是读写 Int32 属性,用于指明出版物的总页数。Pages is a read-write Int32 property that indicates how many total pages the publication has. 值存储在 totalPages 私有字段中。The value is stored in a private field named totalPages. 值必须为正数,否则会抛出 ArgumentOutOfRangeExceptionIt must be a positive number or an ArgumentOutOfRangeException is thrown.

  • 与出版商相关的成员Publisher-related members

    两个只读属性:PublisherTypeTwo read-only properties, Publisher and Type. 值最初是通过调用 Publication 类构造函数来提供。The values are originally supplied by the call to the Publication class constructor.

  • 与出版相关的成员Publishing-related members

    两个方法(PublishGetPublicationDate)用于设置并返回发布日期。Two methods, Publish and GetPublicationDate, set and return the publication date. 调用时,Publish 方法会将 published 标志设置为 true,并将传递给它的日期作为自变量分配给 datePublished 私有字段。The Publish method sets a private published flag to true when it is called and assigns the date passed to it as an argument to the private datePublished field. 如果 published 标志为 falseGetPublicationDate 方法会返回字符串“NYP”;如果为 true,则会返回 datePublished 字段的值。The GetPublicationDate method returns the string "NYP" if the published flag is false, and the value of the datePublished field if it is true.

  • 与版权相关的成员Copyright-related members

    Copyright 方法需要将版权所有者的姓名和版权授予年份用作参数,并将它们分配给属性 CopyrightNameCopyrightDateThe Copyright method takes the name of the copyright holder and the year of the copyright as arguments and assigns them to the CopyrightName and CopyrightDate properties.

  • 重写 ToString 方法An override of the ToString method

    如果类型不重写 Object.ToString 方法,则返回类型的完全限定的名称,这对于区分实例没什么用。If a type does not override the Object.ToString method, it returns the fully qualified name of the type, which is of little use in differentiating one instance from another. Publication 类将 Object.ToString 重写为返回 Title 属性值。The Publication class overrides Object.ToString to return the value of the Title property.

下图展示了基类 Publication 及其隐式继承类 Object 之间的关系。The following figure illustrates the relationship between your base Publication class and its implicitly inherited Object class.

Object 和 Publication 类

BookThe Book class

Book 类表示作为一种特定类型出版物的书籍。The Book class represents a book as a specialized type of publication. 下面的示例展示了 Book 类的源代码。The following example shows the source code for the Book class.

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.");
        ulong nISBN = 0;
        if (! UInt64.TryParse(isbn, out nISBN))
            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("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)
   {
      Book book = obj as Book;
      if (book == null)
         return false;
      else
         return ISBN == book.ISBN;   
   }

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

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

除了继承自 Publication 的成员之外,Book 类还定义了以下唯一成员和成员重写:In addition to the members that it inherits from Publication, the Book class defines the following unique members and member overrides:

  • 两个构造函数Two constructors

    两个 Book 构造函数共用三个常见参数。The two Book constructors share three common parameters. 其中两个参数(titlepublisher)对应于 Publication 构造函数的相应参数。Two, title and publisher, correspond to parameters of the Publication constructor. 第三个参数是 author,存储在不可变的 Author 属性中。The third is author, which is stored to a public immutable Author property. 其中一个构造函数包含存储在 ISBN 自动属性中的 isbn 参数。One constructor includes an isbn parameter, which is stored in the ISBN auto-property.

    第一个构造函数使用 this 关键字来调用另一个构造函数。The first constructor uses the this keyword to call the other constructor. 构造函数链是常见的构造函数定义模式。Constructor chaining is a common pattern in defining constructors. 调用参数最多的构造函数时,由参数较少的构造函数提供默认值。Constructors with fewer parameters provide default values when calling the constructor with the greatest number of parameters.

    第二个构造函数使用 base 关键字,将标题和出版商名称传递给基类构造函数。The second constructor uses the base keyword to pass the title and publisher name to the base class constructor. 如果没有在源代码中显式调用基类构造函数,那么 C# 编译器会自动提供对基类的默认或无参数构造函数的调用。If you don't make an explicit call to a base class constructor in your source code, the C# compiler automatically supplies a call to the base class' default or parameterless constructor.

  • 只读 ISBN 属性,用于返回 Book 对象的国际标准书号,即 10 位或 13 位的专属编号。A read-only ISBN property, which returns the Book object's International Standard Book Number, a unique 10- or 13-digit number. ISBN 作为参数提供给 Book 构造函数之一。The ISBN is supplied as an argument to one of the Book constructors. ISBN 存储在私有支持字段中,由编译器自动生成。The ISBN is stored in a private backing field, which is auto-generated by the compiler.

  • 只读 Author 属性。A read-only Author property. 作者姓名作为参数提供给两个 Book 构造函数,并存储在属性中。The author name is supplied as an argument to both Book constructors and is stored in the property.

  • 两个与价格相关的只读属性(PriceCurrency)。Two read-only price-related properties, Price and Currency. 值作为自变量提供给调用的 SetPrice 方法。Their values are provided as arguments in a SetPrice method call. Currency 属性是三位的 ISO 货币符号(例如,USD 表示美元)。The Currency property is the three-digit ISO currency symbol (for example, USD for the U.S. dollar). 可以从 ISOCurrencySymbol 属性检索 ISO 货币符号。ISO currency symbols can be retrieved from the ISOCurrencySymbol property. 这两个属性均为外部只读,但均可在 Book 类中由代码设置。Both of these properties are externally read-only, but both can be set by code in the Book class.

  • SetPrice 方法,用于设置 PriceCurrency 属性的值。A SetPrice method, which sets the values of the Price and Currency properties. 这些值由那些相同属性返回。Those values are returned by those same properties.

  • 重写 ToString 方法(继承自 Publication)、Object.Equals(Object)GetHashCode 方法(继承自 Object)。Overrides to the ToString method (inherited from Publication) and the Object.Equals(Object) and GetHashCode methods (inherited from Object).

    除非重写,否则 Object.Equals(Object) 方法测试的是引用相等性。Unless it is overridden, the Object.Equals(Object) method tests for reference equality. 也就是说,两个对象变量必须引用同一个对象,才算相等。That is, two object variables are considered to be equal if they refer to the same object. 相比之下,在 Book 类中,两个 Book 对象必须包含相同的 ISBN,才算相等。In the Book class, on the other hand, two Book objects should be equal if they have the same ISBN.

    重写 Object.Equals(Object) 方法时,还必须重写 GetHashCode 方法,此方法返回运行时为了实现高效检索,在经哈希处理的集合中存储项所使用的值。When you override the Object.Equals(Object) method, you must also override the GetHashCode method, which returns a value that the runtime uses to store items in hashed collections for efficient retrieval. 哈希代码应返回与测试相等性一致的值。The hash code should return a value that's consistent with the test for equality. 由于已将 Object.Equals(Object) 重写为在两个 Book 对象的 ISBN 属性相等时返回 true,因此返回的哈希代码是通过调用 ISBN 属性返回的字符串的 GetHashCode 方法计算得出。Since you've overridden Object.Equals(Object) to return true if the ISBN properties of two Book objects are equal, you return the hash code computed by calling the GetHashCode method of the string returned by the ISBN property.

下图展示了 Book 类及其基类 Publication 之间的关系。The following figure illustrates the relationship between the Book class and Publication, its base class.

Publication 和 Book 类

现在可以实例化 Book 对象,调用其唯一成员和继承的成员,并将其作为自变量传递给需要 PublicationBook 类型参数的方法,如以下示例所示。You can now instantiate a Book object, invoke both its unique and inherited members, and pass it as an argument to a method that expects a parameter of type Publication or of type Book, as the following example shows.

using System;
using static System.Console;

public class Example
{
   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");
      Write($"{book.Title} and {book2.Title} are the same publication: " +
            $"{((Publication) book).Equals(book2)}");
   }

   public static void ShowPublicationInfo(Publication pub)
   {
       string pubDate = pub.GetPublicationDate();
       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

设计抽象基类及其派生类Designing abstract base classes and their derived classes

在上面的示例中定义了一个基类,它提供了许多方法的实现代码,以便派生类可以共用代码。In the previous example, you defined a base class that provided an implementation for a number of methods to allow derived classes to share code. 然而,在许多情况下,我们并不希望基类提供实现代码。In many cases, however, the base class is not expected to provide an implementation. 相反,基类是声明抽象方法的抽象类,用作定义每个派生类必须实现的成员的模板 。Instead, the base class is an abstract class that declares abstract methods; it serves as a template that defines the members that each derived class must implement. 通常情况下,在抽象基类中,每个派生类型的实现代码都是相应类型的专属代码。Typically in an abstract base class, the implementation of each derived type is unique to that type. 尽管该类提供了出版物通用的功能的实现代码,但由于实例化 Publication 对象毫无意义,因此,使用 abstract 关键字来标记该类。You marked the class with the abstract keyword because it made no sense to instantiate a Publication object, although the class did provide implementations of functionality common to publications.

例如,每个封闭的二维几何形状都包含两个属性:面积(即形状的内部空间)和周长(或沿形状一周的长度)。For example, each closed two-dimensional geometric shape includes two properties: area, the inner extent of the shape; and perimeter, or the distance along the edges of the shape. 然而,这两个属性的计算方式完全取决于具体的形状。The way in which these properties are calculated, however, depends completely on the specific shape. 例如,圆和三角形的周长计算公式就有所不同。The formula for calculating the perimeter (or circumference) of a circle, for example, is different from that of a triangle. Shape 类是一个包含 abstract 方法的 abstract 类。The Shape class is an abstract class with abstract methods. 这表示派生类共享相同的功能,但这些派生类以不同的方式实现该功能。That indicates derived classes share the same functionality, but those derived classes implement that functionality differently.

以下示例定义了 Shape 抽象基类,此基类又定义了两个属性:AreaPerimeterThe following example defines an abstract base class named Shape that defines two properties: Area and Perimeter. 除了用 abstract 关键字标记类之外,还需要用 abstract 关键字标记每个实例成员。In addition to marking the class with the abstract keyword, each instance member is also marked with the abstract keyword. 在此示例中,Shape 还将 Object.ToString 方法重写为返回类型的名称,而不是其完全限定的名称。In this case, Shape also overrides the Object.ToString method to return the name of the type, rather than its fully qualified name. 基类还定义了两个静态成员(GetAreaGetPerimeter),以便调用方可以轻松检索任何派生类实例的面积和周长。And it defines two static members, GetArea and GetPerimeter, that allow callers to easily retrieve the area and perimeter of an instance of any derived class. 将派生类实例传递给两个方法中的任意一个时,运行时调用的是派生类重写的方法。When you pass an instance of a derived class to either of these methods, the runtime calls the method override of the derived class.

using System;

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 派生一些类。You can then derive some classes from Shape that represent specific shapes. 以下示例定义了三个类:TriangleRectangleCircleThe following example defines three classes, Triangle, Rectangle, and Circle. 每个类都使用特定形状的专属公式来计算面积和周长。Each uses a formula unique for that particular shape to compute the area and perimeter. 一些派生类还定义所表示形状的专属属性(如 Rectangle.DiagonalCircle.Diameter)。Some of the derived classes also define properties, such as Rectangle.Diagonal and Circle.Diameter, that are unique to the shape that they represent.

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 的对象。The following example uses objects derived from Shape. 它实例化派生自 Shape 的一组对象,然后调用 Shape 类的静态方法,用于包装返回的 Shape 属性值。It instantiates an array of objects derived from Shape and calls the static methods of the Shape class, which wraps return Shape property values. 运行时从派生类型的重写属性检索值。The runtime retrieves values from the overridden properties of the derived types. 以下示例还将数组中的每个 Shape 对象显式转换成其派生类型;如果显式转换成功,则检索 Shape 的特定子类的属性。The example also casts each Shape object in the array to its derived type and, if the cast succeeds, retrieves properties of that particular subclass of Shape.

using System;

public class Example
{
   public static void Main()
   {
      Shape[] shapes = { new Rectangle(10, 12), new Square(5),
                        new Circle(3) };
      foreach (var shape in shapes) {
         Console.WriteLine($"{shape}: area, {Shape.GetArea(shape)}; " +
                           $"perimeter, {Shape.GetPerimeter(shape)}");
         var rect = shape as Rectangle;
         if (rect != null) {
            Console.WriteLine($"   Is Square: {rect.IsSquare()}, Diagonal: {rect.Diagonal}");
            continue;
         }
         var sq = shape as Square;
         if (sq != null) {
            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

请参阅See also