Pro C#10 CHAPTER 6 Understanding Inheritance and Polymorphism

CHAPTER 6 Understanding Inheritance and Polymorphism

第6章 了解继承和多态性

Chapter 5 examined the first pillar of OOP: encapsulation. At that time, you learned how to build a single well-defined class type with constructors and various members (fields, properties, methods, constants, and read-only fields). This chapter will focus on the remaining two pillars of OOP: inheritance and polymorphism.
第5章探讨了OOP的第一个支柱:封装。当时,您学习了如何使用构造函数和各种成员(字段、属性、方法、常量和只读字段)生成单个定义良好的类类型。本章将重点介绍 OOP 的其余两个支柱:继承和多态性。

First, you will learn how to build families of related classes using inheritance. As you will see, this form of code reuse allows you to define common functionality in a parent class that can be leveraged, and possibly altered, by child classes. Along the way, you will learn how to establish a polymorphic interface into class hierarchies using virtual and abstract members, as well as the role of explicit casting.
首先,您将学习如何使用继承构建相关类的族。如您所见,这种形式的代码重用允许您在父类中定义可以利用的通用功能,并且可能被子类改变。在此过程中,您将学习如何使用虚拟和抽象成员在类层次结构中建立多态接口,以及显式强制转换的作用。

The chapter will wrap up by examining the role of the ultimate parent class in the .NET base class libraries: System.Object.
本章将通过检查 .NET 基类库中最终父类的角色来结束:System.Object。

Understanding the Basic Mechanics of Inheritance

了解继承的基本机制

Recall from Chapter 5 that inheritance is an aspect of OOP that facilitates code reuse. Specifically speaking, code reuse comes in two flavors: inheritance (the “is-a” relationship) and the containment/delegation model (the “has-a” relationship). Let’s begin this chapter by examining the classical inheritance model of the “is-a” relationship.
回想一下第 5 章,继承是 OOP 的一个方面,它有助于代码重用。具体来说,代码重用有两种形式:继承(“is-a”关系)和包含/委派模型(“has-a”关系)。让我们从检查“is-a”关系的经典继承模型开始本章。

When you establish “is-a” relationships between classes, you are building a dependency between two or more class types. The basic idea behind classical inheritance is that new classes can be created using existing classes as a starting point. To begin with a simple example, create a new Console Application project named BasicInheritance. Now assume you have designed a class named Car that models some basic details of an automobile.
在类之间建立“is-a”关系时,是在两个或多个类类型之间建立依赖关系。经典继承背后的基本思想是可以使用现有类作为起点创建新类。若要从一个简单的示例开始,创建一个名为 BasicInheritance 的新控制台应用程序项目。现在假设您设计了一个名为 Car 的类,用于对汽车的一些基本细节进行建模。

namespace BasicInheritance;
// A simple base class. class Car
{
public readonly int MaxSpeed; private int _currSpeed;

public Car(int max)
{
MaxSpeed = max;
}

public Car()
{
MaxSpeed = 55;
}
public int Speed
{
get { return _currSpeed; } set
{
_currSpeed = value;
if (_currSpeed > MaxSpeed)
{
_currSpeed = MaxSpeed;
}
}
}
}

Notice that the Car class is using encapsulation services to control access to the private currSpeed field using a public property named Speed. At this point, you can exercise your Car type as follows:
请注意,Car 类使用封装服务来控制对私有 currSpeed 字段的访问,该属性使用名为 Speed 的公共属性。此时,您可以按如下方式锻炼您的汽车类型:

using BasicInheritance;

Console.WriteLine(" Basic Inheritance \n");
// Make a Car object, set max speed and current speed. Car myCar = new Car(80) {Speed = 50};

// Print current speed.
Console.WriteLine("My car is going {0} MPH", myCar.Speed); Console.ReadLine();

Specifying the Parent Class of an Existing Class

指定现有类的父类

Now assume you want to build a new class named MiniVan. Like a basic Car, you want to define the MiniVan class to support data for a maximum speed, a current speed, and a property named Speed to allow the object user to modify the object’s state. Clearly, the Car and MiniVan classes are related; in fact, it can be said that a MiniVan “is-a” type of Car. The “is-a” relationship (formally termed classical inheritance) allows you to build new class definitions that extend the functionality of an existing class.
现在假设您要构建一个名为 MiniVan 的新类。与基本 Car 一样,您希望定义 MiniVan 类以支持最大速度、当前速度和名为 Speed 的属性的数据,以允许对象用户修改对象的状态。显然,汽车和小型货车类别是相关的;事实上,可以说小型货车“是一种”类型的汽车。“is-a”关系(正式称为经典继承)允许您构建扩展现有类功能的新类定义。

The existing class that will serve as the basis for the new class is termed a base class, superclass, or parent class. The role of a base class is to define all the common data and members for the classes that extend it.
将用作新类基础的现有类称为基类、超类或父类。基类的作用是为扩展基类的类定义所有公共数据和成员。

The extending classes are formally termed derived or child classes. In C#, you make use of the colon operator on the class definition to establish an “is-a” relationship between classes. Assume you have authored the following new MiniVan class:
扩展类正式称为派生类或子类。在 C# 中,使用类定义上的冒号运算符在类之间建立“is-a”关系。假设您已经创作了以下新的小型货车类:

namespace BasicInheritance;
// MiniVan "is-a" Car. class MiniVan : Car
{
}

Currently, this new class has not defined any members whatsoever. So, what have you gained by extending your MiniVan from the Car base class? Simply put, MiniVan objects now have access to each public member defined within the parent class.
目前,这个新类尚未定义任何成员。那么,通过从汽车基类扩展您的小型货车,您获得了什么?简而言之,MiniVan 对象现在可以访问父类中定义的每个公共成员。

■Note Although constructors are typically defined as public, a derived class never inherits the constructors of a parent class. Constructors are used to construct only the class that they are defined within, although they can be called by a derived class through constructor chaining. This will be covered shortly.
注意 尽管构造函数通常定义为公共函数,但派生类从不继承父类的构造函数。 构造函数仅用于构造在其中定义它们的类,尽管派生类可以通过构造函数链接调用它们。这将很快介绍。

Given the relation between these two class types, you can now make use of the MiniVan class like so:
给定这两种类类型之间的关系,您现在可以像这样使用 MiniVan 类:

Console.WriteLine(" Basic Inheritance \n");
.
// Now make a MiniVan object.
MiniVan myVan = new MiniVan {Speed = 10}; Console.WriteLine("My van is going {0} MPH", myVan.Speed); Console.ReadLine();

Again, notice that although you have not added any members to the MiniVan class, you have direct access to the public Speed property of your parent class and have thus reused code. This is a far better approach than creating a MiniVan class that has the same members as Car, such as a Speed property. If you did duplicate code between these two classes, you would need to now maintain two bodies of code, which is certainly a poor use of your time.
同样,请注意,尽管您尚未向 MiniVan 类添加任何成员,但您可以直接访问父类的公共 Speed 属性,因此重用了代码。这比创建具有与 Car 相同成员的 MiniVan 类(如 Speed 属性)要好得多。如果你在这两个类之间复制了代码,你现在需要维护两个代码体,这肯定是对你的时间的不良利用。

Always remember that inheritance preserves encapsulation; therefore, the following code results in a compiler error, as private members can never be accessed from an object reference:
永远记住,继承保留封装;因此,以下代码会导致编译器错误,因为永远无法从对象引用访问私有成员:

Console.WriteLine(" Basic Inheritance \n");

// Make a MiniVan object. MiniVan myVan = new MiniVan(); myVan.Speed = 10;
Console.WriteLine("My van is going {0} MPH", myVan.Speed);
// Error! Can’t access private members! myVan._currSpeed = 55; Console.ReadLine();

On a related note, if the MiniVan defined its own set of members, it would still not be able to access any private member of the Car base class. Remember, private members can be accessed only by the class that defines it. For example, the following method in MiniVan would result in a compiler error:
在相关的说明中,如果MiniVan定义了自己的成员集,它仍然无法访问Car基类的任何私有成员。请记住,私有成员只能由定义它的类访问。例如,MiniVan 中的以下方法将导致编译器错误:

// MiniVan derives from Car. class MiniVan : Car
{
public void TestMethod()
{
// OK! Can access public members
// of a parent within a derived type. Speed = 10;

// Error! Cannot access private
// members of parent within a derived type.
_currSpeed = 10;
}
}

Regarding Multiple Base Classes

关于多个基类

Speaking of base classes, it is important to keep in mind that C# demands that a given class have exactly one direct base class. It is not possible to create a class type that directly derives from two or more base classes (this technique, which is supported in unmanaged C++, is known as multiple inheritance, or simply MI). If you attempted to create a class that specifies two direct parent classes, as shown in the following code, you would receive compiler errors:
说到基类,重要的是要记住,C# 要求给定的类只有一个直接基类。不可能创建直接派生自两个或多个基类的类类型(非托管C++支持此技术称为多重继承,或简称为 MI)。如果尝试创建指定两个直接父类的类(如以下代码所示),则会收到编译器错误:

// Illegal! C# does not allow
// multiple inheritance for classes! class WontWork
: BaseClassOne, BaseClassTwo
{}

As you will see in Chapter 8, the .NET Core platform does allow a given class, or structure, to implement any number of discrete interfaces. In this way, a C# type can exhibit a number of behaviors while avoiding the complexities associated with MI. Using this technique, you can build sophisticated interface hierarchies that model complex behaviors (again, see Chapter 8).
正如您将在第 8 章中看到的,.NET Core 平台确实允许给定的类或结构实现任意数量的离散接口。这样,C# 类型可以表现出许多行为,同时避免与 MI 相关的复杂性。 使用此技术,您可以构建复杂的接口层次结构来对复杂行为进行建模(同样,请参阅第 8 章)。

Using the sealed Keyword

使用密封的关键字

C# supplies another keyword, sealed, that prevents inheritance from occurring. When you mark a class as sealed, the compiler will not allow you to derive from this type. For example, assume you have decided that it makes no sense to further extend the MiniVan class.
C# 提供了另一个关键字 Seal 来防止发生继承。将类标记为密封时,编译器将不允许从此类型派生。例如,假设您已经决定进一步扩展 MiniVan 类是没有意义的。

// The MiniVan class cannot be extended! sealed class MiniVan : Car
{
}

If you (or a teammate) were to attempt to derive from this class, you would receive a compile-time error.
如果您(或团队成员)尝试从此类派生,您将收到编译时错误。

// Error! Cannot extend
// a class marked with the sealed keyword! class DeluxeMiniVan
: MiniVan
{
}

Most often, sealing a class makes the best sense when you are designing a utility class. For example, the System namespace defines numerous sealed classes, such as the String class. Thus, just like the MiniVan, if you attempt to build a new class that extends System.String, you will receive a compile-time error.
大多数情况下,在设计实用程序类时,密封类最有意义。例如,System 命名空间定义了许多密封类,如 String 类。因此,就像MiniVan一样,如果您尝试构建扩展System.String的新类,您将收到编译时错误。

// Another error! Cannot extend
// a class marked as sealed! class MyString
: String
{
}

■Note In Chapter 4, you learned that C# structures are always implicitly sealed (see Table 4-3). Therefore, you can never derive one structure from another structure, a class from a structure, or a structure from a class. Structures can be used to model only stand-alone, atomic, user-defined data types. If you want to leverage the “is-a” relationship, you must use classes.
注意 在第 4 章中,您了解到 C# 结构始终是隐式密封的(请参阅表 4-3)。因此,您永远不能从另一个结构派生一个结构,从结构派生一个类,或从类派生一个结构。结构只能用于对独立的、原子的、用户定义的数据类型进行建模。如果要利用“is-a”关系,则必须使用类。

As you would guess, there are many more details to inheritance that you will come to know during the remainder of this chapter. For now, simply keep in mind that the colon operator allows you to establish base/derived class relationships, while the sealed keyword prevents subsequent inheritance from occurring.
正如您所猜到的,在本章的其余部分,您将了解更多有关继承的细节。现在,只需记住冒号运算符允许您建立基/派生类关系,而 sealed 关键字可防止发生后续继承。

Revisiting Visual Studio Class Diagrams

重新访问 Visual Studio 类图

In Chapter 2, I briefly mentioned that Visual Studio allows you to establish base/derived class relationships visually at design time. To leverage this aspect of the IDE, your first step is to include a new class diagram file into your current project. To do so, access the Project ➤ Add New Item menu option and click the Class Diagram icon (in Figure 6-1, I renamed the file from ClassDiagram1.cd to Cars.cd).
在第2章中,我简要地提到Visual Studio允许您在设计时直观地建立基/派生类关系。若要利用 IDE 的这一方面,第一步是在当前项目中包含一个新的类图文件。为此,请访问“项目”➤“添加新项”菜单选项,然后单击“类图”图标(在图 6-1 中,我将文件从 ClassDiagram1.cd 重命名为 Cars.cd)。

Alt text
Figure 6-1. Inserting a new class diagram
图 6-1。 插入新的类图

After you click the Add button, you will be presented with a blank designer surface. To add types to a class designer, simply drag each file from the Solution Explorer window onto the surface. Also recall that if you delete an item from the visual designer (simply by selecting it and pressing the Delete key), this will not destroy the associated source code but simply remove the item off the designer surface. Figure 6-2 shows the current class hierarchy.
单击“添加”按钮后,将显示一个空白的设计器图面。若要向类设计器添加类型,只需将每个文件从“解决方案资源管理器”窗口拖到图面上即可。另请注意,如果从可视化设计器中删除某个项(只需选择该项并按 Delete 键),这不会破坏关联的源代码,而只是从设计器图面中删除该项。图 6-2 显示了当前的类层次结构。

Alt text
Figure 6-2. The visual class designer of Visual Studio
图 6-2。 Visual Studio 的视觉类设计器

Beyond simply displaying the relationships of the types within your current application, recall from Chapter 2 that you can also create new types and populate their members using the Class Designer toolbox and Class Details window.
除了简单地显示当前应用程序中类型的关系之外,还记得第 2 章中还可以使用“类设计器”工具箱和“类详细信息”窗口创建新类型并填充其成员。

If you want to make use of these visual tools during the remainder of the book, feel free. However, always make sure you analyze the generated code so you have a solid understanding of what these tools have done on your behalf.
如果您想在本书的其余部分使用这些可视化工具,请随意。但是,请始终确保分析生成的代码,以便对这些工具代表您执行的操作有深入的了解。

Understanding the Second Pillar of OOP: The Details of Inheritance

了解 OOP 的第二个支柱:继承的细节

Now that you have seen the basic syntax of inheritance, let’s create a more complex example and get to know the numerous details of building class hierarchies. To do so, you will be reusing the Employee class you designed in Chapter 5. To begin, create a new C# Console Application project named Employees.
现在您已经了解了继承的基本语法,让我们创建一个更复杂的示例,并了解构建类层次结构的众多细节。为此,您将重用在第 5 章中设计的 Employee 类。首先,创建一个名为“员工”的新 C# 控制台应用程序项目。

Next, copy the Employee.cs, Employee.Core.cs, and EmployeePayTypeEnum.cs files you created in the EmployeeApp example from Chapter 5 into the Employees project.
接下来,将您在第 5 章的 EmployeeApp 示例中创建的 Employee.cs、Employee.Core.cs 和 EmployeePayTypeEnum.cs 文件复制到 Employees 项目中。

■Note Prior to .NET Core, the files needed to be referenced in the .csproj file to use them in a C# project. With .NET Core, all the files in the current directory structure are automatically included in your project. Simply copying the two files from the other project into the current project directory is enough to have them included in your project.
注意 在 .NET Core 之前,需要在 .csproj 文件中引用这些文件才能在 C# 项目中使用它们。使用 .NET Core,当前目录结构中的所有文件都会自动包含在项目中。只需将两个文件从另一个项目复制到当前项目目录中就足以将它们包含在项目中。

Before you start to build some derived classes, you have two details to attend to. Because the original Employee class was created in a project named EmployeeApp, the class has been wrapped within an identically named .NET Core namespace. Chapter 16 will examine namespaces in detail; however, for simplicity, rename the current namespace (in all three file locations) to Employees to match your new project name.
在开始构建一些派生类之前,有两个细节需要注意。由于原始 Employee 类是在名为 EmployeeApp 的项目中创建的,因此该类已包装在名称相同的 .NET Core 命名空间中。第16章将详细检查命名空间;但是,为简单起见,请将当前命名空间(在所有三个文件位置中)重命名为“员工”以匹配新项目名称。

// Be sure to change the namespace name in both C# files! namespace Employees;
partial class Employee
{…}

■Note If you removed the default constructor during the changes to the Employee class in Chapter 5, make sure to add it back into the class.
注意 如果在第 5 章中对 Employee 类进行更改期间删除了默认构造函数,请确保将其添加回类中。

The second detail is to remove any of the commented code from the different iterations of the Employee
class from the Chapter 5 example.
第二个细节是从员工的不同迭代中删除任何注释代码第 5 章示例中的类 。

■Note As a sanity check, compile and run your new project by entering dotnet run in a command prompt (in your project’s directory) or pressing Ctrl+F5 if you are using Visual Studio. The program will not do anything at this point; however, this will ensure you do not have any compiler errors.
注意 作为健全性检查,通过在命令提示符下输入 dotnet run(在项目目录中)或按 Ctrl+F5(如果使用 Visual Studio)来编译和运行新项目。此时程序不会执行任何操作;但是,这将确保您没有任何编译器错误。

Your goal is to create a family of classes that model various types of employees in a company. Assume you want to leverage the functionality of the Employee class to create two new classes (SalesPerson and Manager). The new SalesPerson class “is-an” Employee (as is a Manager). Remember that under the classical inheritance model, base classes (such as Employee) are used to define general characteristics that are common to all descendants. Subclasses (such as SalesPerson and Manager) extend this general functionality while adding more specific functionality.
您的目标是创建一个类系列,对公司中的各种类型的员工进行建模。假设您要利用 Employee 类的功能来创建两个新类(销售人员和经理)。新的销售人员类“是”员工(经理也是如此)。请记住,在经典继承模型下,基类(如 Employee)用于定义所有后代共有的一般特征。子类(如销售人员和经理)扩展了此常规功能,同时添加了更具体的功能。

For your example, you will assume that the Manager class extends Employee by recording the number of stock options, while the SalesPerson class maintains the number of sales made. Insert a new class file (Manager.cs) that defines the Manager class with the following automatic property:
对于您的示例,您将假定经理类通过记录股票期权的数量来扩展员工,而销售人员类则维护销售数量。插入一个新的类文件 (Manager.cs),该文件使用以下自动属性定义管理器类:

namespace Employees;
// Managers need to know their number of stock options. class Manager : Employee
{
public int StockOptions { get; set; }
}

Next, add another new class file (SalesPerson.cs) that defines the SalesPerson class with a fitting automatic property.
接下来,添加另一个新的类文件 (SalesPerson.cs),该文件使用合适的自动属性定义 SalesPerson 类。

namespace Employees;
// Salespeople need to know their number of sales. class SalesPerson : Employee
{
public int SalesNumber { get; set; }
}

Now that you have established an “is-a” relationship, SalesPerson and Manager have automatically inherited all public members of the Employee base class. To illustrate, update your top-level statements as follows:
现在,您已经建立了“is-a”关系,销售人员和经理将自动继承了员工基类的所有公共成员。为了说明这一点,请按如下所示更新顶级语句:

using Employees;
// Create a subclass object and access base class functionality. Console.WriteLine(" The Employee Class Hierarchy \n"); SalesPerson fred = new SalesPerson
{
Age = 31, Name = "Fred", SalesNumber = 50
};

Calling Base Class Constructors with the base Keyword

使用 base 关键字调用基类构造函数

Currently, SalesPerson and Manager can be created only using the “freebie” default constructor (see Chapter 5). With this in mind, assume you have added a new seven-argument constructor to the Manager type, which is invoked as follows:
目前,只能使用“freebie”默认构造函数创建销售人员和经理(请参阅第 5 章)。考虑到这一点,假设您已向 Manager 类型添加了一个新的七参数构造函数,该构造函数的调用方式如下:


// Assume Manager has a constructor matching this signature:
// (string fullName, int age, int empId,
// float currPay, string ssn, int numbOfOpts)
Manager chucky = new Manager("Chucky", 50, 92, 100000, "333-23-2322", 9000);

If you look at the parameter list, you can clearly see that most of these arguments should be stored in the member variables defined by the Employee base class. To do so, you might implement this custom constructor on the Manager class as follows:
如果您查看参数列表,您可以清楚地看到这些参数中的大多数应该存储在 Employee 基类定义的成员变量中。为此,可以在管理器类上实现此自定义构造函数,如下所示:

public Manager(string fullName, int age, int empId, float currPay, string ssn, int numbOfOpts)
{
// This property is defined by the Manager class. StockOptions = numbOfOpts;

// Assign incoming parameters using the
// inherited properties of the parent class. Id = empId;
Age = age;
Name = fullName;
Pay = currPay;
PayType = EmployeePayTypeEnum.Salaried;
// OOPS! This would be a compiler error,
// if the SSN property were read-only! SocialSecurityNumber = ssn;
}

The first issue with this approach is that if you defined any property as read-only (e.g., the SocialSecurityNumber property), you are unable to assign the incoming string parameter to this field, as shown in the final code statement of this custom constructor.
此方法的第一个问题是,如果将任何属性定义为只读(例如,SocialSecurityNumber 属性),则无法将传入的字符串参数分配给此字段,如此自定义构造函数的最终代码语句所示。

The second issue is that you have indirectly created a rather inefficient constructor, given that under C#, unless you say otherwise, the default constructor of a base class is called automatically before the logic of the derived constructor is executed. After this point, the current implementation accesses numerous public properties of the Employee base class to establish its state. Thus, you have really made eight hits (six inherited properties and two constructor calls) during the creation of a Manager object!
第二个问题是,您间接创建了一个效率相当低的构造函数,因为在 C# 下,除非您另有说明,否则在执行派生构造函数的逻辑之前会自动调用基类的默认构造函数。在此之后,当前实现将访问 Employee 基类的大量公共属性以建立其状态。因此,在创建 Manager 对象期间,您确实进行了 8 次命中(6 次继承属性和 2 次构造函数调用)!

To help optimize the creation of a derived class, you will do well to implement your subclass constructors to explicitly call an appropriate custom base class constructor, rather than the default. In this way, you are able to reduce the number of calls to inherited initialization members (which saves processing time). First, ensure your Employee parent class has the following six-argument constructor:
为了帮助优化派生类的创建,最好实现子类构造函数,以显式调用适当的自定义基类构造函数,而不是默认构造函数。通过这种方式,您可以减少对继承的初始化成员的调用次数(从而节省处理时间)。首先,确保 Employee 父类具有以下六参数构造函数:

// Add to the Employee base class.
public Employee(string name, int age, int id, float pay, string empSsn, EmployeePay TypeEnum payType)
{
Name = name;
Id = id;
Age = age;
Pay = pay;
SocialSecurityNumber = empSsn; PayType = payType;
}

Now, let’s retrofit the custom constructor of the Manager type to call this constructor using the base
keyword.

public Manager(string fullName, int age, int empId, float currPay, string ssn, int numbOfOpts)
: base(fullName, age, empId, currPay, ssn, EmployeePayTypeEnum.Salaried)
现在,让我们改造 Manager 类型的自定义构造函数,以使用 base 调用此构造函数关键词。

{
// This property is defined by the Manager class. StockOptions = numbOfOpts;
}

Here, the base keyword is hanging off the constructor signature (much like the syntax used to chain constructors on a single class using the this keyword, as was discussed in Chapter 5), which always indicates a derived constructor is passing data to the immediate parent constructor. In this situation, you are explicitly calling the six-parameter constructor defined by Employee and saving yourself unnecessary calls during the creation of the child class. Additionally, you added a specific behavior to the Manager class, in that the pay type is always set to Salaried. The custom SalesPerson constructor looks almost identical, with the exception that the pay type is set to Commission.
在这里,base 关键字挂在构造函数签名上(很像使用 this 关键字将构造函数链接到单个类上的语法,如第 5 章所述),这始终指示派生构造函数正在将数据传递给直接父构造函数。在这种情况下,您将显式调用 Employee 定义的六参数构造函数,并在创建子类期间省去不必要的调用。此外,您还向经理类添加了特定行为,因为付薪类型始终设置为“受薪”。自定义销售人员构造函数看起来几乎相同,只是付薪类型设置为佣金。

// As a general rule, all subclasses should explicitly call an appropriate
// base class constructor.
public SalesPerson(string fullName, int age, int empId, float currPay, string ssn, int numbOfSales)
: base(fullName, age, empId, currPay, ssn, EmployeePayTypeEnum.Commission)
{
// This belongs with us! SalesNumber = numbOfSales;
}

■ Note You may use the base keyword whenever a subclass wants to access a public or protected member defined by a parent class. Use of this keyword is not limited to constructor logic. You will see examples using base in this manner during the examination of polymorphism, later in this chapter.
注意 每当子类想要访问由父类定义的公共或受保护成员时,都可以使用 base 关键字。此关键字的使用不限于构造函数逻辑。在本章后面的多态性检查期间,您将看到以这种方式使用 base 的示例。

Finally, recall that once you add a custom constructor to a class definition, the default constructor is silently removed. Therefore, be sure to redefine the default constructor for the SalesPerson and Manager types. Here’s an example:
最后,回想一下,将自定义构造函数添加到类定义后,默认构造函数将无提示删除。因此,请确保重新定义销售人员和经理类型的默认构造函数。下面是一个示例:

// Add back the default ctor
// in the Manager class as well. public SalesPerson() {}

Keeping Family Secrets: The protected Keyword

保守家庭秘密:受保护的关键词

As you already know, public items are directly accessible from anywhere, while private items can be accessed only by the class that has defined them. Recall from Chapter 5 that C# takes the lead of many other modern object languages and provides an additional keyword to define member accessibility: protected.
如您所知,公共项可以从任何地方直接访问,而私有项只能由定义它们的类访问。回想一下第 5 章, C# 领先于许多其他现代对象语言,并提供了一个额外的关键字来定义成员可访问性:受保护。

When a base class defines protected data or protected members, it establishes a set of items that can be accessed directly by any descendant. If you want to allow the SalesPerson and Manager child classes to directly access the data sector defined by Employee, you can update the original Employee class definition (in the EmployeeCore.cs file) as follows:
当基类定义受保护的数据或受保护的成员时,它会建立一组可由任何后代直接访问的项。如果要允许“销售人员”和“经理”子类直接访问 Employee 定义的数据扇区,可以更新原始 Employee 类定义(在 EmployeeCore.cs 文件中),如下所示:

// Protected state data. partial class Employee

{
// Derived classes can now directly access this information. protected string EmpName;
protected int EmpId; protected float CurrPay; protected int EmpAge; protected string EmpSsn;
protected EmployeePayTypeEnum EmpPayType;…
}

■ Note Convention is that protected members are named PascalCased (EmpName) and not underscore- camelCase (_empName). This is not a requirement of the language, but a common code style. If you decide to update the names as I have done here, make sure to rename all of the backing methods in your properties to match the PascalCased protected properties.
当基类定义受保护的数据或受保护的成员时,它会建立一组可由任何后代直接访问的项。如果要允许“销售人员”和“经理”子类直接访问 Employee 定义的数据扇区,可以更新原始 Employee 类定义(在 EmployeeCore.cs 文件中),如下所示:

The benefit of defining protected members in a base class is that derived types no longer have to access the data indirectly using public methods or properties. The possible downfall, of course, is that when a derived type has direct access to its parent’s internal data, it is possible to accidentally bypass existing
business rules found within public properties. When you define protected members, you are creating a level of trust between the parent class and the child class, as the compiler will not catch any violation of your type’s business rules.
在基类中定义受保护成员的好处是,派生类型不再需要使用公共方法或属性间接访问数据。当然,可能的失败是,当派生类型可以直接访问其父级的内部数据时,可能会意外地绕过现有的在公共属性中找到的业务规则。定义受保护的成员时,将在父类和子类之间创建信任级别,因为编译器不会捕获任何违反类型业务规则的行为。

Finally, understand that as far as the object user is concerned, protected data is regarded as private (as the user is “outside” the family). Therefore, the following is illegal:
最后,了解就对象用户而言,受保护的数据被视为私有数据(因为用户在家庭“之外”)。因此,以下行为是非法的:

// Error! Can’t access protected data from client code. Employee emp = new Employee();
emp.empName = "Fred";

■ Note Although protected field data can break encapsulation, it is quite safe (and useful) to define protected methods. When building class hierarchies, it is common to define a set of methods that are only for use by derived types and are not intended for use by the outside world.
注意 尽管受保护的字段数据可以破坏封装,但定义受保护的方法非常安全(且有用)。在生成类层次结构时,通常会定义一组仅供派生类型使用且不供外部世界使用的方法。

Adding a sealed Class
添加密封类

Recall that a sealed class cannot be extended by other classes. As mentioned, this technique is most often used when you are designing a utility class. However, when building class hierarchies, you might find that a certain branch in the inheritance chain should be “capped off,” as it makes no sense to further extend the lineage. For example, assume you have added yet another class to your program (PtSalesPerson) that extends the existing SalesPerson type. Figure 6-3 shows the current update.
回想一下,密封类不能由其他类扩展。如前所述,此技术最常用于设计实用程序类。但是,在构建类层次结构时,您可能会发现继承链中的某个分支应该被“限制”,因为进一步扩展世系是没有意义的。例如,假设您已向程序 (PtSalesPerson) 添加了另一个扩展现有 SalesPerson 类型的类。图 6-3 显示了当前更新。

Alt text

Figure 6-3. The PtSalesPerson class
图 6-3。 PtSalesPerson 类

PtSalesPerson is a class representing, of course, a part-time salesperson. For the sake of argument, let’s say you want to ensure that no other developer is able to subclass from PTSalesPerson. To prevent others from extending a class, use the sealed keyword.
PtSalesperson是一个代表兼职销售人员的班级。为了便于讨论,假设您想确保没有其他开发人员能够从 PTSalesPerson 进行子类化。若要防止其他人扩展类,请使用密封关键字。

namespace Employees;
sealed class PtSalesPerson : SalesPerson
{
public PtSalesPerson(string fullName, int age, int empId, float currPay, string ssn, int numbOfSales)
: base(fullName, age, empId, currPay, ssn, numbOfSales)
{
}
// Assume other members here…
}

Understanding Inheritance with Record Types (New 9.0)

了解记录类型的继承(新 9.0)

The new C# 9.0 record types also support inheritance. To explore this, place your work in the Employees project on hold and create a new console app named RecordInheritance.
新的 C# 9.0 记录类型还支持继承。若要探索这一点,请将“员工”项目中的工作置于暂停状态,并创建一个名为“记录继承”的新控制台应用。

Inheritance for Record Types with Standard Properties

具有标准属性的记录类型的继承

Add two new files named Car.cs and MiniVan.cs, and add the following record defining code into their respective files:
添加两个名为 Car.cs 和 MiniVan.cs 的新文件,并将以下记录定义代码添加到各自的文件中:

//Car.cs
namespace RecordInheritance;
//Car record type public record Car
{
public string Make { get; init; } public string Model { get; init; } public string Color { get; init; }

public Car(string make, string model, string color)
{
Make = make;
Model = model;
Color = color;
}
}

//MiniVan.cs
namespace RecordInheritance;
//MiniVan record type
public sealed record MiniVan : Car
{
public int Seating { get; init; }
public MiniVan(string make, string model, string color, int seating) : base(make, model, color)
{
Seating = seating;
}
}

Notice that there isn’t much difference between these examples using record types and the previous examples using classes. The sealed access modifier on the record type prevents other record types from deriving from the sealed record types. Although not used in the listed examples, the protected access modifier on properties and methods behave the same as with class inheritance. You will also find the remaining topics in this chapter work with inherited record types. This is because record types are just a special type of class (as detailed in Chapter 5).
请注意,这些使用记录类型的示例与前面使用类的示例之间没有太大区别。记录类型上的密封访问修饰符可防止从密封记录类型派生其他记录类型。尽管在列出的示例中未使用,但属性和方法上的受保护访问修饰符的行为与类继承相同。您还将发现本章中的其余主题使用继承的记录类型。这是因为记录类型只是一种特殊类型的类(详见第5章)。

Record types also include implicit casts to their base class, as shown in the following code:
记录类型还包括对其基类的隐式强制转换,如以下代码所示:

using RecordInheritance; Console.WriteLine("Record type inheritance!"); Car c = new Car("Honda","Pilot","Blue");
MiniVan m = new MiniVan("Honda", "Pilot", "Blue",10); Console.WriteLine($"Checking MiniVan is-a Car:{m is Car}");

As one would expect, the output from the check m is that Car returns true, as the following output shows:
正如人们所期望的那样,检查 m 的输出是 Car 返回 true,如以下输出所示:

Record type inheritance! Checking minvan is-a car:True

It’s important to note that even though record types are specialized classes, you cannot cross-inherit between classes and records. To be clear, classes cannot inherit from record types, and record types cannot inherit from classes. Consider the following code, and notice that the last two examples won’t compile:
请务必注意,即使记录类型是专用类,也不能在类和记录之间交叉继承。需要明确的是,类不能从记录类型继承,记录类型不能从类继承。请考虑以下代码,请注意最后两个示例无法编译:

namespace RecordInheritance; public class TestClass { } public record TestRecord { }

//Classes cannot inherit records
// public class Test2 : TestRecord { }

//Records types cannot inherit from classes
// public record Test2 : TestClass { }

Inheritance for Record Types with Positional Parameters

具有位置参数的记录类型的继承

Inheritance also works with positional record types. The derived record declares positional parameters for all of the parameters in the base record. The derived record doesn’t hide them but uses them from the base record. The derived record only creates and initializes properties that are not on the base record.
继承也适用于位置记录类型。派生记录为基本记录中的所有参数声明位置参数。派生记录不会隐藏它们,而是从基本记录中使用它们。派生记录仅创建和初始化不在基记录上的属性。

To see this in action, create a new file named PositionalRecordTypes.cs in your project. Add the following code into your file:
若要查看此操作的实际效果,请在项目中创建一个名为 PositionalRecordTypes.cs 的新文件。将以下代码添加到文件中:

namespace RecordInheritance;
public record PositionalCar (string Make, string Model, string Color);
public record PositionalMiniVan (string Make, string Model, string Color, int seating)
: PositionalCar(Make, Model, Color);

public record MotorCycle(string Make, string Model);
public record Scooter(string Make, string Model) : MotorCycle(Make,Model); public record FancyScooter(string Make, string Model, string FancyColor)
: Scooter(Make, Model);

Add the following code to show what you already know to be true: that the positional record types work exactly the same as record types:
添加以下代码以显示您已经知道的情况:位置记录类型的工作方式与记录类型完全相同:

PositionalCar pc = new PositionalCar("Honda", "Pilot", "Blue"); PositionalMiniVan pm = new PositionalMiniVan("Honda", "Pilot", "Blue", 10);
Console.WriteLine($"Checking PositionalMiniVan is-a PositionalCar:{pm is PositionalCar}");

Nondestructive Mutation with Inherited Record Types

具有继承记录类型的非破坏性突变

When creating new record type instances using the with expression, the resulting record type is the same runtime type of the operand. Take the following example:
使用 with 表达式创建新的记录类型实例时,生成的记录类型与操作数的运行时类型相同。举个例子:

MotorCycle mc = new FancyScooter("Harley", "Lowrider","Gold"); Console.WriteLine($"mc is a FancyScooter: {mc is FancyScooter}"); MotorCycle mc2 = mc with { Make = "Harley", Model = "Lowrider" }; Console.WriteLine($"mc2 is a FancyScooter: {mc2 is FancyScooter}");

In both of these examples, the runtime type of the instances is FancyScooter, not MotorCycle:
在这两个示例中,实例的运行时类型是 FancyScooter,而不是 MotorCycle:

Record type inheritance! mc is a FancyScooter: True
mc2 is a FancyScooter: True

Equality with Inherited Record Types

在这两个示例中,实例的运行时类型是 FancyScooter,而不是 MotorCycle:

Recall from Chapter 5 that record types use value semantics to determine equality. One additional detail regarding record types is that the type of the record is part of the equality consideration. Take into consideration the MotorCycle and Scooter types from earlier:
回想一下第 5 章,记录类型使用值语义来确定相等性。有关记录类型的另一个详细信息是,记录的类型是相等性考虑的一部分。考虑前面的摩托车和踏板车类型:

public record MotorCycle(string Make, string Model);
public record Scooter(string Make, string Model) : MotorCycle(Make,Model);

Ignoring the fact that typically inherited classes extend base classes, these simple examples define two different record types that have the same properties. When creating instances with the same values for the properties, they fail the equality test due to being different types. Take the following code and results, for example:
忽略通常继承的类扩展基类的事实,这些简单示例定义了具有相同属性的两种不同记录类型。为属性创建具有相同值的实例时,由于类型不同,它们无法通过相等性测试。以以下代码和结果为例:

MotorCycle mc3 = new MotorCycle("Harley","Lowrider"); Scooter sc = new Scooter("Harley", "Lowrider");
Console.WriteLine($"MotorCycle and Scooter are equal: {Equals(mc3,sc)}");

Record type inheritance!
MotorCycle and Scooter are equal: False

The reason for the two not being equal is that the equality check with record types uses the runtime type, not the declared type. The following example further illustrates this:
两者不相等的原因是记录类型的相等性检查使用运行时类型,而不是声明的类型。以下示例进一步说明了这一点:

MotorCycle mc3 = new MotorCycle("Harley","Lowrider"); MotorCycle scMotorCycle = new Scooter("Harley", "Lowrider");
Console.WriteLine($"MotorCycle and Scooter Motorcycle are equal: {Equals(mc3,scMotorCycle)}");

Notice that both the mc3 and scMotorCycle variables are declared as MotorCycle record types. Despite this, the types are not equal, since the runtime types are different:
请注意,mc3 和 scMotorCycle 变量都声明为 MotorCycle 记录类型。尽管如此,类型并不相等,因为运行时类型不同:

Record type inheritance!
MotorCycle and Scooter Motorcycle are equal: False

Deconstructor Behavior with Inherited Record Types

具有继承记录类型的解构函数行为

The Deconstruct() method of a derived record returns the values of all positional properties of the declared, compile-time type. In this first example, the FancyColor property is not deconstructed because the compile- time type is MotorCycle:
派生记录的 Deconstruct() 方法返回声明的编译时类型的所有位置属性的值。在第一个示例中,未解构 FancyColor 属性,因为编译时类型为 MotorCycle:

MotorCycle mc = new FancyScooter("Harley", "Lowrider","Gold"); var (make1, model1) = mc; //doesn’t deconstruct FancyColor
var (make2, model2, fancyColor2) = (FancyScooter)mc;

However, if the variable is cast to the derived type, then all of the positional properties of the derived type are deconstructed, as shown here:
但是,如果将变量强制转换为派生类型,则会解构派生类型的所有位置属性,如下所示:

MotorCycle mc = new FancyScooter("Harley", "Lowrider","Gold"); var (make2, model2, fancyColor2) = (FancyScooter)mc;

Programming for Containment/Delegation

遏制/委派编程

Recall that code reuse comes in two flavors. You have just explored the classical “is-a” relationship. Before you examine the third pillar of OOP (polymorphism), let’s examine the “has-a” relationship (also known as the containment/delegation model or aggregation). Returning to the Employees project, create a new file named BenefitPackage.cs and add the code to model an employee benefits package, as follows:
回想一下,代码重用有两种形式。您刚刚探索了经典的“is-a”关系。在检查 OOP(多态性)的第三个支柱之前,让我们检查一下“has-a”关系(也称为包含/委派模型或聚合)。返回到“员工”项目,创建一个名为 BenefitPack 的新文件.cs并添加代码以对员工福利包进行建模,如下所示:

namespace Employees;
// This new type will function as a contained class. class BenefitPackage
{
// Assume we have other members that represent
// dental/health benefits, and so on. public double ComputePayDeduction()
{
return 125.0;
}
}

Obviously, it would be rather odd to establish an “is-a” relationship between the BenefitPackage class and the employee types. (Employee “is-a” BenefitPackage? I don’t think so.) However, it should be clear that some sort of relationship between the two could be established. In short, you would like to express the idea that each employee “has-a” BenefitPackage. To do so, you can update the Employee class definition as follows:
显然,在福利包类别和员工类型之间建立“是”关系是相当奇怪的。(员工“是”福利包?我不这么认为。但是,应该明确的是,两者之间可以建立某种关系。简而言之,您想表达每个员工“都有”福利包的想法。为此,您可以更新 Employee 类定义,如下所示:

// Employees now have benefits. partial class Employee
{
// Contain a BenefitPackage object.
protected BenefitPackage EmpBenefits = new BenefitPackage();

}

At this point, you have successfully contained another object. However, exposing the functionality of the contained object to the outside world requires delegation. Delegation is simply the act of adding public members to the containing class that use the contained object’s functionality.
此时,您已成功包含另一个对象。但是,向外部世界公开所包含对象的功能需要委派。委派只是将公共成员添加到使用所包含对象的功能的包含类的操作。

For example, you could update the Employee class to expose the contained empBenefits object using a custom property, as well as make use of its functionality internally using a new method named GetBenefitCost().
例如,您可以更新 Employee 类以使用自定义属性公开包含的 empBenefits 对象,以及使用名为 GetBenefitCost() 的新方法在内部使用其功能。

partial class Employee
{
// Contain a BenefitPackage object.
protected BenefitPackage EmpBenefits = new BenefitPackage();

// Expose certain benefit behaviors of object. public double GetBenefitCost()
=> EmpBenefits.ComputePayDeduction();

// Expose object through a custom property. public BenefitPackage Benefits
{
get { return EmpBenefits; } set { EmpBenefits = value; }
}
}

In the following updated code, notice how you can interact with the internal BenefitsPackage type defined by the Employee type:
在以下更新的代码中,请注意如何与员工类型定义的内部福利包类型进行交互:

Console.WriteLine(" The Employee Class Hierarchy \n");

Manager chucky = new Manager("Chucky", 50, 92, 100000, "333-23-2322", 9000); double cost = chucky.GetBenefitCost();
Console.WriteLine($"Benefit Cost: {cost}"); Console.ReadLine();

Understanding Nested Type Definitions

了解嵌套类型定义

Chapter 5 briefly mentioned the concept of nested types, which is a spin on the “has-a” relationship you have just examined. In C# (as well as other .NET languages), it is possible to define a type (enum, class, interface, struct, or delegate) directly within the scope of a class or structure. When you have done so, the nested (or “inner”) type is considered a member of the nesting (or “outer”) class and in the eyes of the runtime can be manipulated like any other member (fields, properties, methods, and events). The syntax used to nest a type is quite straightforward.
第5章简要提到了嵌套类型的概念,这是对你刚刚研究的“has-a”关系的旋转。在 C#(以及其他 .NET 语言)中,可以直接在类或结构的范围内定义类型(枚举、类、接口、结构或委托)。执行此操作后,嵌套(或“内部”)类型被视为嵌套(或“外部”)类的成员,并且在运行时眼中可以像任何其他成员(字段、属性、方法和事件)一样进行操作。用于嵌套类型的语法非常简单。

public class OuterClass
{
// A public nested type can be used by anybody. public class PublicInnerClass {}

// A private nested type can only be used by members
// of the containing class. private class PrivateInnerClass {}
}

Although the syntax is fairly clear, understanding why you would want to do this might not be readily apparent. To understand this technique, ponder the following traits of nesting a type:
尽管语法相当清晰,但理解为什么要这样做可能并不明显。若要理解此技术,请思考嵌套类型的以下特征:

• Nested types allow you to gain complete control over the access level of the inner type because they may be declared privately (recall that non-nested classes cannot be declared using the private keyword).
嵌套类型允许您完全控制内部类型的访问级别,因为它们可以私下声明(回想一下,非嵌套类不能使用 private 关键字声明)。

• Because a nested type is a member of the containing class, it can access private members of the containing class.
由于嵌套类型是包含类的成员,因此它可以访问包含类的私有成员。
• Often, a nested type is useful only as a helper for the outer class and is not intended for use by the outside world.
通常,嵌套类型仅用作外部类的帮助程序,不适合外部世界使用。

When a type nests another class type, it can create member variables of the type, just as it would for any point of data. However, if you want to use a nested type from outside the containing type, you must qualify it by the scope of the nesting type. Consider the following code:
当一个类型嵌套另一个类类型时,它可以创建该类型的成员变量,就像它对任何数据点所做的那样。但是,如果要使用包含类型外部的嵌套类型,则必须通过嵌套类型的作用域对其进行限定。请考虑以下代码:

// Create and use the public inner class. OK! OuterClass.PublicInnerClass inner;
inner = new OuterClass.PublicInnerClass();

// Compiler Error! Cannot access the private class. OuterClass.PrivateInnerClass inner2;
inner2 = new OuterClass.PrivateInnerClass();

To use this concept within the employee’s example, assume you have now nested the BenefitPackage directly within the Employee class type.
要在员工示例中使用此概念,假设您现在已经嵌套了福利包直接在员工类类型中。

partial class Employee
{
public class BenefitPackage
{
// Assume we have other members that represent
// dental/health benefits, and so on. public double ComputePayDeduction()
{
return 125.0;
}
}

}

The nesting process can be as “deep” as you require. For example, assume you want to create an enumeration named BenefitPackageLevel, which documents the various benefit levels an employee may choose. To programmatically enforce the tight connection between Employee, BenefitPackage, and BenefitPackageLevel, you could nest the enumeration as follows:
嵌套过程可以根据需要“深度”。例如,假设您要创建一个名为 BenefitPackageLevel 的枚举,该枚举记录了员工可以选择的各种福利级别。若要以编程方式强制实施 Employee、BenefitPackage 和 BenefitPackageLevel 之间的紧密连接,可以按如下所示嵌套枚举:

// Employee nests BenefitPackage. public partial class Employee
{
// BenefitPackage nests BenefitPackageLevel. public class BenefitPackage
{
public enum BenefitPackageLevel
{
Standard, Gold, Platinum
}

public double ComputePayDeduction()
{
return 125.0;
}
}

}

Because of the nesting relationships, note how you are required to make use of this enumeration:
由于嵌套关系,请注意如何要求使用此枚举:


// Define my benefit level. Employee.BenefitPackage.BenefitPackageLevel myBenefitLevel =
Employee.BenefitPackage.BenefitPackageLevel.Platinum;

Excellent! At this point, you have been exposed to a number of keywords (and concepts) that allow you to build hierarchies of related types via classical inheritance, containment, and nested types. If the details aren’t crystal clear right now, don’t sweat it. You will be building a number of additional hierarchies over the remainder of this book. Next up, let’s examine the final pillar of OOP: polymorphism.
非常好!此时,您已经接触到许多关键字(和概念),这些关键字(和概念)允许您通过经典继承、包含和嵌套类型构建相关类型的层次结构。如果现在细节还不清楚,请不要担心。您将在本书的其余部分构建许多其他层次结构。接下来,让我们来看看 OOP 的最后一个支柱:多态性。

Understanding the Third Pillar of OOP: C#’s Polymorphic Support

了解 OOP 的第三个支柱:C# 的多态支持

Recall that the Employee base class defined a method named GiveBonus(), which was originally implemented as follows (before updating it to use the property pattern):
回想一下,Employee 基类定义了一个名为 GiveBonus() 的方法,该方法最初实现如下(在更新它以使用属性模式之前):

public partial class Employee
{
public void GiveBonus(float amount) => _currPay += amount;

}

Because this method has been defined with the public keyword, you can now give bonuses to salespeople and managers (as well as part-time salespeople).
由于此方法已使用公共关键字定义,因此您现在可以向销售人员和经理(以及兼职销售人员)提供奖金。

Console.WriteLine(" The Employee Class Hierarchy \n");

// Give each employee a bonus?
Manager chucky = new Manager("Chucky", 50, 92, 100000, "333-23-2322", 9000); chucky.GiveBonus(300);
chucky.DisplayStats();
Console.WriteLine();

SalesPerson fran = new SalesPerson("Fran", 43, 93, 3000, "932-32-3232", 31); fran.GiveBonus(200);
fran.DisplayStats();
Console.ReadLine();

The problem with the current design is that the publicly inherited GiveBonus() method operates identically for all subclasses. Ideally, the bonus of a salesperson or part-time salesperson should consider the number of sales. Perhaps managers should gain additional stock options in conjunction with a monetary bump in salary. Given this, you are suddenly faced with an interesting question: “How can related types respond differently to the same request?” Again, glad you asked!
当前设计的问题在于,公共继承的 GiveBonus() 方法对所有子类的操作相同。理想情况下,销售人员或兼职销售人员的奖金应考虑销售数量。也许经理人应该在增加工资的同时获得额外的股票期权。鉴于此,您突然面临一个有趣的问题:“相关类型如何以不同的方式响应同一请求?再次,很高兴你问!

Using the virtual and override Keywords

使用虚拟关键字和覆盖关键字

Polymorphism provides a way for a subclass to define its own version of a method defined by its base class, using the process termed method overriding. To retrofit your current design, you need to understand the meaning of the virtual and override keywords. If a base class wants to define a method that may be (but does not have to be) overridden by a subclass, it must mark the method with the virtual keyword.
多态性为子类提供了一种方法,可以使用称为方法重写的过程来定义其基类定义的方法的自己的版本。要改造您当前的设计,您需要了解虚拟和覆盖关键字的含义。如果基类想要定义一个可能(但不必)被子类重写的方法,则必须使用 virtual 关键字标记该方法。

partial class Employee
{
// This method can now be "overridden" by a derived class. public virtual void GiveBonus(float amount)
{
Pay += amount;
}

}

■ Note Methods that have been marked with the virtual keyword are (not surprisingly) termed virtual methods.
注意 用 virtual 关键字标记的方法(毫不奇怪)称为虚拟方法。

When a subclass wants to change the implementation details of a virtual method, it does so using the override keyword. For example, SalesPerson and Manager could override GiveBonus() as follows (assume that PTSalesPerson will not override GiveBonus() and, therefore, simply inherits the version defined by SalesPerson):
当子类想要更改虚拟方法的实现细节时,它使用 override 关键字来实现。例如,SalesPerson 和 Manager 可以按如下方式覆盖 GiveBonus()(假设 PTSalesPerson 不会覆盖 GiveBonus(),因此只是继承 SalesPerson 定义的版本):

//SalesPerson.cs namespace Employees;

class SalesPerson : Employee
{

// A salesperson’s bonus is influenced by the number of sales. public override void GiveBonus(float amount)
{
int salesBonus = 0;
if (SalesNumber >= 0 && SalesNumber <= 100)
{
salesBonus = 10;
}
else
{
if (SalesNumber >= 101 && SalesNumber <= 200)
{
salesBonus = 15;
}
else
{
salesBonus = 20;
}
}
base.GiveBonus(amount * salesBonus);
}
}

//Manager.cs namespace Employees;
class Manager : Employee
{

public override void GiveBonus(float amount)
{
base.GiveBonus(amount); Random r = new Random(); StockOptions += r.Next(500);
}
}

Notice how each overridden method is free to leverage the default behavior using the base keyword.
请注意,每个重写的方法都可以使用 base 关键字自由利用默认行为。

In this way, you have no need to completely reimplement the logic behind GiveBonus() but can reuse (and possibly extend) the default behavior of the parent class.
通过这种方式,您无需完全重新实现 GiveBonus() 背后的逻辑,但可以重用(并可能扩展)父类的默认行为。

Also assume that the current DisplayStats() method of the Employee class has been declared virtually.
还假设 Employee 类的当前 DisplayStats() 方法已虚拟声明。

public virtual void DisplayStats()
{
Console.WriteLine("Name: {0}", Name);
Console.WriteLine("Id: {0}", Id);
Console.WriteLine("Age: {0}", Age);
Console.WriteLine("Pay: {0}", Pay);
Console.WriteLine("SSN: {0}", SocialSecurityNumber);
}

By doing so, each subclass can override this method to account for displaying the number of sales (for salespeople) and current stock options (for managers). For example, consider Manager’s version of the
DisplayStats() method (the SalesPerson class would implement DisplayStats() in a similar manner to
show the number of sales).
通过这样做,每个子类都可以重写此方法,以显示销售数量(对于销售人员)和当前股票期权(对于经理)。例如,考虑经理版本的DisplayStats() 方法(SalesPerson 类将以类似于显示销售数量)。

//Manager.cs
public override void DisplayStats()
{
base.DisplayStats();
Console.WriteLine("Number of Stock Options: {0}", StockOptions);
}
//SalesPerson.cs
public override void DisplayStats()
{
base.DisplayStats();
Console.WriteLine("Number of Sales: {0}", SalesNumber);
}

Now that each subclass can interpret what these virtual methods mean for itself, each object instance behaves as a more independent entity.
现在,每个子类都可以解释这些虚拟方法对自己的含义,每个对象实例的行为都是一个更加独立的实体。

Console.WriteLine(" The Employee Class Hierarchy \n");

// A better bonus system!
Manager chucky = new Manager("Chucky", 50, 92, 100000, "333-23-2322", 9000); chucky.GiveBonus(300);
chucky.DisplayStats();
Console.WriteLine();

SalesPerson fran = new SalesPerson("Fran", 43, 93, 3000, "932-32-3232", 31); fran.GiveBonus(200);
fran.DisplayStats();
Console.ReadLine();

The following output shows a possible test run of your application thus far:
以下输出显示了到目前为止应用程序可能的测试运行:

The Employee Class Hierarchy Name: Chucky
ID: 92
Age: 50
Pay: 100300
SSN: 333-23-2322
Number of Stock Options: 9337

Name: Fran ID: 93
Age: 43
Pay: 5000
SSN: 932-32-3232
Number of Sales: 31

Overriding Virtual Members with Visual Studio/Visual Studio Code

使用 Visual Studio/Visual Studio 代码覆盖虚拟成员

As you might have already noticed, when you are overriding a member, you must recall the type of every parameter—not to mention the method name and parameter-passing conventions (ref, out, and params). Both Visual Studio and Visual Studio Code have a helpful feature that you can make use of when overriding a virtual member. If you type the word override within the scope of a class type (then hit the spacebar), IntelliSense will automatically display a list of all the overridable members defined in your parent classes, excluding methods already overridden.
您可能已经注意到,在重写成员时,必须调用每个参数的类型,更不用说方法名称和参数传递约定(ref、out 和参数)。Visual Studio 和 Visual Studio Code 都有一个有用的功能,您可以在覆盖虚拟成员时使用该功能。如果在类类型的范围内键入单词重写(然后按空格键),IntelliSense 将自动显示父类中定义的所有可重写成员的列表,不包括已重写的方法。

When you select a member and hit the Enter key, the IDE responds by automatically filling in the method stub on your behalf. Note that you also receive a code statement that calls your parent’s version of the virtual member (you are free to delete this line if it is not required). For example, if you used this
technique when overriding the DisplayStats() method, you might find the following autogenerated code:
选择成员并按 Enter 键时,IDE 将通过代表您自动填写方法存根来响应。请注意,您还会收到一个代码语句,该语句调用您父级版本的虚拟成员(如果不需要,您可以自由删除此行)。例如,如果您使用了这个在重写 DisplayStats() 方法时,您可能会发现以下自动生成的代码:

public override void DisplayStats()
{
base.DisplayStats();
}

Sealing Virtual Members (Updated 10.0)

密封虚拟成员(10.0 更新)

Recall that the sealed keyword can be applied to a class type to prevent other types from extending its behavior via inheritance. As you might remember, you sealed PtSalesPerson because you assumed it made no sense for other developers to extend this line of inheritance any further.
回想一下,密封关键字可以应用于类类型,以防止其他类型通过继承扩展其行为。您可能还记得,您密封了 PtSalesPerson,因为您认为其他开发人员进一步扩展这条继承线是没有意义的。

On a related note, sometimes you might not want to seal an entire class but simply want to prevent derived types from overriding particular virtual methods. For example, assume you do not want part-time salespeople to obtain customized bonuses. To prevent the PTSalesPerson class from overriding the virtual GiveBonus() method, you could effectively seal this method in the SalesPerson class as follows:
在相关的说明中,有时您可能不想密封整个类,而只是希望防止派生类型重写特定的虚拟方法。例如,假设您不希望兼职销售人员获得定制的奖金。为了防止 PTSalesPerson 类重写虚拟 GiveBonus() 方法,您可以有效地将此方法密封在 SalesPerson 类中,如下所示:

// SalesPerson has sealed the GiveBonus() method! class SalesPerson : Employee
{

public override sealed void GiveBonus(float amount)
{

}
}

Here, SalesPerson has indeed overridden the virtual GiveBonus() method defined in the Employee class; however, it has explicitly marked it as sealed. Thus, if you attempted to override this method in the PtSalesPerson class, you would receive compile-time errors, as shown in the following code:
在这里,销售人员确实覆盖了 Employee 类中定义的虚拟 GiveBonus() 方法;但是,它已明确将其标记为密封。因此,如果您尝试在 PtSalesPerson 类中重写此方法,您将收到编译时错误,如以下代码所示:

sealed class PTSalesPerson : SalesPerson
{

// Compiler error! Can’t override this method
// in the PTSalesPerson class, as it was sealed. public override void GiveBonus(float amount)
{
}
}

New in C# 10, the ToString() method for a record can be sealed, preventing the compiler from synthesizing a ToString() method for any derived record types. Returning to the CarRecord from Chapter 5, notice the sealed ToString() method:
C# 10 中的新增功能是,可以密封记录的 ToString() 方法,从而防止编译器为任何派生记录类型合成 ToString() 方法。

public record CarRecord
{
public string Make { get; init; } public string Model { get; init; } public string Color { get; init; }

public CarRecord() {}
public CarRecord(string make, string model, string color)
{
Make = make;
Model = model;
Color = color;
}
public sealed override string ToString() => $"The is a {Color} {Make} {Model}";
}

Understanding Abstract Classes

了解抽象类

Currently, the Employee base class has been designed to supply various data members for its descendants, as well as supply two virtual methods (GiveBonus() and DisplayStats()) that may be overridden by a given descendant. While this is all well and good, there is a rather odd byproduct of the current design; you can directly create instances of the Employee base class.
目前,Employee 基类被设计为为其后代提供各种数据成员,并提供两个可能被给定后代覆盖的虚拟方法(GiveBonus() 和 DisplayStats())。虽然这一切都很好,但当前设计有一个相当奇怪的副产品;可以直接创建 Employee 基类的实例。

// What exactly does this mean? Employee X = new Employee();

In this example, the only real purpose of the Employee base class is to define common members for all subclasses. In all likelihood, you did not intend anyone to create a direct instance of this class, reason being that the Employee type itself is too general of a concept. For example, if I were to walk up to you and say “I’m an employee,” I would bet your first question to me would be “What kind of employee are you? Are you a consultant, trainer, admin assistant, copy editor, or White House aide?”
在此示例中,Employee 基类的唯一实际用途是为所有子类定义公共成员。您很可能不打算让任何人创建此类的直接实例,原因是 Employee 类型本身的概念过于笼统。例如,如果我走到你面前说“我是一名员工”,我敢打赌你问我的第一个问题是“你是什么样的员工?你是顾问、培训师、行政助理、文案编辑还是白宫助手?”

Given that many base classes tend to be rather nebulous entities, a far better design for this example is to prevent the ability to directly create a new Employee object in code. In C#, you can enforce this
programmatically by using the abstract keyword in the class definition, thus creating an abstract base class.
鉴于许多基类往往是相当模糊的实体,此示例的更好设计是防止在代码中直接创建新的 Employee 对象。在 C# 中,可以强制执行此操作以编程方式在类定义中使用 abstract 关键字,从而创建抽象基类。

// Update the Employee class as abstract
// to prevent direct instantiation. abstract partial class Employee
{

}

With this, if you now attempt to create an instance of the Employee class, you are issued a compile- time error.
这样,如果您现在尝试创建 Employee 类的实例,则会发出编译时错误。

// Error! Cannot create an instance of an abstract class! Employee X = new Employee();

At first glance, it might seem strange to define a class that you cannot directly create an instance of. Recall, however, that base classes (abstract or not) are useful, in that they contain all the common data and functionality of derived types. Using this form of abstraction, you are able to model that the “idea” of an employee is completely valid; it is just not a concrete entity. Also understand that although you cannot directly create an instance of an abstract class, it is still assembled in memory when derived classes are created. Thus, it is perfectly fine (and common) for abstract classes to define any number of constructors that are called indirectly when derived classes are allocated.
乍一看,定义一个不能直接创建实例的类似乎很奇怪。但是,请记住,基类(抽象或非抽象)是有用的,因为它们包含派生类型的所有通用数据和功能。使用这种抽象形式,您可以模拟员工的“想法”是完全有效的;它只是不是一个具体的实体。还要了解,尽管不能直接创建抽象类的实例,但在创建派生类时,它仍然在内存中组装。因此,抽象类定义任意数量的构造函数是完全可以的(也很常见),这些构造函数在分配派生类时间接调用。

At this point, you have constructed a fairly interesting employee hierarchy. You will add a bit more functionality to this application later in this chapter when examining C# casting rules. Until then, Figure 6-4 illustrates the crux of your current design.
至此,您已经构建了一个相当有趣的员工层次结构。在本章后面的检查 C# 转换规则时,您将向此应用程序添加更多功能。在此之前,图 6-4 说明了当前设计的关键。

Alt text
Figure 6-4. The employee hierarchy
图 6-4。 员工层次结构

Understanding the Polymorphic Interface

了解多态界面

When a class has been defined as an abstract base class (via the abstract keyword), it may define any number of abstract members. Abstract members can be used whenever you want to define a member that does not supply a default implementation but must be accounted for by each derived class. By doing so, you enforce a polymorphic interface on each descendant, leaving them to contend with the task of providing the details behind your abstract methods.
当一个类被定义为抽象基类(通过抽象关键字)时,它可以定义任意数量的抽象成员。每当要定义不提供默认实现但必须由每个派生类考虑的成员时,都可以使用抽象成员。通过这样做,您可以在每个后代上强制实施多态接口,让他们应对提供抽象方法背后的细节的任务。

Simply put, an abstract base class’s polymorphic interface simply refers to its set of virtual and abstract methods. This is much more interesting than first meets the eye because this trait of OOP allows you to build easily extendable and flexible software applications. To illustrate, you will be implementing (and slightly modifying) the hierarchy of shapes briefly examined in Chapter 5 during the overview of the pillars of OOP. To begin, create a new C# Console Application project named Shapes.
简单地说,抽象基类的多态接口只是指代它的虚拟和抽象方法集。这比第一次看到的要有趣得多,因为OOP的这种特性允许您构建易于扩展和灵活的软件应用程序。为了说明这一点,您将实现(并稍微修改)第 5 章中简要检查的形状层次结构 ,在概述OOP。首先,创建一个名为 Shapes 的新 C# 控制台应用程序项目。
In Figure 6-5, notice that the Hexagon and Circle types each extend the Shape base class. Like any base class, Shape defines a number of members (a PetName property and Draw() method, in this case) that are common to all descendants.
在图 6-5 中,请注意,六边形和圆形类型分别扩展了 Shape 基类。与任何基类一样,Shape 定义了所有后代共有的许多成员(在本例中为 PetName 属性和 Draw() 方法)。

Alt text
Figure 6-5. The shapes hierarchy
图 6-5。 形状层次结构

Much like the employee hierarchy, you should be able to tell that you don’t want to allow the object user to create an instance of Shape directly, as it is too abstract of a concept. Again, to prevent the direct creation of the Shape type, you could define it as an abstract class. As well, given that you want the derived types
to respond uniquely to the Draw() method, let’s mark it as virtual and define a default implementation. Notice that the constructor is marked as protected so it can be called only from derived classes.
与员工层次结构非常相似,您应该能够判断出您不希望允许对象用户直接创建 Shape 的实例,因为它的概念太抽象了。同样,为了防止直接创建 Shape 类型,可以将其定义为抽象类。同样,鉴于您需要派生类型为了唯一地响应 Draw() 方法,让我们将其标记为虚拟并定义一个默认实现。请注意,构造函数被标记为受保护,因此只能从派生类调用它。

// The abstract base class of the hierarchy. namespace Shapes;
abstract class Shape
{
protected Shape(string name = "NoName")
{
PetName = name;
}

public string PetName { get; set; }

// A single virtual method. public virtual void Draw()
{
Console.WriteLine("Inside Shape.Draw()");
}
}

Notice that the virtual Draw() method provides a default implementation that simply prints out a message that informs you that you are calling the Draw() method within the Shape base class. Now recall that when a method is marked with the virtual keyword, the method provides a default implementation that all derived types automatically inherit. If a child class so chooses, it may override the method but does not have to. Given this, consider the following implementation of the Circle and Hexagon types:
请注意,虚拟 Draw() 方法提供了一个默认实现,该实现只是打印出一条消息,通知您正在 Shape 基类中调用 Draw() 方法。 现在回想一下,当一个方法被标记为 virtual 关键字时,该方法会提供一个所有派生类型都会自动继承的默认实现。如果子类选择这样做,它可以重写该方法,但不必重写。鉴于此,请考虑以下圆形和六边形类型的实现:

//Circle.cs namespace Shapes;
// Circle DOES NOT override Draw(). class Circle : Shape
{
public Circle() {}
public Circle(string name) : base(name){}
}

//Hexagon.cs namespace Shapes;
// Hexagon DOES override Draw(). class Hexagon : Shape
{
public Hexagon() {}
public Hexagon(string name) : base(name){} public override void Draw()
{
Console.WriteLine("Drawing {0} the Hexagon", PetName);
}
}

The usefulness of abstract methods becomes crystal clear when you once again remember that subclasses are never required to override virtual methods (as in the case of Circle). Therefore, if you create an instance of the Hexagon and Circle types, you’d find that Hexagon understands how to “draw” itself correctly or at least print out an appropriate message to the console. Circle, however, is more than a bit confused.
当你再次记住子类永远不需要覆盖虚拟方法(如 Circle 的情况)时,抽象方法的有用性变得非常明显。因此,如果您创建 Hexagon 和 Circle 类型的实例,您会发现 Hexagon 知道如何正确“绘制”自身或至少将适当的消息打印到控制台。然而,Circle却有点困惑。

using Shapes;
Console.WriteLine(" Fun with Polymorphism \n");

Hexagon hex = new Hexagon("Beth"); hex.Draw();
Circle cir = new Circle("Cindy");
// Calls base class implementation! cir.Draw();
Console.ReadLine();

Now consider the following output of the previous code:
现在考虑前面代码的以下输出:

Fun with Polymorphism Drawing Beth the Hexagon
Inside Shape.Draw()

Clearly, this is not an intelligent design for the current hierarchy. To force each child class to override the Draw() method, you can define Draw() as an abstract method of the Shape class, which by definition means you provide no default implementation whatsoever. To mark a method as abstract in C#, you use the abstract keyword. Notice that abstract members do not provide any implementation whatsoever.
显然,这不是当前层次结构的智能设计。若要强制每个子类重写 Draw() 方法,可以将 Draw() 定义为 Shape 类的抽象方法,根据定义,这意味着您不提供任何默认实现。 若要在 C# 中将方法标记为抽象,请使用 abstract 关键字。请注意,抽象成员不提供任何实现。

abstract class Shape
{
// Force all child classes to define how to be rendered. public abstract void Draw();

}

■ Note Abstract methods can be defined only in abstract classes. If you attempt to do otherwise, you will be issued a compiler error.
注意 抽象方法只能在抽象类中定义。如果尝试不这样做,将向您发出编译器错误。

Methods marked with abstract are pure protocol. They simply define the name, return type (if any), and parameter set (if required). Here, the abstract Shape class informs the derived types that “I have a method named Draw() that takes no arguments and returns nothing. If you derive from me, you figure out the details.”
用抽象标记的方法纯协议。它们只是定义名称、返回类型(如果有)和参数集(如果需要)。在这里,抽象的 Shape 类通知派生类型“我有一个名为 Draw() 的方法,它不带任何参数,也不返回任何内容。如果你从我这里得到,你就知道细节了。

Given this, you are now obligated to override the Draw() method in the Circle class. If you do not, Circle is also assumed to be a non-creatable abstract type that must be adorned with the abstract keyword (which is obviously not useful in this example). Here is the code update:
鉴于此,您现在有义务重写 Circle 类中的 Draw() 方法。如果你不这样做,Circle 也被假定为一个不可创建的抽象类型,必须用抽象关键字来装饰(这在本例中显然没有用)。以下是代码更新:

// If we did not implement the abstract Draw() method, Circle would also be
// considered abstract, and would have to be marked abstract! class Circle : Shape

{
public Circle() {}
public Circle(string name) : base(name) {} public override void Draw()
{
Console.WriteLine("Drawing {0} the Circle", PetName);
}
}

The short answer is that you can now assume that anything deriving from Shape does indeed have a unique version of the Draw() method. To illustrate the full story of polymorphism, consider the following code:
简短的回答是,您现在可以假设从 Shape 派生的任何内容确实具有 Draw() 方法的唯一版本。为了说明多态性的完整故事,请考虑以下代码:

Console.WriteLine(" Fun with Polymorphism \n");

// Make an array of Shape-compatible objects.
Shape[] myShapes = {new Hexagon(), new Circle(), new Hexagon("Mick"), new Circle("Beth"), new Hexagon("Linda")};

// Loop over each item and interact with the
// polymorphic interface. foreach (Shape s in myShapes)
{
s.Draw();
}
Console.ReadLine();

Here is the output from the modified code:
以下是修改后的代码的输出:

Fun with Polymorphism Drawing NoName the Hexagon Drawing NoName the Circle
Drawing Mick the Hexagon Drawing Beth the Circle Drawing Linda the Hexagon

This code illustrates polymorphism at its finest. Although it is not possible to directly create an instance of an abstract base class (the Shape), you are able to freely store references to any subclass with an abstract base variable. Therefore, when you are creating an array of Shapes, the array can hold any object deriving from the Shape base class (if you attempt to place Shape-incompatible objects into the array, you receive a compiler error).
此代码最能说明多态性。虽然不能直接创建抽象基类(Shape)的实例,但您可以使用抽象基变量自由存储对任何子类的引用。因此,在创建 Shape s 数组时,该数组可以保存从 Shape 基类派生的任何对象(如果尝试将形状不兼容的对象放入数组中,则会收到编译器错误)。

Given that all items in the myShapes array do indeed derive from Shape, you know they all support the same “polymorphic interface” (or said more plainly, they all have a Draw() method). As you iterate over the array of Shape references, it is at runtime that the underlying type is determined. At this point, the correct version of the Draw() method is invoked in memory.
鉴于 myShapes 数组中的所有项确实都派生自 Shape,您知道它们都支持相同的“多态接口”(或者更直白地说,它们都有一个 Draw() 方法)。循环访问 Shape 引用数组时,将在运行时确定基础类型。此时,将在内存中调用正确版本的 Draw() 方法。

This technique also makes it simple to safely extend the current hierarchy. For example, assume you derived more classes from the abstract Shape base class (Triangle, Square, etc.). Because of the
polymorphic interface, the code within your foreach loop would not have to change in the slightest, as the compiler enforces that only Shape-compatible types are placed within the myShapes array.
此技术还使安全地扩展当前层次结构变得简单。例如,假设您从抽象 Shape 基类(三角形、正方形等)派生了更多类。因为多态接口,foreach 循环中的代码不必发生丝毫更改,因为编译器强制将仅与 Shape 兼容的类型放置在 myShapes 数组中。

Understanding Member Shadowing

了解成员重影

C# provides a facility that is the logical opposite of method overriding, termed shadowing. Formally speaking, if a derived class defines a member that is identical to a member defined in a base class, the derived class has shadowed the parent’s version. In the real world, the possibility of this occurring is the greatest when you are subclassing from a class you (or your team) did not create yourself (such as when you purchase a third-party software package).
C# 提供了一种与方法重写(称为重影)在逻辑上相反的工具。从形式上讲,如果派生类定义的成员与基类中定义的成员相同,则派生类将隐藏父级的版本。在现实世界中,当您从您(或您的团队)不是自己创建的类进行子类时(例如当您购买第三方软件包时),发生这种情况的可能性最大。

For the sake of illustration, assume you receive a class named ThreeDCircle from a co-worker (or classmate) that defines a subroutine named Draw() taking no arguments.
为了便于说明,假设您从同事(或同学)那里收到一个名为 ThreeDCircle 的类,该类定义了一个名为 Draw() 的子例程,不带任何参数。

namespace Shapes; class ThreeDCircle
{
public void Draw()
{
Console.WriteLine("Drawing a 3D Circle");
}
}

You figure that ThreeDCircle “is-a” Circle, so you derive from your existing Circle type.
您认为 ThreeDCircle “是一个”圆,因此您从现有的 Circle 类型派生。

class ThreeDCircle : Circle
{
public void Draw()
{
Console.WriteLine("Drawing a 3D Circle");
}
}

After you recompile, you find the following warning:
重新编译后,您会看到以下警告:

‘ThreeDCircle.Draw()’ hides inherited member ‘Circle.Draw()’. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.

The problem is that you have a derived class (ThreeDCircle) that contains a method that is identical to an inherited method. To address this issue, you have a few options. You could simply update the child’s version of Draw() using the override keyword (as suggested by the compiler). With this approach, the ThreeDCircle type is able to extend the parent’s default behavior as required. However, if you don’t have access to the code defining the base class (again, as would be the case in many third-party libraries), you would be unable to modify the Draw() method as a virtual member, as you don’t have access to the code file!
问题是您有一个派生类 (ThreeDCircle),其中包含与继承方法相同的方法。要解决此问题,您有几种选择。您可以简单地使用 override 关键字更新子版本的 Draw()(如编译器建议的那样)。使用此方法,ThreeDCircle 类型能够根据需要扩展父项的默认行为。但是,如果您无权访问定义基类的代码(同样,就像许多第三方库中的情况一样),则将无法将 Draw() 方法修改为虚拟成员,因为您无权访问代码文件!

As an alternative, you can include the new keyword to the offending Draw() member of the derived type (ThreeDCircle, in this example). Doing so explicitly states that the derived type’s implementation is intentionally designed to effectively ignore the parent’s version (again, in the real world, this can be helpful if external software somehow conflicts with your current software).
作为替代方法,您可以将 new 关键字包含在派生类型(在本例中为 ThreeDCircle)的违规 Draw() 成员中。这样做显式声明派生类型的实现是有意设计为有效地忽略父版本(同样,在现实世界中,如果外部软件与您当前的软件发生冲突,这可能会很有帮助)。

// This class extends Circle and hides the inherited Draw() method. class ThreeDCircle : Circle
{
// Hide any Draw() implementation above me. public new void Draw()

{
Console.WriteLine("Drawing a 3D Circle");
}
}

You can also apply the new keyword to any member type inherited from a base class (field, constant, static member, or property). As a further example, assume that ThreeDCircle wants to hide the inherited PetName property.
还可以将 new 关键字应用于从基类继承的任何成员类型(字段、常量、静态成员或属性)。作为进一步的示例,假设 ThreeDCircle 想要隐藏继承的 PetName 属性。

class ThreeDCircle : Circle
{
// Hide the PetName property above me. public new string PetName { get; set; }

// Hide any Draw() implementation above me. public new void Draw()
{
Console.WriteLine("Drawing a 3D Circle");
}
}

Finally, be aware that it is still possible to trigger the base class implementation of a shadowed member using an explicit cast, as described in the next section. The following code shows an example:
最后,请注意,仍然可以使用显式强制转换触发重影成员的基类实现,如下一节所述。以下代码显示了一个示例:


// This calls the Draw() method of the ThreeDCircle. ThreeDCircle o = new ThreeDCircle();
o.Draw();

// This calls the Draw() method of the parent! ((Circle)o).Draw();
Console.ReadLine();

Understanding Base Class/Derived Class Casting Rules

了解基类/派生类转换规则

Now that you can build a family of related class types, you need to learn the rules of class casting operations. To do so, let’s return to the employee hierarchy created earlier in this chapter and add some new methods to the Program.cs file (if you are following along, open the Employees project). As described later in this chapter, the ultimate base class in the system is System.Object. Therefore, everything “is-an” Object and can be treated as such. Given this fact, it is legal to store an instance of any type within an object variable.
现在,您可以构建一系列相关的类类型,您需要学习类转换操作的规则。为此,让我们返回到本章前面创建的员工层次结构,并向 Program.cs 文件添加一些新方法(如果您正在继续操作,请打开 Employees 项目)。如本章后面所述,系统中的最终基类是 System.Object。因此,一切都“是”对象,可以这样对待。鉴于这一事实,在对象变量中存储任何类型的实例都是合法的。

static void CastingExamples()
{
// A Manager "is-a" System.Object, so we can
// store a Manager reference in an object variable just fine.
object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5);
}

In the Employees project, Managers, SalesPerson, and PtSalesPerson types all extend Employee, so you can store any of these objects in a valid base class reference. Therefore, the following statements are also legal:
在“员工”项目中,“经理”、“销售人员”和“PtSales人员”类型都扩展了“员工”,因此您可以将这些对象中的任何一个存储在有效的基类引用中。因此,以下声明也是合法的:

static void CastingExamples()
{
// A Manager "is-a" System.Object, so we can
// store a Manager reference in an object variable just fine.
object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5);

// A Manager "is-an" Employee too.
Employee moonUnit = new Manager("MoonUnit Zappa", 2, 3001, 20000, "101-11-1321", 1);

// A PtSalesPerson "is-a" SalesPerson.
SalesPerson jill = new PtSalesPerson("Jill", 834, 3002, 100000, "111-12-1119", 90);
}

The first law of casting between class types is that when two classes are related by an “is-a” relationship, it is always safe to store a derived object within a base class reference. Formally, this is called an implicit cast, as “it just works” given the laws of inheritance. This leads to some powerful programming constructs. For example, assume you have defined a new method within your current Program.cs file.
在类类型之间进行强制转换的第一定律是,当两个类通过“is-a”关系关联时,将派生对象存储在基类引用中始终是安全的。从形式上讲,这被称为隐式强制转换,因为考虑到继承法则,“它只是有效”。这导致了一些强大的编程结构。例如,假设您已在当前 Program.cs 文件中定义了一个新方法。

static void GivePromotion(Employee emp)
{
// Increase pay…
// Give new parking space in company garage…

Console.WriteLine("{0} was promoted!", emp.Name);
}

Because this method takes a single parameter of type Employee, you can effectively pass any descendant from the Employee class into this method directly, given the “is-a” relationship.
由于此方法采用 Employee 类型的单个参数,因此在给定“is-a”关系的情况下,可以有效地将 Employee 类中的任何后代直接传递到此方法中。

static void CastingExamples()
{
// A Manager "is-a" System.Object, so we can
// store a Manager reference in an object variable just fine.
object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5);

// A Manager "is-an" Employee too.
Employee moonUnit = new Manager("MoonUnit Zappa", 2, 3001, 20000, "101-11-1321", 1); GivePromotion(moonUnit);

// A PTSalesPerson "is-a" SalesPerson.
SalesPerson jill = new PtSalesPerson("Jill", 834, 3002, 100000, "111-12-1119", 90); GivePromotion(jill);
}

The previous code compiles given the implicit cast from the base class type (Employee) to the derived type. However, what if you also wanted to promote Frank Zappa (currently stored in a general System.
Object reference)? If you pass the frank object directly into this method, you will find a compiler error as follows:
前面的代码在给定从基类类型 (Employee) 到派生类型的隐式强制转换的情况下进行编译。但是,如果您还想推广弗兰克扎帕(目前存储在通用系统中。对象引用)?如果将 frank 对象直接传递到此方法中,则会发现编译器错误,如下所示:

object frank = new Manager("Frank Zappa", 9, 3000, 40000, "111-11-1111", 5);
// Error!
GivePromotion(frank);

The problem is that you are attempting to pass in a variable that is not declared as an Employee but a more general System.Object. Given that object is higher up the inheritance chain than Employee, the compiler will not allow for an implicit cast, in an effort to keep your code as type-safe as possible.
问题是您正在尝试传入一个未声明为 Employee 而是更通用的 System.Object 的变量。假设该对象在继承链中高于 Employee,编译器将不允许隐式强制转换,以使代码尽可能类型安全。

Even though you can figure out that the object reference is pointing to an Employee-compatible class in memory, the compiler cannot, as that will not be known until runtime. You can satisfy the compiler by performing an explicit cast. This is the second law of casting: you can, in such cases, explicitly downcast using the C# casting operator. The basic template to follow when performing an explicit cast looks something like the following:
即使您可以确定对象引用指向内存中与 Employee 兼容的类,编译器也不能,因为直到运行时才会知道。可以通过执行显式强制转换来满足编译器的要求。这是强制转换的第二定律:在这种情况下,可以使用 C# 强制转换运算符显式向下转换。执行显式强制转换时要遵循的基本模板如下所示:

(ClassIWantToCastTo)referenceIHave

Thus, to pass the object variable into the GivePromotion() method, you can author the following code:
因此,要将对象变量传递到 GivePromotion() 方法中,您可以编写以下代码:

// OK! GivePromotion((Manager)frank);

Using the C# as Keyword

使用 C# 作为关键字

Be aware that explicit casting is evaluated at runtime, not compile time. For the sake of argument, assume your Employees project had a copy of the Hexagon class created earlier in this chapter. For simplicity, you can add the following class to the current project:
请注意,显式强制转换是在运行时计算的,而不是在编译时计算的。为了便于讨论,假设您的 Employees 项目具有本章前面创建的 Hexagon 类的副本。为简单起见,可以将以下类添加到当前项目中:

namespace Shapes; class Hexagon
{
public void Draw()
{
Console.WriteLine("Drawing a hexagon!");
}
}

Although casting the Employee object to a shape object makes absolutely no sense, code such as the following could compile without error:
尽管将 Employee 对象强制转换为形状对象绝对没有意义,但如下代码可以编译而不会出错:
// Ack! You can’t cast frank to a Hexagon, but this compiles fine! object frank = new Manager();
Hexagon hex = (Hexagon)frank;

However, you would receive a runtime error, or, more formally, a runtime exception. Chapter 7 will examine the full details of structured exception handling; however, it is worth pointing out, for the time being, that when you are performing an explicit cast, you can trap the possibility of an invalid cast using the try and catch keywords (again, see Chapter 7 for full details).
但是,您将收到运行时错误,或者更正式地说,会收到运行时异常。第7章将研究结构化异常处理的全部细节;但是,值得指出的是,暂时,当您执行显式强制转换时,您可以使用 try 和 catch 关键字捕获无效转换的可能性(同样,有关完整详细信息,请参阅第 7 章)。

// Catch a possible invalid cast. object frank = new Manager(); Hexagon hex;
try
{
hex = (Hexagon)frank;
}
catch (InvalidCastException ex)
{
Console.WriteLine(ex.Message);
}

Obviously, this is a contrived example; you would never bother casting between these types in this situation. However, assume you have an array of System.Object types, only a few of which contain Employee-compatible objects. In this case, you would like to determine whether an item in an array is compatible to begin with and, if so, perform the cast.
显然,这是一个人为的例子;在这种情况下,您永远不会在这些类型之间进行转换。但是,假设您有一个 System.Object 类型的数组,其中只有少数包含与员工兼容的对象。在这种情况下,您希望确定数组中的项是否兼容,如果是,则执行强制转换。

C# provides the as keyword to quickly determine at runtime whether a given type is compatible with another. When you use the as keyword, you are able to determine compatibility by checking against a null return value. Consider the following:
C# 提供 as 关键字,用于在运行时快速确定给定类型是否与另一个类型兼容。使用 as 关键字时,可以通过检查 null 返回值来确定兼容性。请考虑以下事项:

// Use "as" to test compatibility. object[] things = new object[4]; things[0] = new Hexagon(); things[1] = false;
things[2] = new Manager(); things[3] = "Last thing";

foreach (object item in things)
{
Hexagon h = item as Hexagon; if (h == null)
{
Console.WriteLine("Item is not a hexagon");
}
else
{
h.Draw();
}
}

Here, you loop over each item in the array of objects, checking each one for compatibility with the Hexagon class. If (and only if!) you find a Hexagon-compatible object, you invoke the Draw() method. Otherwise, you simply report the items are not compatible.
在这里,您遍历对象数组中的每个项目,检查每个项目是否与 Hexagon 类兼容。如果(并且仅当!)找到一个与 Hexagon 兼容的对象,则调用 Draw() 方法。否则,您只需报告项目不兼容。

Using the C# is Keyword (Updated 7.0, 9.0)

使用 C# is 关键字(更新 7.0、9.0)

In addition to the as keyword, the C# language provides the is keyword to determine whether two items are compatible. Unlike the as keyword, however, the is keyword returns false, rather than a null reference, if the types are incompatible. Currently, the GivePromotion() method has been designed to take any possible type derived from Employee. Consider the following update, which now checks to see exactly which “type of employee” has been passed in:
除了 as 关键字之外,C# 语言还提供了 is 关键字来确定两个项是否兼容。但是,与 as 关键字不同,如果类型不兼容,则 is 关键字返回 false,而不是 null 引用。目前,GivePromotion() 方法已被设计为采用从 Employee 派生的任何可能类型。请考虑以下更新,该更新现在检查是否准确查看传入的“员工类型”:

static void GivePromotion(Employee emp)
{
Console.WriteLine("{0} was promoted!", emp.Name); if (emp is SalesPerson)
{
Console.WriteLine("{0} made {1} sale(s)!", emp.Name, ((SalesPerson)emp).SalesNumber); Console.WriteLine();
}
else if (emp is Manager)
{
Console.WriteLine("{0} had {1} stock options…", emp.Name, ((Manager)emp).StockOptions); Console.WriteLine();
}
}

Here, you are performing a runtime check to determine what the incoming base class reference is actually pointing to in memory. After you determine whether you received a SalesPerson or Manager type, you are able to perform an explicit cast to gain access to the specialized members of the class. Also notice that you are not required to wrap your casting operations within a try/catch construct, as you know that the cast is safe if you enter either if scope, given your conditional check.
在这里,您将执行运行时检查,以确定传入基类引用在内存中实际指向的内容。确定是否收到“销售人员”或“经理”类型后,可以执行显式强制转换以获取对类中专用成员的访问权限。另请注意,您不需要将转换操作包装在 try/catch 构造中,因为您知道,如果输入 if 范围,给定条件检查,则转换是安全的。

New in C# 7.0, the is keyword can also assign the converted type to a variable if the cast works. This cleans up the preceding method by preventing the “double-cast” problem. In the preceding example, the first cast is done when checking to see whether the type matches, and if it does, then the variable has to be cast again. Consider this update to the preceding method:
在这里,您将执行运行时检查,以确定传入基类引用在内存中实际指向的内容。确定是否收到“销售人员”或“经理”类型后,可以执行显式强制转换以获取对类中专用成员的访问权限。另请注意,您不需要将转换操作包装在 try/catch 构造中,因为您知道,如果输入 if 范围,给定条件检查,则转换是安全的。

static void GivePromotion(Employee emp)
{
Console.WriteLine("{0} was promoted!", emp.Name);
//Check if is SalesPerson, assign to variable s if (emp is SalesPerson s)
{
Console.WriteLine("{0} made {1} sale(s)!", s.Name, s.SalesNumber); Console.WriteLine();
}
//Check if is Manager, if it is, assign to variable m else if (emp is Manager m)
{
Console.WriteLine("{0} had {1} stock options…", m.Name, m.StockOptions); Console.WriteLine();
}
}

C# 9.0 introduced additional pattern matching capabilities (covered in Chapter 3). These updated pattern matches can be used with the is keyword. For example, to check if the employee is not a Manager and not a SalesPerson, use the following code:
C# 9.0 引入了其他模式匹配功能(在第 3 章中介绍)。这些更新的模式匹配可以与 is 关键字一起使用。例如,若要检查员工是否不是经理,也不是销售人员,请使用以下代码:

if (emp is not Manager and not SalesPerson)
{
Console.WriteLine("Unable to promote {0}. Wrong employee type", emp.Name); Console.WriteLine();
}

Discards with the is Keyword (New 7.0)

使用 is 关键字丢弃(新版 7.0)

The is keyword can also be used in conjunction with the discard variable placeholder. If you want to create a catchall in your if or switch statement, you can do so as follows:
is 关键字也可以与丢弃变量占位符结合使用。如果要在 if 或 switch 语句中创建包罗万象,可以按如下方式执行此操作:

if (obj is var _)
{
//do something
}

This will match everything, so be careful about the order in which you use the comparer with the discard. The updated GivePromotion() method is shown here:
is 关键字也可以与丢弃变量占位符结合使用。如果要在 if 或 switch 语句中创建包罗万象,可以按如下方式执行此操作:

if (emp is SalesPerson s)
{
Console.WriteLine("{0} made {1} sale(s)!", s.Name, s.SalesNumber); Console.WriteLine();
}
//Check if is Manager, if it is, assign to variable m else if (emp is Manager m)
{
Console.WriteLine("{0} had {1} stock options…", m.Name, m.StockOptions); Console.WriteLine();
}
else if (emp is var _)
{
Console.WriteLine("Unable to promote {0}. Wrong employee type", emp.Name); Console.WriteLine();
}

The final if statement will catch any Employee instance that is not a Manager, SalesPerson, or PtSalesPerson. Remember that you can downcast to a base class, so the PtSalesPerson will register as a SalesPerson.
is 关键字也可以与丢弃变量占位符结合使用。如果要在 if 或 switch 语句中创建包罗万象,可以按如下方式执行此操作:

Revisiting Pattern Matching (New 7.0)

重新审视模式匹配(新 7.0)

Chapter 3 introduced the C# 7 feature of pattern matching along with the updates that came with C# 9.0. Now that you have a firm understanding of casting, it’s time for a better example. The preceding example can now be cleanly updated to use a pattern matching switch statement, as follows:
第 3 章介绍了模式匹配的 C# 7 功能以及 C# 9.0 附带的更新。既然您对铸造有了深刻的了解,那么是时候举一个更好的例子了。前面的示例现在可以干净地更新为使用模式匹配开关语句,如下所示:

static void GivePromotion(Employee emp)
{
Console.WriteLine("{0} was promoted!", emp.Name); switch (emp)
{
case SalesPerson s:
Console.WriteLine("{0} made {1} sale(s)!", emp.Name, s.SalesNumber); break;
case Manager m:
Console.WriteLine("{0} had {1} stock options…", emp.Name, m.StockOptions); break;
}
Console.WriteLine();
}

When adding a when clause to the case statement, the full definition of the object as it is cast is available for use. For example, the SalesNumber property exists only on the SalesPerson class and not the Employee class. If the cast in the first case statement succeeds, the variable s will hold an instance of a SalesPerson class, so the case statement could be updated to the following:
将 when 子句添加到 case 语句时,可以使用对象转换时的完整定义。例如,属性仅存在于 SalesPerson 类上,而不存在于 Employee 类上。如果第一个 case 语句中的强制转换成功,则变量 s 将保存 SalesPerson 类的实例,因此 case 语句可以更新为以下内容:

case SalesPerson s when s.SalesNumber > 5:

These new additions to the is and switch statements provide nice improvements that help reduce the amount of code to perform matching, as the previous examples demonstrated.
正如前面的示例所示,对 is 和 switch 语句的这些新增功能提供了很好的改进,有助于减少执行匹配的代码量。

Discards with switch Statements (New 7.0)

使用开关语句丢弃(新版 7.0)

Discards can also be used in switch statements, as shown in the following code:
丢弃也可以用于 switch 语句,如以下代码所示:

switch (emp)
{
case SalesPerson s when s.SalesNumber > 5:
Console.WriteLine("{0} made {1} sale(s)!", emp.Name, s.SalesNumber); break;
case Manager m:
Console.WriteLine("{0} had {1} stock options…", emp.Name, m.StockOptions); break;
case Employee _:
Console.WriteLine("Unable to promote {0}. Wrong employee type", emp.Name); break;
}

Every type coming in is already an Employee, so the final case statement is always true. However, as discussed when pattern matching was introduced in Chapter 3, once a match is made, the switch statement is exited. This demonstrates the importance of getting the order correct. If the final statement was moved to the top, no Employee would ever be promoted.
每个进入的类型都已经是员工,因此最终的情况陈述始终为真。但是,正如第 3 章中引入模式匹配时所讨论的那样,一旦进行了匹配,switch 语句就会退出。这说明了正确排序的重要性。如果最终声明被移到顶部,则不会晋升任何员工。

Understanding the Super Parent Class: System.Object

了解超父类:System.Object

To wrap up this chapter, I’d like to examine the details of the super parent class: Object. As you were reading the previous section, you might have noticed that the base classes in your hierarchies (Car, Shape, Employee) never explicitly specify their parent classes.
为了结束本章,我想检查一下超级父类的细节:对象。在阅读上一节时,您可能已经注意到层次结构中的基类(汽车、形状、员工)从未显式指定其父类。

// Who is the parent of Car? class Car
{…}

In the .NET Core universe, every type ultimately derives from a base class named System.Object, which can be represented by the C# object keyword (lowercase o). The Object class defines a set of common members for every type in the framework. In fact, when you do build a class that does not explicitly define its parent, the compiler automatically derives your type from Object. If you want to be clear in your intentions, you are free to define classes that derive from Object as follows (however, again, there is no need to do so):
在 .NET Core 领域中,每个类型最终都派生自一个名为 System.Object 的基类,该基类可以用 C# object 关键字(小写 o)表示。Object 类为框架中的每个类型定义一组公共成员。事实上,当您生成一个未显式定义其父级的类时,编译器会自动从 Object 派生您的类型。如果你想明确你的意图,你可以自由地定义从 Object 派生的类,如下所示(但是,同样,没有必要这样做):

// Here we are explicitly deriving from System.Object. class Car : object
{…}

Like any class, System.Object defines a set of members. In the following formal C# definition, note that some of these items are declared virtual, which specifies that a given member may be overridden by a subclass, while others are marked with static (and are therefore called at the class level):
在 .NET Core 领域中,每个类型最终都派生自一个名为 System.Object 的基类,该基类可以用 C# object 关键字(小写 o)表示。Object 类为框架中的每个类型定义一组公共成员。事实上,当您生成一个未显式定义其父级的类时,编译器会自动从 Object 派生您的类型。如果你想明确你的意图,你可以自由地定义从 Object 派生的类,如下所示(但是,同样,没有必要这样做):

public class Object
{
// Virtual members.
public virtual bool Equals(object obj); protected virtual void Finalize(); public virtual int GetHashCode(); public virtual string ToString();

// Instance-level, nonvirtual members.
public Type GetType();
protected object MemberwiseClone();

// Static members.
public static bool Equals(object objA, object objB);
public static bool ReferenceEquals(object objA, object objB);
}

Table 6-1 offers a rundown of the functionality provided by some of the methods you’re most likely to use.
表 6-1 简要介绍了您最有可能使用的某些方法所提供的功能。

Table 6-1. Core Members of System.Object
表 6-1. System.Object 的核心成员

Instance Method of Object Class
对象的实例方法
Meaning in Life
Equals() By default, this method returns true only if the items being compared refer to the same item in memory. Thus, Equals() is used to compare object references, not the state of the object. Typically, this method is overridden to return true only if the objects being compared have the same internal state values (i.e., value-based semantics).Be aware that if you override Equals(), you should also override GetHashCode(), as these methods are used internally by Hashtable types to retrieve subobjects from the container.Also recall from Chapter 4 that the ValueType class overrides this method for all structures, so they work with value-based comparisons.
默认情况下,仅当要比较的项引用内存中的同一项时,此方法才返回 true。因此,Equals() 用于比较对象引用,而不是对象的状态。通常,仅当要比较的对象具有相同的内部状态值(即基于值的语义)时,才会重写此方法以返回 true。请注意,如果重写 Equals(),则还应覆盖 GetHashCode(),因为这些方法由 Hashtable 类型在内部用于从容器中检索子对象。还要回顾一下第 4 章,ValueType 类对所有结构都重写了此方法,因此它们使用基于值的比较。
Finalize() For the time being, you can understand this method (when overridden) is called to free any allocated resources before the object is destroyed. I talk more about the CoreCLR garbage collection services in Chapter 9.
目前,您可以理解调用此方法(重写时)是为了在销毁对象之前释放任何已分配的资源。我将在第 9 章中详细介绍 CoreCLR 垃圾回收服务。
GetHashCode() This method returns an int that identifies a specific object instance.
此方法返回标识特定对象实例的 int。
ToString() This method returns a string representation of this object, using the <namespace>.<type name> format (termed the fully qualified name). This method will often be overridden by a subclass to return a tokenized string of name-value pairs that represent the object’s internal state, rather than its fully qualified name.
此方法返回此对象的字符串表示形式,使用<命名空间>.<键入名称>格式(称为完全限定名)。此方法通常会被子类重写,以返回表示对象内部状态的名称-值对的标记化字符串,而不是其完全限定名称。
GetType() This method returns a Type object that fully describes the object you are currently referencing. In short, this is a runtime type identification (RTTI) method available to all objects (discussed in greater detail in Chapter 17).
此方法返回一个 Type 对象,该对象完全描述当前引用的对象。简而言之,这是一种适用于所有对象的运行时类型识别(RTTI)方法(在第17章中有更详细的讨论)。
MemberwiseClone() This method exists to return a member-by-member copy of the current object, which is often used when cloning an object (see Chapter 8).
此方法的存在是为了返回当前对象的成员副本,该副本通常在克隆对象时使用(请参阅第 8 章)。

To illustrate some of the default behavior provided by the Object base class, create a final C# Console Application project named ObjectOverrides. Insert a new C# class type that contains the following empty class definition for a type named Person:
若要说明 Object 基类提供的一些默认行为,请创建一个名为 ObjectOverride 的最终 C# 控制台应用程序项目。插入一个新的 C# 类类型,其中包含名为 Person 的类型的以下空类定义:

namespace ObjectOverrides;
// Remember! Person extends Object. class Person
{
}

Now, update your top-level statements to interact with the inherited members of System.Object as follows:
现在,更新顶级语句以与 System.Object 的继承成员进行交互,如下所示:

using ObjectOverrides;
Console.WriteLine(" Fun with System.Object \n"); Person p1 = new Person();

// Use inherited members of System.Object. Console.WriteLine("ToString: {0}", p1.ToString()); Console.WriteLine("Hash code: {0}", p1.GetHashCode()); Console.WriteLine("Type: {0}", p1.GetType());

// Make some other references to p1. Person p2 = p1;
object o = p2;
// Are the references pointing to the same object in memory? if (o.Equals(p1) && p2.Equals(o))
{
Console.WriteLine("Same instance!");
}
Console.ReadLine();
}

Here is the output of the current code:

Fun with System.Object ToString: ObjectOverrides.Person Hash code: 58225482
Type: ObjectOverrides.Person Same instance!

Notice how the default implementation of ToString() returns the fully qualified name of the current type (ObjectOverrides.Person). As you will see later during the examination of building custom namespaces in Chapter 15, every C# project defines a “root namespace,” which has the same name of the project itself. Here, you created a project named ObjectOverrides; thus, the Person type and the Program. cs file have both been placed within the ObjectOverrides namespace.
请注意 ToString() 的默认实现如何返回当前类型 (ObjectOverrides.Person) 的完全限定名称。正如稍后在第 15 章中检查构建自定义命名空间时将看到的那样,每个 C# 项目都定义了一个“根命名空间”,该命名空间具有与项目本身。在这里,您创建了一个名为“对象覆盖”的项目;因此,人员类型和程序.cs文件都已放置在 ObjectOverrides 命名空间中。

The default behavior of Equals() is to test whether two variables are pointing to the same object in memory. Here, you create a new Person variable named p1. At this point, a new Person object is placed on the managed heap. p2 is also of type Person. However, you are not creating a new instance but rather assigning this variable to reference p1. Therefore, p1 and p2 are both pointing to the same object in memory, as is the variable o (of type object, which was thrown in for good measure). Given that p1, p2, and o all point to the same memory location, the equality test succeeds.
Equals() 的默认行为是测试两个变量是否指向内存中的同一对象。在这里,您将创建一个名为 p1 的新 Person 变量。此时,一个新的 Person 对象放置在托管堆上。p2 也是“人”类型。但是,您不是在创建新实例,而是在创建将此变量分配给引用 P1。因此,p1 和 p2 都指向内存中的同一对象,变量 o(对象类型,为了更好地测量而抛出)也是如此。假设 p1、p2 和 o 都指向相同的内存位置,则相等性测试成功。

Although the canned behavior of System.Object can fit the bill in a number of cases, it is quite common for your custom types to override some of these inherited methods. To illustrate, update the Person class to support some properties representing an individual’s first name, last name, and age, each of which can be set by a custom constructor.
尽管 System.Object 的固定行为在许多情况下可以满足要求,但自定义类型重写其中一些继承方法是很常见的。为了进行说明,请更新 Person 类以支持表示个人名字、姓氏和年龄的某些属性,每个属性都可以由自定义构造函数设置。

namespace ObjectOverrides;
// Remember! Person extends Object. class Person
{
public string FirstName { get; set; } = ""; public string LastName { get; set; } = ""; public int Age { get; set; }

public Person(string fName, string lName, int personAge)
{
FirstName = fName;
LastName = lName;
Age = personAge;
}
public Person(){}
}

Overriding System.Object.ToString( )

覆盖 System.Object.ToString( )

Many classes (and structures) that you create can benefit from overriding ToString() to return a string textual representation of the type’s current state. This can be quite helpful for purposes of debugging (among other reasons). How you choose to construct this string is a matter of personal choice; however, a recommended approach is to separate each name-value pair with semicolons and wrap the entire string within square brackets (many types in the .NET Core base class libraries follow this approach). Consider the following overridden ToString() for your Person class:
您创建的许多类(和结构)都可以从重写 ToString() 以返回类型当前状态的字符串文本表示形式中受益。这对于调试(以及其他原因)非常有用。您选择如何构造此字符串是个人选择的问题;但是,建议的方法是用分号分隔每个名称/值对,并将整个字符串括在方括号内(.NET Core 基类库中的许多类型都遵循此方法)。考虑以下重写的 ToString() 用于您的 Person 类:

public override string ToString() => $"[First Name: {FirstName}; Last Name: {LastName}; Age: {Age}]";

This implementation of ToString() is quite straightforward, given that the Person class has only three pieces of state data. However, always remember that a proper ToString() override should also account for any data defined up the chain of inheritance.
ToString() 的实现非常简单,因为 Person 类只有三段状态数据。但是,请始终记住,正确的 ToString() 覆盖也应该考虑继承链上定义的任何数据。

When you override ToString() for a class extending a custom base class, the first order of business is to obtain the ToString() value from your parent using the base keyword. After you have obtained your parent’s string data, you can append the derived class’s custom information.
当您重写扩展自定义基类的类的 ToString() 时,第一个业务顺序是使用 base 关键字从父级获取 ToString() 值。 获取父类的字符串数据后,可以追加派生类的自定义信息。

Overriding System.Object.Equals( )

覆盖系统.对象.等于( )

Let’s also override the behavior of Object.Equals() to work with value-based semantics. Recall that, by default, Equals() returns true only if the two objects being compared reference the same object instance in memory. For the Person class, it may be helpful to implement Equals() to return true if the two variables being compared contain the same state values (e.g., first name, last name, and age).
我们还覆盖 Object.Equals() 的行为以使用基于值的语义。回想一下,默认情况下,仅当要比较的两个对象引用内存中的同一对象实例时,Equals() 才返回 true。对于 Person 类,如果要比较的两个变量包含相同的状态值(例如,名字、姓氏和年龄),则实现 Equals() 返回 true 可能会有所帮助。
First, notice that the incoming argument of the Equals() method is a general System.Object. Given this, your first order of business is to ensure the caller has indeed passed in a Person object and, as an extra safeguard, to make sure the incoming parameter is not a null reference.
首先,请注意 Equals() 方法的传入参数是一个通用的 System.Object。鉴于此,您的第一个业务顺序是确保调用方确实传入了 Person 对象,并且作为额外的保护措施,确保传入参数不是 null 引用。

After you have established the caller has passed you an allocated Person, one approach to implement Equals() is to perform a field-by-field comparison against the data of the incoming object to the data of the current object.
在确定调用方已将已分配的人员传递给您后,实现 Equals() 的一种方法是对传入对象的数据与当前对象的数据执行逐字段比较。

public override bool Equals(object obj)
{
if (!(obj is Person temp))
{
return false;
}
if (temp.FirstName == this.FirstName && temp.LastName == this.LastName && temp.Age == this.Age)
{
return true;
}
return false;
}

Here, you are examining the values of the incoming object against the values of your internal values (note the use of the this keyword). If the names and age of each are identical, you have two objects with the same state data and, therefore, return true. Any other possibility results in returning false.
在这里,您正在根据内部值的值检查传入对象的值(请注意 this 关键字的使用)。如果每个对象的名称和期限相同,则有两个对象具有相同的状态数据,因此返回 true。任何其他可能性都会导致返回 false。

While this approach does indeed work, you can certainly imagine how labor intensive it would be to implement a custom Equals() method for nontrivial types that may contain dozens of data fields. One common shortcut is to leverage your own implementation of ToString(). If a class has a prim-and-proper implementation of ToString() that accounts for all field data up the chain of inheritance, you can simply perform a comparison of the object’s string data (checking for null).
虽然这种方法确实有效,但您当然可以想象为可能包含数十个数据字段的非平凡类型实现自定义 Equals() 方法是多么的劳动密集。一个常见的快捷方式是利用你自己的 ToString() 实现。如果一个类具有 ToString() 的原始和正确的实现,该实现考虑了继承链上的所有字段数据,则只需对对象的字符串数据进行比较(检查 null)。

// No need to cast "obj" to a Person anymore,
// as everything has a ToString() method. public override bool Equals(object obj)
=> obj?.ToString() == ToString();

Notice in this case that you no longer need to check whether the incoming argument is of the correct type (a Person, in this example), as everything in .NET supports a ToString() method. Even better, you no longer need to perform a property-by-property equality check, as you are now simply testing the value returned from ToString().
请注意,在这种情况下,您不再需要检查传入参数的类型是否正确(在本例中为 Person),因为 .NET 中的所有内容都支持 ToString() 方法。更好的是,您不再需要执行逐个属性的相等性检查,因为您现在只需测试从 ToString() 返回的值。

Overriding System.Object.GetHashCode()

覆盖System.Object.GetHashCode()

When a class overrides the Equals() method, you should also override the default implementation of GetHashCode(). Simply put, a hash code is a numerical value that represents an object as a particular state. For example, if you create two string variables that hold the value Hello, you will obtain the same hash code. However, if one of the string objects were in all lowercase (hello), you would obtain different hash codes.
当类重写 Equals() 方法时,还应覆盖 GetHashCode() 的默认实现。简单地说,哈希码是一个数值,表示对象作为特定状态。例如,如果创建两个保存值 Hello 的字符串变量,则将获得相同的哈希代码。但是,如果其中一个字符串对象全部为小写 (hello),您将获得不同的哈希代码。

By default, System.Object.GetHashCode() uses your object’s current location in memory to yield the hash value. However, if you are building a custom type that you intend to store in a Hashtable type (within the System.Collections namespace), you should always override this member, as the Hashtable will be internally invoking Equals() and GetHashCode() to retrieve the correct object.
当类重写 Equals() 方法时,还应覆盖 GetHashCode() 的默认实现。简单地说,哈希码是一个数值,表示对象作为特定状态。例如,如果创建两个保存值 Hello 的字符串变量,则将获得相同的哈希代码。但是,如果其中一个字符串对象全部为小写 (hello),您将获得不同的哈希代码。

■ Note To be more specific, the System.Collections.Hashtable class calls GetHashCode() internally to gain a general idea where the object is located, but a subsequent (internal) call to Equals() determines the exact match.
注意 更具体地说,System.Collections.Hashtable 类在内部调用 GetHashCode() 以获得对象所在的大致概念,但随后(内部)对 Equals() 的调用决定了完全匹配。

Although you are not going to place your Person into a System.Collections.Hashtable in this example, for completion let’s override GetHashCode(). There are many algorithms that can be used to create a hash code—some fancy, others not so fancy. Most of the time, you are able to generate a hash code value by leveraging the System.String’s GetHashCode() implementation.
注意 更具体地说,System.Collections.Hashtable 类在内部调用 GetHashCode() 以获得对象所在的大致概念,但随后(内部)对 Equals() 的调用决定了完全匹配。

Given that the String class already has a solid hash code algorithm that is using the character data of the String to compute a hash value, if you can identify a piece of field data on your class that should be unique for all instances (such as a Social Security number), simply call GetHashCode() on that point of field data. Thus, if the Person class defined an SSN property, you could author the following code:
假设 String 类已经有一个可靠的哈希代码算法,该算法使用 String 的字符数据来计算哈希值,如果您可以识别类上的一段字段数据,该字段数据应该是对于所有实例(例如社会保险号)都是唯一的,只需在该字段数据点上调用 GetHashCode()。 因此,如果 Person 类定义了 SSN 属性,则可以编写以下代码:

// Assume we have an SSN property as so. class Person
{
public string SSN {get; } = "";
public Person(string fName, string lName, int personAge, string ssn)
{

FirstName = fName;
LastName = lName;
Age = personAge;
SSN = ssn;
}
// Return a hash code based on unique string data. public override int GetHashCode() => SSN.GetHashCode();
}

If you use a read-write property for the basis of the hash code, you will receive a warning. Once an object is created, the hash code should be immutable. In the previous example, the SSN property has only a get method, which makes the property read-only, and can be set only in the constructor.
如果使用读写属性作为哈希代码的基础,则会收到警告。创建对象后,哈希代码应该是不可变的。在前面的示例中,SSN 属性只有一个 get 方法,该方法使该属性成为只读的,并且只能在构造函数中设置。

If you cannot find a single point of unique string data but you have overridden ToString() (which satisfies the read-only convention), call GetHashCode() on your own string representation.
如果找不到唯一字符串数据的单个点,但已覆盖 ToString()(满足只读约定),请在自己的字符串表示形式上调用 GetHashCode()。

// Return a hash code based on the person’s ToString() value. public override int GetHashCode() => ToString().GetHashCode();

Testing Your Modified Person Class

测试修改后的人员类

Now that you have overridden the virtual members of Object, update the top-level statements to test your updates.
现在,您已经覆盖了 Object 的虚拟成员,请更新顶级语句以测试更新。

// NOTE: We want these to be identical to test
// the Equals() and GetHashCode() methods.
Person p1 = new Person("Homer", "Simpson", 50, "111-11-1111"); Person p2 = new Person("Homer", "Simpson", 50, "111-11-1111");
// Get stringified version of objects. Console.WriteLine("p1.ToString() = {0}", p1.ToString()); Console.WriteLine("p2.ToString() = {0}", p2.ToString());

// Test overridden Equals().
Console.WriteLine("p1 = p2?: {0}", p1.Equals(p2));

// Test hash codes.
//still using the hash of the SSN
Console.WriteLine("Same hash codes?: {0}", p1.GetHashCode() == p2.GetHashCode()); Console.WriteLine();

// Change age of p2 and test again. p2.Age = 45;
Console.WriteLine("p1.ToString() = {0}", p1.ToString()); Console.WriteLine("p2.ToString() = {0}", p2.ToString()); Console.WriteLine("p1 = p2?: {0}", p1.Equals(p2));
//still using the hash of the SSN
Console.WriteLine("Same hash codes?: {0}", p1.GetHashCode() == p2.GetHashCode()); Console.ReadLine();

The output is shown here:
输出如下所示:

Fun with System.Object
p1.ToString() = [First Name: Homer; Last Name: Simpson; Age: 50] p2.ToString() = [First Name: Homer; Last Name: Simpson; Age: 50] p1 = p2?: True
Same hash codes?: True

p1.ToString() = [First Name: Homer; Last Name: Simpson; Age: 50] p2.ToString() = [First Name: Homer; Last Name: Simpson; Age: 45] p1 = p2?: False
Same hash codes?: True

Using the Static Members of System.Object

使用 System.Object 的静态成员

In addition to the instance-level members you have just examined, System.Object does define two static members that also test for value-based or reference-based equality. Consider the following code:
除了刚刚检查的实例级成员之外,System.Object 还定义了两个静态成员,它们还测试基于值或基于引用的相等性。请考虑以下代码:

static void StaticMembersOfObject()
{
// Static members of System.Object.
Person p3 = new Person("Sally", "Jones", 4); Person p4 = new Person("Sally", "Jones", 4);
Console.WriteLine("P3 and P4 have same state: {0}", object.Equals(p3, p4)); Console.WriteLine("P3 and P4 are pointing to same object: {0}",
object.ReferenceEquals(p3, p4));
}

Here, you are able to simply send in two objects (of any type) and allow the System.Object class to determine the details automatically.
除了刚刚检查的实例级成员之外,System.Object 还定义了两个静态成员,它们还测试基于值或基于引用的相等性。请考虑以下代码:

The output (when called from the top-level statements) is shown here:
输出(从顶级语句调用时)如下所示:

Fun with System.Object P3 and P4 have the same state: True
P3 and P4 are pointing to the same object: False

Summary

总结
This chapter explored the role and details of inheritance and polymorphism. Over these pages you were introduced to numerous new keywords and tokens to support each of these techniques. For example, recall that the colon token is used to establish the parent class of a given type. Parent types are able to define any number of virtual and/or abstract members to establish a polymorphic interface. Derived types override such members using the override keyword.
本章探讨了遗传和多态性的作用和细节。在这些页面上,向您介绍了许多新的关键字和令牌,以支持每种技术。例如,回想一下,冒号标记用于建立给定类型的父类。父类型能够定义任意数量的虚拟和/或抽象成员来建立多态接口。派生类型使用 override 关键字重写此类成员。

In addition to building numerous class hierarchies, this chapter also examined how to explicitly cast between base and derived types and wrapped up by diving into the details of the cosmic parent class in the .NET base class libraries: System.Object.
除了构建大量类层次结构外,本章还研究了如何在基类型和派生类型之间显式转换,并通过深入研究.NET 基类库:System.Object。

发表评论