Pro C#10 CHAPTER 5 Understanding Encapsulation

PART III Object Oriented Programming with C

第三部分 使用 C 进行面向对象编程#

CHAPTER 5 Understanding Encapsulation

了解封装

In Chapters 3 and 4, you investigated a number of core syntactical constructs that are commonplace to any .NET Core application you might be developing. Here, you will begin your examination of the object-oriented capabilities of C#. The first order of business is to examine the process of building
well-defined class types that support any number of constructors. After you understand the basics of defining classes and allocating objects, the remainder of this chapter will examine the role of encapsulation. Along the way, you will learn how to define class properties and come to understand the details of the static keyword, object initialization syntax, read-only fields, constant data, and partial classes.
在第 3 章和第 4 章中,您研究了您可能正在开发的任何 .NET Core 应用程序常见的一些核心语法结构。在这里,您将开始研究 C# 的面向对象功能。首要任务是检查构建过程支持任意数量的构造函数的明确定义的类类型。了解定义类和分配对象的基础知识后,本章的其余部分将研究封装的作用。在此过程中,您将学习如何定义类属性,并了解静态关键字、对象初始化语法、只读字段、常量数据和分部类的详细信息。

Introducing the C# Class Type

介绍 C# 类类型
As far as the .NET platform is concerned, one of the most fundamental programming constructs is the class type. Formally, a class is a user-defined type that is composed of field data (often called member variables) and members that operate on this data (such as constructors, properties, methods, events, etc.). Collectively, the set of field data represents the “state” of a class instance (otherwise known as an object). The power of object-oriented languages, such as C#, is that by grouping data and related functionality in a unified class definition, you are able to model your software after entities in the real world.
就 .NET 平台而言,最基本的编程构造之一是类类型。从形式上讲,类是一种用户定义的类型,由字段数据(通常称为成员变量)和对此数据进行操作的成员(如构造函数、属性、方法、事件等)组成。总的来说,字段数据集表示类实例(也称为对象)的“状态”。面向对象语言(如 C#)的强大之处在于,通过在统一的类定义中对数据和相关功能进行分组,您可以根据现实世界中的实体对软件进行建模。

To get the ball rolling, create a new C# Console Application project named SimpleClassExample. Next, insert a new class file (named Car.cs) into your project. In this new file, add the following file-scoped namespace:
若要使球滚动,请创建一个名为 SimpleClassExample 的新 C# 控制台应用程序项目。接下来,将一个新的类文件(名为 Car.cs)插入到项目中。在此新文件中,添加以下文件范围的命名空间:

namespace SimpleClassExample;
A class is defined in C# using the class keyword. Here is the simplest possible declaration (make sure to add the class declaration after the SimpleClassExample namespace):
class Car
{
}

After you have defined a class type, you will need to consider the set of member variables that will be used to represent its state. For example, you might decide that cars maintain an int data type to represent the current speed and a string data type to represent the car’s friendly pet name. Given these initial design notes, update your Car class as follows:
定义类类型后,需要考虑将用于表示其状态的成员变量集。例如,您可能决定汽车维护一个 int 数据类型来表示当前速度,并使用一个字符串数据类型来表示汽车的友好宠物名称。根据这些初始设计说明,按如下方式更新您的 Car 类:

class Car
{
// The ‘state’ of the Car. public string petName; public int currSpeed;
}

Notice that these member variables are declared using the public access modifier. Public members of a class are directly accessible once an object of this type has been created. Recall the term object is used to describe an instance of a given class type created using the new keyword.
请注意,这些成员变量是使用公共访问修饰符声明的。创建此类型的对象后,可以直接访问类的公共成员。回想一下,术语对象用于描述使用 new 关键字创建的给定类类型的实例。

■ Note Field data of a class should seldom (if ever) be defined as public. To preserve the integrity of your state data, it is a far better design to define data as private (or possibly protected) and allow controlled access to the data via properties (as shown later in this chapter). However, to keep this first example as simple as possible, public data fits the bill.
请注意,这些成员变量是使用公共访问修饰符声明的。创建此类型的对象后,可以直接访问类的公共成员。回想一下,术语对象用于描述使用 new 关键字创建的给定类类型的实例。

After you have defined the set of member variables representing the state of the class, the next design step is to establish the members that model its behavior. For this example, the Car class will define one method named SpeedUp() and another named PrintState(). Update your class as so:
定义表示类状态的成员变量集后,下一个设计步骤是建立对其行为进行建模的成员。对于此示例,Car 类将定义一个名为 SpeedUp() 的方法和另一个名为 PrintState() 的方法。按如下方式更新您的课程

class Car
{
// The ‘state’ of the Car. public string petName; public int currSpeed;

// The functionality of the Car.
// Using the expression-bodied member syntax
// covered in Chapter 4 public void PrintState()
=> Console.WriteLine("{0} is going {1} MPH.", petName, currSpeed);
public void SpeedUp(int delta)
=> currSpeed += delta;
}
PrintState() is more or less a diagnostic function that will simply dump the current state of a given
Car object to the command window. SpeedUp() will increase the speed of the Car object by the amount specified by the incoming int parameter. Now, update your top-level statements in the Program.cs file with the following code:
PrintState() 或多或少是一个诊断函数,它将简单地转储给定的当前状态命令窗口的汽车对象。 SpeedUp() 将按传入的 int 参数指定的量提高 Car 对象的速度。现在,使用以下代码更新 Program.cs 文件中的顶级语句:

using SimplClassExample;
Console.WriteLine(" Fun with Class Types \n");
// Allocate and configure a Car object. Car myCar = new Car();
myCar.petName = "Henry"; myCar.currSpeed = 10;
// Speed up the car a few times and print out the
// new state.
for (int i = 0; i <= 10; i++)
{
myCar.SpeedUp(5); myCar.PrintState();
}
Console.ReadLine();

After you run your program, you will see that the Car variable (myCar) maintains its current state throughout the life of the application, as shown in the following output:
运行程序后,您将看到 Car 变量 (myCar) 在应用程序的整个生命周期中保持其当前状态,如以下输出所示:

Fun with Class Types

Henry is going 15 MPH.
Henry is going 20 MPH.
Henry is going 25 MPH.
Henry is going 30 MPH.
Henry is going 35 MPH.
Henry is going 40 MPH.
Henry is going 45 MPH.
Henry is going 50 MPH.
Henry is going 55 MPH.
Henry is going 60 MPH.
Henry is going 65 MPH.

Allocating Objects with the new Keyword

使用新关键字分配对象

As shown in the previous code example, objects must be allocated into memory using the new keyword. If you do not use the new keyword and attempt to use your class variable in a subsequent code statement, you will receive a compiler error. For example, the following top-level statement will not compile:
如前面的代码示例所示,必须使用 new 关键字将对象分配到内存中。如果不使用 new 关键字并尝试在后续代码语句中使用类变量,则会收到编译器错误。例如,以下顶级语句将无法编译:

Console.WriteLine(" Fun with Class Types \n");
// Compiler error! Forgot to use ‘new’ to create object! Car myCar;
myCar.petName = "Fred";

To correctly create an object using the new keyword, you may define and allocate a Car object on a single line of code.
要使用 new 关键字正确创建对象,可以在一行代码上定义和分配 Car 对象。

Console.WriteLine(" Fun with Class Types \n"); Car myCar = new Car();
myCar.petName = "Fred";

As an alternative, if you want to define and allocate a class instance on separate lines of code, you may do so as follows:
要使用 new 关键字正确创建对象,可以在一行代码上定义和分配 Car 对象。

Console.WriteLine(" Fun with Class Types \n"); Car myCar;
myCar = new Car(); myCar.petName = "Fred";

Here, the first code statement simply declares a reference to a yet-to-be-determined Car object. It is not until you assign a reference to an object that this reference points to a valid object in memory.
在这里,第一个代码语句只是声明对尚未确定的 Car 对象的引用。直到您指定对对象的引用,此引用才会指向内存中的有效对象。
In any case, at this point you have a trivial class that defines a few points of data and some basic operations. To enhance the functionality of the current Car class, you need to understand the role of constructors.
无论如何,此时您有一个简单的类,它定义了几个数据点和一些基本操作。若要增强当前 Car 类的功能,需要了解构造函数的角色。

Understanding Constructors

了解构造函数

Given that objects have state (represented by the values of an object’s member variables), a programmer will typically want to assign relevant values to the object’s field data before use. Currently, the Car class demands that the petName and currSpeed fields be assigned on a field-by-field basis. For the current example, this is not too problematic, given that you have only two public data points. However, it is not uncommon for a class to have dozens of fields to contend with. Clearly, it would be undesirable to author 20 initialization statements to set 20 points of data!
给定对象具有状态(由对象的成员变量的值表示),程序员通常希望在使用之前为对象的字段数据分配相关值。目前,Car 类要求逐字段分配 petName 和 currSpeed 字段。对于当前示例,此问题不大,因为您只有两个公共数据点。但是,一个类要应对数十个字段的情况并不少见。显然,编写 20 个初始化语句来设置 20 个数据点是不可取的!

Thankfully, C# supports the use of constructors, which allow the state of an object to be established at the time of creation. A constructor is a special method of a class that is called indirectly when creating an object using the new keyword. However, unlike a “normal” method, constructors never have a return value (not even void) and are always named identically to the class they are constructing.
值得庆幸的是,C# 支持使用构造函数,这些构造函数允许在创建时建立对象的状态。构造函数是类的特殊方法,在使用 new 关键字创建对象时间接调用该方法。但是,与“普通”方法不同,构造函数永远不会有返回值(甚至没有 void),并且始终与它们正在构造的类相同。

Understanding the Role of the Default Constructor

了解默认构造函数的角色

Every C# class is provided with a “freebie” default constructor that you can redefine if need be. By definition, a default constructor never takes arguments. After allocating the new object into memory, the default constructor ensures that all field data of the class is set to an appropriate default value (see Chapter 3 for information regarding the default values of C# data types).
每个 C# 类都提供了一个“免费赠品”默认构造函数,如果需要,可以重新定义该构造函数。根据定义,默认构造函数从不接受参数。将新对象分配到内存后,默认构造函数确保类的所有字段数据都设置为适当的默认值(有关 C# 数据类型的默认值的信息,请参阅第 3 章)。

If you are not satisfied with these default assignments, you may redefine the default constructor to suit your needs. To illustrate, update your C# Car class as follows:
如果对这些默认赋值不满意,可以重新定义默认构造函数以满足您的需要。为了进行说明,请更新 C# Car 类,如下所示:

class Car
{
// The ‘state’ of the Car. public string petName; public int currSpeed;

// A custom default constructor. public Car()
{

}

}

petName = "Chuck"; currSpeed = 10;

In this case, you are forcing all Car objects to begin life named Chuck at a rate of 10 MPH. With this, you are able to create a Car object set to these default values as follows:
在本例中,您将强制所有 Car 对象以 10 英里/小时的速度开始名为 Chuck 的生命。这样,您就可以创建一个设置为这些默认值的 Car 对象,如下所示:

Console.WriteLine(" Fun with Class Types \n");

// Invoking the default constructor. Car chuck = new Car();

// Prints "Chuck is going 10 MPH." chuck.PrintState();

Defining Custom Constructors

定义自定义构造函数

Typically, classes define additional constructors beyond the default. In doing so, you provide the object user with a simple and consistent way to initialize the state of an object directly at the time of creation. Ponder the following update to the Car class, which now supports a total of three constructors:
通常,类定义默认值之外的其他构造函数。这样,您就可以为对象用户提供一种简单且一致的方式来在创建时直接初始化对象的状态。考虑对 Car 类的以下更新,该类现在总共支持三个构造函数:

class Car
{
// The ‘state’ of the Car. public string petName; public int currSpeed;

// A custom default constructor. public Car()
{
petName = "Chuck"; currSpeed = 10;
}

// Here, currSpeed will receive the
// default value of an int (zero). public Car(string pn)
{
petName = pn;
}

// Let caller set the full state of the Car. public Car(string pn, int cs)
{

}

}

petName = pn; currSpeed = cs;

Keep in mind that what makes one constructor different from another (in the eyes of the C# compiler) is the number of and/or type of constructor arguments. Recall from Chapter 4, when you define a method of the same name that differs by the number or type of arguments, you have overloaded the method. Thus, the Car class has overloaded the constructor to provide a number of ways to create an object at the time of declaration. In any case, you are now able to create Car objects using any of the public constructors. Here is an example:
请记住,使一个构造函数与另一个构造函数不同的(在 C# 编译器眼中)是构造函数参数的数量和/或类型。回想一下第 4 章,当您定义一个因参数的数量或类型而异的同名方法时,您已经重载了该方法。因此,Car 类重载了构造函数,以提供多种在声明时创建对象的方法。在任何情况下,您现在都可以使用任何公共构造函数创建 Car 对象。下面是一个示例:

Console.WriteLine(" Fun with Class Types \n");

// Make a Car called Chuck going 10 MPH. Car chuck = new Car(); chuck.PrintState();

// Make a Car called Mary going 0 MPH. Car mary = new Car("Mary"); mary.PrintState();

// Make a Car called Daisy going 75 MPH. Car daisy = new Car("Daisy", 75); daisy.PrintState();

Constructors As Expression-Bodied Members (New 7.0)

构造函数作为表达式体成员 (New 7.0)

C# 7 added additional uses for the expression-bodied member style. Constructors, finalizers, and get/set accessors on properties and indexers now accept the new syntax. With this in mind, the previous constructors can be written like this:
C# 7 为表达式主体成员样式添加了其他用法。属性和索引器上的构造函数、终结器和 get/set 访问器现在接受新语法。考虑到这一点,前面的构造函数可以这样编写:

// Here, currSpeed will receive the
// default value of an int (zero). public Car(string pn) => petName = pn;

The second custom constructor cannot be converted to an expression since expression-bodied members must be one-line methods.
第二个自定义构造函数不能转换为表达式,因为表达式体成员必须是单行方法。

Constructors with out Parameters (New 7.3)

不带 out 参数的构造函数(新 7.3)

Constructors (as well as field and property initializers, covered later) can use out parameters starting with C# 7.3. For a trivial example of this, add the following constructor to the Car class:
构造函数(以及字段和属性初始值设定项,稍后将介绍)可以使用从 C# 7.3 开始的参数。对于这方面的简单示例,请将以下构造函数添加到 Car 类:

public Car(string pn, int cs, out bool inDanger)
{
petName = pn; currSpeed = cs; if (cs > 100)
{
inDanger = true;
}
else
{
inDanger = false;
}
}

All of the rules of out parameters must be followed. In this example, the inDanger parameter must be assigned a value before the conclusion of the constructor.
必须遵守 out 参数的所有规则。在此示例中,必须在构造函数结束之前为 inDanger 参数赋值。

Understanding the Default Constructor Revisited

重新访问默认构造函数
As you have just learned, all classes are provided with a free default constructor. Insert a new file into your project named Motorcycle.cs, and add the following to define a Motorcycle class:
正如您刚刚了解到的,所有类都提供了一个免费的默认构造函数。将一个名为 Motorcycle.cs 的新文件插入到项目中,并添加以下内容以定义 Motorcycle 类:

namespace SimpleClassExample; class Motorcycle
{
public void PopAWheely()

{
Console.WriteLine("Yeeeeeee Haaaaaeewww!");
}
}

Now you are able to create an instance of the Motorcycle type via the default constructor out of the box.
现在,您可以通过现成的默认构造函数创建 Motorcycle 类型的实例。

Console.WriteLine(" Fun with Class Types \n"); Motorcycle mc = new Motorcycle();
mc.PopAWheely();

However, as soon as you define a custom constructor with any number of parameters, the default constructor is silently removed from the class and is no longer available. Think of it this way: if you do not define a custom constructor, the C# compiler grants you a default to allow the object user to allocate an instance of your type with the field data set to the correct default values. However, when you define a unique constructor, the compiler assumes you have taken matters into your own hands.
但是,一旦定义了具有任意数量参数的自定义构造函数,默认构造函数就会从类中以静默方式删除,并且不再可用。可以这样想:如果未定义自定义构造函数,C# 编译器将授予默认值,以允许对象用户将字段数据集分配给正确的默认值的类型的实例。但是,当您定义唯一的构造函数时,编译器会假定您已将事情掌握在自己手中。

Therefore, if you want to allow the object user to create an instance of your type with the default constructor, as well as your custom constructor, you must explicitly redefine the default. To this end, understand that in a vast majority of cases, the implementation of the default constructor of a class is intentionally empty, as all you require is the ability to create an object with default values. Consider the following update to the Motorcycle class:
因此,如果要允许对象用户使用默认构造函数以及自定义构造函数创建类型的实例,则必须显式重新定义默认值。为此,请了解,在绝大多数情况下,类的默认构造函数的实现是故意为空的,因为您所需要的只是能够使用默认值创建对象。请考虑对摩托车类进行以下更新:

class Motorcycle
{
public int driverIntensity;

public void PopAWheely()
{
for (int i = 0; i <= driverIntensity; i++)
{
Console.WriteLine("Yeeeeeee Haaaaaeewww!");
}
}
// Put back the default constructor, which will
// set all data members to default values. public Motorcycle() {}

// Our custom constructor. public Motorcycle(int intensity)
{
driverIntensity = intensity;
}
}

■ Note now that you better understand the role of class constructors, here is a nice shortcut. Both Visual studio and Visual studio Code provide the ctor code snippet. When you type ctor and press the Tab key, the ide will automatically define a custom default constructor. You can then add custom parameters and implementation logic. give it a try.
因此,如果要允许对象用户使用默认构造函数以及自定义构造函数创建类型的实例,则必须显式重新定义默认值。为此,请了解,在绝大多数情况下,类的默认构造函数的实现是故意为空的,因为您所需要的只是能够使用默认值创建对象。请考虑对摩托车类进行以下更新:

Understanding the Role of the this Keyword

了解此关键字的作用

C# supplies a this keyword that provides access to the current class instance. One possible use of the this keyword is to resolve scope ambiguity, which can arise when an incoming parameter is named identically to a data field of the class. However, you could simply adopt a naming convention that does not result in such ambiguity; to illustrate this use of the this keyword, update your Motorcycle class with a new string field (named name) to represent the driver’s name. Next, add a method named SetDriverName() implemented as follows:
C# 提供了一个 this 关键字,该关键字提供对当前类实例的访问。this 关键字的一种可能用途是解决范围歧义,当传入参数与类的数据字段命名相同时,可能会出现歧义。但是,您可以简单地采用不会导致这种歧义的命名约定;若要说明 this 关键字的这种用法,请使用新的字符串字段(名为名称)更新 Motorcycle 类以表示驱动程序的名称。接下来,添加一个名为 SetDriverName() 的方法,实现如下:

class Motorcycle
{
public int driverIntensity;

// New members to represent the name of the driver. public string name;
public void SetDriverName(string name) => name = name;

}

Although this code will compile, the C# compiler will display a warning message informing you that you have assigned a variable back to itself! To illustrate, update your code to call SetDriverName() and then print out the value of the name field. You might be surprised to find that the value of the name field is an empty string!
尽管此代码将编译,但 C# 编译器将显示一条警告消息,通知您已将变量赋回自身!为了说明这一点,请更新代码以调用 SetDriverName(),然后打印出名称字段的值。您可能会惊讶地发现名称字段的值是一个空字符串!

// Make a Motorcycle with a rider named Tiny? Motorcycle c = new Motorcycle(5); c.SetDriverName("Tiny");
c.PopAWheely();
Console.WriteLine("Rider name is {0}", c.name); // Prints an empty name value!

The problem is that the implementation of SetDriverName() is assigning the incoming parameter back to itself given that the compiler assumes name is referring to the variable currently in the method scope rather than the name field at the class scope. To inform the compiler that you want to set the current object’s name data field to the incoming name parameter, simply use this to resolve the ambiguity.
问题在于,SetDriverName() 的实现将传入参数赋回自身,因为编译器假定 name 引用当前在方法作用域中的变量,而不是类作用域中的 name 字段。要通知编译器要将当前对象的名称数据字段设置为传入的 name 参数,只需使用它来解决歧义。

public void SetDriverName(string name) => this.name = name;

If there is no ambiguity, you are not required to make use of the this keyword when accessing data fields or members. For example, if you rename the string data member from name to driverName (which will also require you to update your top-level statements), the use of this is optional as there is no longer a scope ambiguity.
如果没有歧义,则在访问数据字段或成员时不需要使用 this 关键字。例如,如果将字符串数据成员从 name 重命名为 driverName(这也需要更新顶级语句),则 this 的使用是可选的,因为不再存在范围歧义。

class Motorcycle
{
public int driverIntensity; public string driverName;

public void SetDriverName(string name)
{
// These two statements are functionally the same. driverName = name;

this.driverName = name;
}

}

Even though there is little to be gained when using this in unambiguous situations, you might still find this keyword useful when implementing class members, as IDEs such as Visual Studio and Visual Studio Code will enable IntelliSense when this is specified. This can be helpful when you have forgotten the name of a class member and want to quickly recall the definition.
尽管在明确的情况下使用它时几乎没有什么好处,但在实现类成员时,您仍然可能会发现此关键字很有用,因为 IDE (如 Visual Studio 和 Visual Studio Code)将在指定此关键字时启用 IntelliSense。当您忘记了类成员的名称并希望快速调用定义时,这会很有帮助。

■ Note a common naming convention is to start private (or internal) class-level variable names with an underscore (e.g., _driverName) so intellisense shows all of your variables at the top of the list. in our trivial example, all of the fields are public, so this naming convention would not apply. Through the rest of the book, you will see private and internal variables named with a leading underscore.
请注意,常见的命名约定是以下划线(例如_driverName)开始私有(或内部)类级变量名称,以便智能感知在列表顶部显示所有变量。在我们的简单示例中,所有字段都是公共的,因此此命名约定不适用。在本书的其余部分,您将看到以前导下划线命名的私有变量和内部变量。

Chaining Constructor Calls Using this

以此链接构造函数调用

Another use of the this keyword is to design a class using a technique termed constructor chaining. This design pattern is helpful when you have a class that defines multiple constructors. Given that constructors often validate the incoming arguments to enforce various business rules, it can be quite common to find redundant validation logic within a class’s constructor set. Consider the following updated Motorcycle:
this 关键字的另一个用途是使用称为构造函数链接的技术设计类。当您有一个定义多个构造函数的类时,此设计模式非常有用。鉴于构造函数经常验证传入的参数以强制实施各种业务规则,因此在类的构造函数集中找到冗余验证逻辑是很常见的。考虑以下更新的摩托车:

class Motorcycle
{
public int driverIntensity; public string driverName;

public Motorcycle() { }

// Redundant constructor logic! public Motorcycle(int intensity)
{
if (intensity > 10)
{
intensity = 10;
}
driverIntensity = intensity;
}

public Motorcycle(int intensity, string name)
{
if (intensity > 10)
{
intensity = 10;
}

}

}

driverIntensity = intensity; driverName = name;

Here (perhaps in an attempt to ensure the safety of the rider) each constructor is ensuring that the intensity level is never greater than 10. While this is all well and good, you do have redundant code statements in two constructors. This is less than ideal, as you are now required to update code in multiple locations if your rules change (e.g., if the intensity should not be greater than 5 rather than 10).
在这里(也许是为了确保骑手的安全),每个构造函数都确保强度级别永远不会大于 10。虽然这一切都很好,但您确实有冗余代码两个构造函数中的语句。这不太理想,因为如果您的规则发生变化(例如,如果强度不应大于 5 而不是 10),您现在需要在多个位置更新代码。

One way to improve the current situation is to define a method in the Motorcycle class that will validate the incoming argument(s). If you were to do so, each constructor could make a call to this method before making the field assignment(s). While this approach does allow you to isolate the code you need to update when the business rules change, you are now dealing with the following redundancy:
改善当前情况的一种方法是在 Motorcycle 类中定义一个方法,该方法将验证传入的参数。如果要这样做,则每个构造函数都可以在进行字段分配之前调用此方法。虽然此方法确实允许您隔离在业务规则更改时需要更新的代码,但您现在正在处理以下冗余:

class Motorcycle
{
public int driverIntensity; public string driverName;

// Constructors.
public Motorcycle() { }

public Motorcycle(int intensity)
{
SetIntensity(intensity);
}

public Motorcycle(int intensity, string name)
{
SetIntensity(intensity); driverName = name;
}

public void SetIntensity(int intensity)
{
if (intensity > 10)
{
intensity = 10;
}
driverIntensity = intensity;
}

}

A cleaner approach is to designate the constructor that takes the greatest number of arguments as the “master constructor” and have its implementation perform the required validation logic. The remaining constructors can make use of the this keyword to forward the incoming arguments to the master constructor and provide any additional parameters as necessary. In this way, you need to worry only about maintaining a single constructor for the entire class, while the remaining constructors are basically empty.
更简洁的方法是将接受最多参数的构造函数指定为“主构造函数”,并让其实现执行所需的验证逻辑。其余构造函数可以使用 this 关键字将传入参数转发给主构造函数,并根据需要提供任何其他参数。这样,您只需要担心为整个类维护单个构造函数,而其余的构造函数基本上是空的。

Here is the final iteration of the Motorcycle class (with one additional constructor for the sake of illustration). When chaining constructors, note how the this keyword is “dangling” off the constructor’s declaration (via a colon operator) outside the scope of the constructor itself.
下面是 Motorcycle 类的最终迭代(为了便于说明,还有一个额外的构造函数)。链接构造函数时,请注意 this 关键字如何在构造函数本身的范围之外“悬空”构造函数的声明(通过冒号运算符)。

class Motorcycle
{
public int driverIntensity; public string driverName;

// Constructor chaining. public Motorcycle() {}
public Motorcycle(int intensity)
: this(intensity, "") {} public Motorcycle(string name)
: this(0, name) {}

// This is the ‘master’ constructor that does all the real work. public Motorcycle(int intensity, string name)
{
if (intensity > 10)
{
intensity = 10;
}

}

}

driverIntensity = intensity; driverName = name;

Understand that using the this keyword to chain constructor calls is never mandatory. However, when you make use of this technique, you do tend to end up with a more maintainable and concise class definition. Again, using this technique, you can simplify your programming tasks, as the real work is delegated to a single constructor (typically the constructor that has the most parameters), while the other constructors simply “pass the buck.”
了解使用 this 关键字链接构造函数调用从来都不是强制性的。但是,当您使用此技术时,您最终往往会得到一个更易于维护、更简洁的类定义。同样,使用此技术,您可以简化编程任务,因为实际工作委托给单个构造函数(通常是具有最多参数的构造函数),而其他构造函数只是“推卸责任”。

■ Note recall from Chapter 4 that C# supports optional parameters. if you use optional parameters in your class constructors, you can achieve the same benefits as constructor chaining with less code. You will see how to do so in just a moment.
请注意第 4 章中的 C# 支持可选参数。 如果在类构造函数中使用可选参数,则可以获得与使用更少代码的构造函数链接相同的好处。稍后您将看到如何做到这一点。

Observing Constructor Flow

观察构造函数流

On a final note, do know that once a constructor passes arguments to the designated master constructor (and that constructor has processed the data), the constructor invoked originally by the caller will finish executing any remaining code statements. To clarify, update each of the constructors of the Motorcycle class with a fitting call to Console.WriteLine().
最后一点,要知道,一旦构造函数将参数传递给指定的主构造函数(并且该构造函数已经处理了数据),调用方最初调用的构造函数将完成执行任何剩余的代码语句。为了澄清这一点,请使用对 Console.WriteLine() 的合适调用来更新 Motorcycle 类的每个构造函数。

class Motorcycle
{
public int driverIntensity; public string driverName;

// Constructor chaining. public Motorcycle()
{
Console.WriteLine("In default constructor");
}

public Motorcycle(int intensity)
: this(intensity, "")
{
Console.WriteLine("In constructor taking an int");
}

public Motorcycle(string name)
: this(0, name)
{
Console.WriteLine("In constructor taking a string");
}

// This is the ‘main’ constructor that does all the real work. public Motorcycle(int intensity, string name)
{
Console.WriteLine("In main constructor"); if (intensity > 10)
{
intensity = 10;
}

}

}

driverIntensity = intensity; driverName = name;

Now, ensure your top-level statements exercise a Motorcycle object as follows:
现在,确保您的顶级语句按如下方式执行 Motorcycle 对象:

Console.WriteLine(" Fun with class Types \n");

// Make a Motorcycle.
Motorcycle c = new Motorcycle(5); c.SetDriverName("Tiny"); c.PopAWheely();
Console.WriteLine("Rider name is {0}", c.driverName); Console.ReadLine();

With this, ponder the output from the previous code:
有了这个,思考前面代码的输出:

Fun with Motorcycles In main constructor
In constructor taking an int Yeeeeeee Haaaaaeewww!
Yeeeeeee Haaaaaeewww! Yeeeeeee Haaaaaeewww! Yeeeeeee Haaaaaeewww! Yeeeeeee Haaaaaeewww! Yeeeeeee Haaaaaeewww! Rider name is Tiny

As you can see, the flow of constructor logic is as follows:
如您所见,构造函数逻辑的流程如下:
• You create your object by invoking the constructor requiring a single int.
您可以通过调用需要单个 int 的构造函数来创建对象。
• This constructor forwards the supplied data to the master constructor and provides any additional startup arguments not specified by the caller.
此构造函数将提供的数据转发给主构造函数,并提供调用方未指定的任何其他启动参数。
• The master constructor assigns the incoming data to the object’s field data.
主构造函数将传入数据分配给对象的字段数据。
• Control is returned to the constructor originally called and executes any remaining code statements.
控制权返回到最初调用的构造函数,并执行任何剩余的代码语句。

The nice thing about using constructor chaining is that this programming pattern will work with any version of the C# language and .NET platform. However, if you are targeting .NET 4.0 and higher, you can further simplify your programming tasks by making use of optional arguments as an alternative to traditional constructor chaining.
使用构造函数链接的好处是,此编程模式适用于任何版本的 C# 语言和 .NET 平台。但是,如果面向 .NET 4.0 及更高版本,则可以通过使用可选参数作为传统构造函数链接的替代方法来进一步简化编程任务。

Revisiting Optional Arguments

重新访问可选参数

In Chapter 4, you learned about optional and named arguments. Recall that optional arguments allow you to define supplied default values to incoming arguments. If the caller is happy with these defaults, they are not required to specify a unique value; however, they may do so to provide the object with custom data.
Consider the following version of Motorcycle, which now provides a number of ways to construct objects using a single constructor definition:
在第 4 章中,您了解了可选参数和命名参数。回想一下,可选参数允许您定义为传入参数提供的默认值。如果调用方对这些默认值感到满意,则不需要指定唯一值;但是,他们这样做是为了向对象提供自定义数据。请考虑以下版本的 Motorcycle,它现在提供了多种使用单个构造函数定义构造对象的方法:

class Motorcycle
{
// Single constructor using optional args.
public Motorcycle(int intensity = 0, string name = "")
{
if (intensity > 10)
{
intensity = 10;
}

}

}

driverIntensity = intensity; driverName = name;

With this one constructor, you are now able to create a new Motorcycle object using zero, one, or two arguments. Recall that named argument syntax allows you to essentially skip over acceptable default settings (see Chapter 3).
有了这个构造函数,您现在可以使用零个、一个或两个参数创建新的 Motorcycle 对象。回想一下,命名参数语法允许您基本上跳过可接受的默认设置(请参阅第 3 章)。

static void MakeSomeBikes()
{
// driverName = "", driverIntensity = 0 Motorcycle m1 = new Motorcycle(); Console.WriteLine("Name= {0}, Intensity= {1}",
m1.driverName, m1.driverIntensity);

// driverName = "Tiny", driverIntensity = 0 Motorcycle m2 = new Motorcycle(name:"Tiny"); Console.WriteLine("Name= {0}, Intensity= {1}",
m2.driverName, m2.driverIntensity);

// driverName = "", driverIntensity = 7 Motorcycle m3 = new Motorcycle(7); Console.WriteLine("Name= {0}, Intensity= {1}",
m3.driverName, m3.driverIntensity);
}

In any case, at this point you are able to define a class with field data (aka member variables) and various operations such as methods and constructors. Next up, let’s formalize the role of the static keyword.
无论如何,此时您都可以定义一个包含字段数据(也称为成员变量)和各种操作(如方法和构造函数)的类。接下来,让我们正式确定静态关键字的角色。

Understanding the static Keyword

了解静态关键字
A C# class may define any number of static members, which are declared using the static keyword. When you do so, the member in question must be invoked directly from the class level, rather than from an object reference variable. To illustrate the distinction, consider your good friend System.Console. As you have seen, you do not invoke the WriteLine() method from the object level, as shown here:
C# 类可以定义任意数量的静态成员,这些成员使用 static 关键字声明。执行此操作时,必须直接从类级别调用相关成员,而不是从对象引用变量调用。为了说明这种区别,请考虑您的好朋友System.Console。如您所见,您不会从对象级别调用 WriteLine() 方法,如下所示:

// Compiler error! WriteLine() is not an object level method! Console c = new Console();
c.WriteLine("I can’t be printed…");

Instead, simply prefix the class name to the static WriteLine() member.
相反,只需将类名前缀为静态 WriteLine() 成员。

// Correct! WriteLine() is a static method. Console.WriteLine("Much better! Thanks…");

Simply put, static members are items that are deemed (by the class designer) to be so commonplace that there is no need to create an instance of the class before invoking the member. While any class can define static members, they are quite commonly found within utility classes. By definition, a utility class is a class that does not maintain any object-level state and is not created with the new keyword. Rather, a utility class exposes all functionality as class-level (aka static) members.
简单地说,静态成员是(由类设计者)认为非常常见的项,以至于在调用成员之前不需要创建类的实例。虽然任何类都可以定义静态成员,但它们在实用程序类中很常见。根据定义,实用程序类是不维护任何对象级状态且不使用 new 关键字创建的类。相反,实用程序类将所有功能公开为类级(也称为静态)成员。

For example, if you were to use the Visual Studio Object Browser (via the View ➤ Object Browser menu item) to view the System namespace, you would see that all the members of the Console, Math, Environment, and GC classes (among others) expose all their functionality via static members. These are but a few utility classes found within the .NET Core base class libraries.
例如,如果要使用 Visual Studio 对象浏览器(通过“视图”➤“对象浏览器”菜单项)查看“系统”命名空间,则会看到控制台、数学类、环境和 GC 类(以及其他类)的所有成员都通过静态成员公开其所有功能。这些只是在 .NET Core 基类库中找到的几个实用工具类。

Again, be aware that static members are not only found in utility classes; they can be part of any class definition at all. Just remember that static members promote a given item to the class level rather than the object level. As you will see over the next few sections, the static keyword can be applied to the following:
同样,请注意,静态成员不仅存在于实用程序类中;它们完全可以是任何类定义的一部分。请记住,静态成员将给定项目提升到类级别而不是对象级别。正如您将在接下来的几节中看到的那样,static 关键字可以应用于以下内容:

•Data of a class
类的数据
•Methods of a class
类的方法
•Properties of a class
类的属性
•A constructor
构造函数
•The entire class definition
整个类定义
•In conjunction with the C# using keyword
结合 C# using 关键字

Let’s see each of our options, beginning with the concept of static data.
让我们看看我们的每个选项,从静态数据的概念开始。

■Note You will examine the role of static properties later in this chapter while examining the properties themselves.
注意 在本章后面,您将在检查属性本身时检查静态属性的作用。

Defining Static Field Data

定义静态字段数据

Most of the time when designing a class, you define data as instance-level data or, said another way, as nonstatic data. When you define instance-level data, you know that every time you create a new object, the object maintains its own independent copy of the data. In contrast, when you define static data of a class, the memory is shared by all objects of that category.
大多数时候,在设计类时,您将数据定义为实例级数据,或者换句话说,定义为非静态数据。定义实例级数据时,您知道每次创建新对象时,该对象都会维护其自己的独立数据副本。相反,当您定义类的静态数据时,内存由该类别的所有对象共享。

To see the distinction, create a new Console Application project named StaticDataAndMembers. Now, insert a file into your project named SavingsAccount.cs, and in that file create a new class named SavingsAccount. Begin by defining an instance-level variable (to model the current balance) and a custom constructor to set the initial balance.
若要查看区别,请创建一个名为 StaticDataAndMembers的新控制台应用程序项目。现在,将一个名为 Savings Account.cs 的文件插入到项目中,并在该文件中创建一个名为 Savings Account 的新类。首先定义一个实例级变量(用于对当前余额进行建模)和一个自定义构造函数来设置初始余额。

namespace StaticDataAndMembers;
// A simple savings account class. class SavingsAccount
{
// Instance-level data. public double currBalance;
public SavingsAccount(double balance)
{
currBalance = balance;
}
}

When you create SavingsAccount objects, memory for the currBalance field is allocated for each object. Thus, you could create five different SavingsAccount objects, each with their own unique balance. Furthermore, if you change the balance on one account, the other objects are not affected.
创建储蓄帐户对象时,将为每个对象分配 currBalance 字段的内存。因此,您可以创建五个不同的储蓄账户对象,每个对象都有自己独特的余额。此外,如果您更改一个帐户的余额,则其他对象不受影响。

Static data, on the other hand, is allocated once and shared among all objects of the same class category. Add a static variable named currInterestRate to the SavingsAccount class, which is set to a default value of 0.04.
另一方面,静态数据分配一次,并在同一类类别的所有对象之间共享。将一个名为 currInterestRate 的静态变量添加到 Savings Account 类,该变量设置为默认值 0.04。

// A simple savings account class. class SavingsAccount
{
// A static point of data.
public static double currInterestRate = 0.04;

// Instance-level data. public double currBalance;

public SavingsAccount(double balance)
{
currBalance = balance;
}
}

Create three instances of SavingsAccount in top-level statements, as follows:
在顶级语句中创建三个储蓄账户实例,如下所示:

using StaticDataAndMembers;

Console.WriteLine(" Fun with Static Data \n"); SavingsAccount s1 = new SavingsAccount(50); SavingsAccount s2 = new SavingsAccount(100); SavingsAccount s3 = new SavingsAccount(10000.75); Console.ReadLine();
The in-memory data allocation would look something like Figure 5-1.

Alt text

Figure 5-1. Static data is allocated once and shared among all instances of the class
图 5-1。 静态数据分配一次,并在类的所有实例之间共享

Here, the assumption is that all saving accounts should have the same interest rate. Because static data is shared by all objects of the same category, if you were to change it in any way, all objects will “see” the new value the next time they access the static data, as they are all essentially looking at the same memory location. To understand how to change (or obtain) static data, you need to consider the role of static methods.
在这里,假设所有储蓄账户都应该有相同的利率。由于静态数据由同一类别的所有对象共享,因此,如果要以任何方式更改它,所有对象都将“看到”新的值,因为它们基本上都在查看相同的内存位置。要了解如何更改(或获取)静态数据,需要考虑静态方法的作用。

Defining Static Methods

定义静态方法

Let’s update the SavingsAccount class to define two static methods. The first static method (GetInterestRate()) will return the current interest rate, while the second static method (SetInterestRate()) will allow you to change the interest rate.
让我们更新 Savings Account 类以定义两个静态方法。第一个静态方法(GetInterestRate())将返回当前利率,而第二个静态方法(SetInterestRate())将允许您更改利率。

// A simple savings account class. class SavingsAccount
{
// Instance-level data. public double currBalance;

// A static point of data.
public static double currInterestRate = 0.04;

public SavingsAccount(double balance)
{
currBalance = balance;
}

// Static members to get/set interest rate.
public static void SetInterestRate(double newRate)
=> currInterestRate = newRate;

public static double GetInterestRate()
=> currInterestRate;
}

Now, observe the following usage:
现在,观察以下用法:

using StaticDataAndMembers;

Console.WriteLine(" Fun with Static Data \n"); SavingsAccount s1 = new SavingsAccount(50); SavingsAccount s2 = new SavingsAccount(100);

// Print the current interest rate.
Console.WriteLine("Interest Rate is: {0}", SavingsAccount.GetInterestRate());

// Make new object, this does NOT ‘reset’ the interest rate. SavingsAccount s3 = new SavingsAccount(10000.75);
Console.WriteLine("Interest Rate is: {0}", SavingsAccount.GetInterestRate()); Console.ReadLine();

The output of the previous code is shown here:
前面代码的输出如下所示:

Fun with Static Data Interest Rate is: 0.04
Interest Rate is: 0.04

As you can see, when you create new instances of the SavingsAccount class, the value of the static data is not reset, as the CoreCLR will allocate the static data into memory exactly one time. After that point, all objects of type SavingsAccount operate on the same value for the static currInterestRate field.
如您所见,当您创建 Savings Account 类的新实例时,静态数据的值不会重置,因为 CoreCLR 会将静态数据精确分配到内存中一次。在此之后,类型为“储蓄账户”的所有对象对静态 currInterest 字段的相同值进行操作。

When designing any C# class, one of your design challenges is to determine which pieces of data should be defined as static members and which should not. While there are no hard-and-fast rules, remember that a static data field is shared by all objects of that type. Therefore, if you are defining a point of data that all objects should share between them, static is the way to go.
在设计任何 C# 类时,设计难题之一是确定哪些数据段应定义为静态成员,哪些不应定义。虽然没有硬性规定,但请记住,静态数据字段由该类型的所有对象共享。因此,如果要定义所有对象之间应共享的数据点,则静态是要走的路。

Consider what would happen if the interest rate variable were not defined using the static keyword. This would mean every SavingsAccount object would have its own copy of the currInterestRate field. Now, assume you created 100 SavingsAccount objects and needed to change the interest rate. That would require you to call the SetInterestRate() method 100 times! Clearly, this would not be a useful way to model “shared data.” Again, static data is perfect when you have a value that should be common to all objects of that category.
考虑一下如果不使用 static 关键字定义利率变量会发生什么情况。这意味着每个储蓄账户对象都有自己的currInterestRate字段副本。现在,假设您创建了 100 个储蓄账户对象,并且需要更改利率。这将需要您调用 SetInterestRate() 方法100 次!显然,这不是对“共享数据”进行建模的有用方法。同样,当您具有该类别的所有对象都应该通用的值时,静态数据是完美的。

■ Note it is a compiler error for a static member to reference nonstatic members in its implementation. on a related note, it is an error to use the this keyword on a static member because this implies an object!
请注意,静态成员在其实现中引用非静态成员是一个编译器错误。在相关的说明中,在静态成员上使用 this 关键字是错误的,因为这意味着一个对象!

Defining Static Constructors

定义静态构造函数

A typical constructor is used to set the value of an object’s instance-level data at the time of creation. However, what would happen if you attempted to assign the value of a static point of data in a typical constructor? You might be surprised to find that the value is reset each time you create a new object.
典型的构造函数用于在创建时设置对象的实例级数据的值。但是,如果您尝试在典型构造函数中分配静态数据点的值,会发生什么情况?您可能会惊讶地发现,每次创建新对象时,该值都会重置。

To illustrate, assume you have updated the SavingsAccount class constructor as follows (also note you are no longer assigning the currInterestRate field inline):
为了说明这一点,假设您已按如下方式更新了 Savings Account 类构造函数(另请注意,您不再以内联方式分配 currInterestRate 字段):

class SavingsAccount
{
public double currBalance;
public static double currInterestRate;

// Notice that our constructor is setting
// the static currInterestRate value. public SavingsAccount(double balance)
{

}

}

currInterestRate = 0.04; // This is static data! currBalance = balance;

Now, assume you have authored the following code in the top-level statements:
现在,假设您已在顶级语句中创作了以下代码:

// Make an account.
SavingsAccount s1 = new SavingsAccount(50);

// Print the current interest rate.
Console.WriteLine("Interest Rate is: {0}", SavingsAccount.GetInterestRate());

// Try to change the interest rate via property. SavingsAccount.SetInterestRate(0.08);

// Make a second account.
SavingsAccount s2 = new SavingsAccount(100);

// Should print 0.08…right??
Console.WriteLine("Interest Rate is: {0}", SavingsAccount.GetInterestRate()); Console.ReadLine();

If you executed the previous code, you would see that the currInterestRate variable is reset each time you create a new SavingsAccount object, and it is always set to 0.04. Clearly, setting the value of static data in a normal instance-level constructor sort of defeats the whole purpose. Every time you make a new object, the class-level data is reset. One approach to setting a static field is to use member initialization syntax, as you did originally.
如果执行前面的代码,则每次创建新的 Savings Account 对象时,都会重置 currInterestRate 变量,并且该变量始终设置为 0.04。显然,在普通的实例级构造函数中设置静态数据的值有点违背了整个目的。每次创建新对象时,都会重置类级数据。设置静态字段的一种方法是使用成员初始化语法,就像您最初所做的那样。

class SavingsAccount
{
public double currBalance;

// A static point of data.
public static double currInterestRate = 0.04;

}

This approach will ensure the static field is assigned only once, regardless of how many objects you create. However, what if the value for your static data needed to be obtained at runtime? For example, in a typical banking application, the value of an interest rate variable would be read from a database or external file. Performing such tasks usually requires a method scope such as a constructor to execute the code statements.
此方法将确保静态字段只分配一次,无论您创建多少个对象。但是,如果需要在运行时获取静态数据的值,该怎么办?例如,在典型的银行应用程序中,将从数据库或外部文件中读取利率变量的值。执行此类任务通常需要方法范围(如构造函数)来执行代码语句。

For this reason, C# allows you to define a static constructor, which allows you to safely set the values of your static data. Consider the following update to your class:
因此,C# 允许您定义静态构造函数,从而可以安全地设置静态数据的值。请考虑对类进行以下更新:

class SavingsAccount
{
public double currBalance;
public static double currInterestRate;

public SavingsAccount(double balance)
{
currBalance = balance;
}

// A static constructor!
static SavingsAccount()
{

}

}

Console.WriteLine("In static constructor!"); currInterestRate = 0.04;

Simply put, a static constructor is a special constructor that is an ideal place to initialize the values of static data when the value is not known at compile time (e.g., you need to read in the value from an external file, read in the value from a database, generate a random number, or whatnot). If you were to rerun the previous code, you would find the output you expect. Note that the message “In static constructor!” prints only one time, as the CoreCLR calls all static constructors before the first use (and never calls them again for that instance of the application).
简单地说,静态构造函数是一个特殊的构造函数,当值在编译时未知时,它是初始化静态数据值的理想位置(例如,您需要从外部文件中读取值,从数据库中读取值,生成随机数,等等)。如果要重新运行前面的代码,则会找到所需的输出。请注意,消息“In static constructor!”只打印一次,因为 CoreCLR 在首次使用之前调用所有静态构造函数(并且永远不会为该应用程序的该实例再次调用它们)。

Fun with Static Data In static constructor!
Interest Rate is: 0.04 Interest Rate is: 0.08

Here are a few points of interest regarding static constructors:
以下是有关静态构造函数的一些关注点:

•A given class may define only a single static constructor. In other words, the static constructor cannot be overloaded.
给定的类只能定义单个静态构造函数。换句话说,静态构造函数不能重载。
•A static constructor does not take an access modifier and cannot take any parameters.
静态构造函数不采用访问修饰符,也不能采用任何参数。
•A static constructor executes exactly one time, regardless of how many objects of the type are created.
静态构造函数只执行一次,而不考虑创建了多少类型的对象。
•The runtime invokes the static constructor when it creates an instance of the class or before accessing the first static member invoked by the caller.
运行时在创建类的实例时或在访问调用方调用的第一个静态成员之前调用静态构造函数。
•The static constructor executes before any instance-level constructors.
静态构造函数在任何实例级构造函数之前执行。

Given this modification, when you create new SavingsAccount objects, the value of the static data is preserved, as the static member is set only one time within the static constructor, regardless of the number of objects created.
根据此修改,当您创建新的 Savings Account 对象时,将保留静态数据的值,因为静态成员在静态构造函数中仅设置一次,而不考虑创建的对象数。

Defining Static Classes

定义静态类

It is also possible to apply the static keyword directly on the class level. When a class has been defined as static, it is not creatable using the new keyword, and it can contain only members or data fields marked with the static keyword. If this is not the case, you receive compiler errors.
也可以直接在类级别应用 static 关键字。当类被定义为静态时,它不能使用 new 关键字创建,并且它只能包含用 static 关键字标记的成员或数据字段。如果不是这种情况,您将收到编译器错误。

■ Note recall that a class (or structure) that exposes only static functionality is often termed a utility class. When designing a utility class, it is good practice to apply the static keyword to the class definition.
请注意,仅公开静态功能的类(或结构)通常称为实用程序类。设计实用程序类时,最好将 static 关键字应用于类定义。

At first glance, this might seem like a fairly odd feature, given that a class that cannot be created does not appear all that helpful. However, if you create a class that contains nothing but static members and/or constant data, the class has no need to be allocated in the first place! To illustrate, create a new class named TimeUtilClass and define it as follows:
乍一看,这似乎是一个相当奇怪的功能,因为无法创建的类似乎并没有那么有用。但是,如果您创建一个只包含静态成员和/或常量数据的类,则首先不需要分配该类!为了说明这一点,请创建一个名为 TimeUtilClass 的新类,并按如下方式定义它:

namespace StaticDataAndMembers;
// Static classes can only
// contain static members! static class TimeUtilClass

{
public static void PrintTime()
=> Console.WriteLine(DateTime.Now.ToShortTimeString());

public static void PrintDate()
=> Console.WriteLine(DateTime.Today.ToShortDateString());
}

Given that this class has been defined with the static keyword, you cannot create an instance of the TimeUtilClass using the new keyword. Rather, all functionality is exposed from the class level. To test this class, add the following to the top-level statements:
鉴于此类已使用 static 关键字定义,则无法使用 new 关键字创建 TimeUtilClass 的实例。相反,所有功能都是从类级别公开的。若要测试此类,请将以下内容添加到顶级语句中:

// These compile just fine. TimeUtilClass.PrintDate(); TimeUtilClass.PrintTime();

// Compiler error! Can’t create instance of static classes!
TimeUtilClass u = new TimeUtilClass(); Console.ReadLine();

Importing Static Members via the C# using Keyword

使用 Keyword 通过 C# 导入静态成员

C# 6 added support for importing static members with the using keyword. To illustrate, consider the C# file currently defining the utility class. Because you are making calls to the WriteLine() method of the Console class, as well as the Now and Today properties of the DateTime class, you must have a using statement for the System namespace. Since the members of these classes are all static, you could alter your code file with the following static using directives:
C# 6 添加了对使用 using 关键字导入静态成员的支持。为了进行说明,请考虑当前定义实用工具类的 C# 文件。由于要调用 Console 类的 WriteLine() 方法以及 DateTime 类的 Now 和 Today 属性,因此必须具有 System 命名空间的 using 语句。由于这些类的成员都是静态的,因此可以使用以下静态 using 指令更改代码文件:

// Import the static members of Console and DateTime.
// 导入控制台和日期时间的静态成员。
using static System.Console;
using static System.DateTime;

With these “static imports,” the remainder of your code file is able to directly use the static members of the Console and DateTime classes, without the need to prefix the defining class. For example, you could update your utility class like so:
通过这些“静态导入”,代码文件的其余部分可以直接使用 Console 和 DateTime 类的静态成员,而无需为定义类添加前缀。例如,您可以像这样更新实用程序类:

static class TimeUtilClass
{
public static void PrintTime()
=> WriteLine(Now.ToShortTimeString());

public static void PrintDate()
=> WriteLine(Today.ToShortDateString());
}

A more realistic example of code simplification with importing static members might involve a C# class that is making substantial use of the System.Math class (or some other utility class). Since this class has nothing but static members, it could be somewhat easier to have a static using statement for this type and then directly call into the members of the Math class in your code file.
使用导入静态成员简化代码的一个更实际的示例可能涉及大量使用 System.Math 类(或某个其他实用工具类)的 C# 类。由于此类只有静态成员,因此为这种类型的静态 using 语句使用静态语句,然后直接调用代码文件中 Math 类的成员可能会更容易一些。

However, be aware that overuse of static import statements could result in potential confusion. First, what if multiple classes define a WriteLine() method? The compiler is confused and so are others reading your code. Second, unless developers are familiar with the .NET Core code libraries, they might not know that WriteLine() is a member of the Console class. Unless people were to notice the set of static imports at the top of a C# code file, they might be quite unsure where this method is actually defined. For these reasons, I will limit the use of static using statements in this text.
但是,请注意,过度使用静态导入语句可能会导致潜在的混淆。首先,如果多个类定义一个 WriteLine() 方法怎么办?编译器很困惑,其他人也在阅读您的代码。其次,除非开发人员熟悉 .NET Core 代码库,否则他们可能不知道 WriteLine() 是 Console 类的成员。除非人们注意到 C# 代码文件顶部的静态导入集,否则他们可能非常不确定此方法的实际定义位置。出于这些原因,我将在本文中限制静态 using 语句的使用。

In any case, at this point in the chapter, you should feel comfortable defining simple class types containing constructors, fields, and various static (and nonstatic) members. Now that you understand the basics of class construction, you can formally investigate the three pillars of object-oriented programming.
无论如何,在本章的这一点上,您应该可以轻松地定义包含构造函数、字段和各种静态(和非静态)成员的简单类类型。现在您已经了解了类构造的基础知识,您可以正式研究面向对象编程的三大支柱。

Defining the Pillars of OOP

定义 OOP 的支柱

All object-oriented languages (C#, Java, C++, Visual Basic, etc.) must contend with these three core principles, often called the pillars of object-oriented programming (OOP):
所有面向对象的语言(C#,Java,C++,Visual Basic等)都必须与这三个核心原则相抗衡,通常称为面向对象编程(OOP)的支柱:

•Encapsulation: How does this language hide an object’s internal implementation details and preserve data integrity?
封装:这种语言如何隐藏对象的内部实现细节并保持数据完整性?
•Inheritance: How does this language promote code reuse?
继承:这种语言如何促进代码重用?
•Polymorphism: How does this language let you treat related objects in a similar way?
多态性:这种语言如何让你以类似的方式处理相关对象?

Before digging into the details of each pillar, it is important that you understand their basic roles. Here is an overview of each pillar, which will be examined in full detail over the remainder of this chapter and the next.
在深入研究每个支柱的细节之前,了解它们的基本角色非常重要。以下是每个支柱的概述,将在本章的其余部分和下一章中对其进行全面详细的研究。

■ Note The examples in this section are contained in the oopexamples project of the chapter’s code samples.
注意 本节中的示例包含在本章代码示例的 oopexamples 项目中。

Understanding the Role of Encapsulation

了解封装的作用

The first pillar of OOP is called encapsulation. This trait boils down to the language’s ability to hide unnecessary implementation details from the object user. For example, assume you are using a class named DatabaseReader, which has two primary methods, named Open() and Close().
OOP 的第一个支柱称为封装。这种特性归结为语言能够对对象用户隐藏不必要的实现细节。例如,假设您正在使用一个名为 DatabaseReader 的类,该类有两个主要方法,分别名为 Open() 和 Close()。

// Assume this class encapsulates the details of opening and closing a database.
假设此类封装了打开和关闭数据库的详细信息。
DatabaseReader dbReader = new DatabaseReader(); dbReader.Open(@"C:\AutoLot.mdf");

// Do something with data file and close the file.
// 对数据文件执行某些操作并关闭该文件。
dbReader.Close();

The fictitious DatabaseReader class encapsulates the inner details of locating, loading, manipulating, and closing a data file. Programmers love encapsulation, as this pillar of OOP keeps coding tasks simpler. There is no need to worry about the numerous lines of code that are working behind the scenes to carry out the work of the DatabaseReader class. All you do is create an instance and send the appropriate messages (e.g., “Open the file named AutoLot.mdf located on my C drive”).
虚构的 DatabaseReader 类封装了查找、加载、操作和关闭数据文件的内部详细信息。程序员喜欢封装,因为OOP的这一支柱使编码任务更简单。无需担心在幕后执行 DatabaseReader 类工作的大量代码行。您要做的就是创建一个实例并发送相应的消息(例如,“打开位于我的 C 驱动器上的名为 AutoLot 的文件.mdf”)。

Closely related to the notion of encapsulating programming logic is the idea of data protection. Ideally, an object’s state data should be specified using either the private, internal, or protected keyword. In this way, the outside world must ask politely in order to change or obtain the underlying value. This is a good thing, as publicly declared data points can easily become corrupted (ideally by accident rather than intent!). You will formally examine this aspect of encapsulation in just a bit.
与封装编程逻辑的概念密切相关的是数据保护的概念。理想情况下,应使用私有、内部或受保护的关键字指定对象的状态数据。这样,外界必须礼貌地询问,才能改变或获得潜在的价值。这是一件好事,因为公开声明的数据点很容易被破坏(理想情况下是偶然的,而不是故意的!稍后您将正式检查封装的这一方面。

Understanding the Role of Inheritance

了解继承的作用

The next pillar of OOP, inheritance, boils down to the language’s ability to allow you to build new class definitions based on existing class definitions. In essence, inheritance allows you to extend the behavior of a base (or parent) class by inheriting core functionality into the derived subclass (also called a child class). Figure 5-2 shows a simple example.
OOP的下一个支柱,继承,归结为语言的能力,它允许你基于现有的类定义构建新的类定义。实质上,继承允许您通过将核心功能继承到派生子类(也称为子类)中来扩展基(或父)类的行为。图 5-2 显示了一个简单的示例。

Alt text
Figure 5-2. The “is-a” relationship
图 5-2。 “是”关系

You can read the diagram in Figure 5-2 as “A Hexagon is-a Shape that is-an Object.” When you have classes related by this form of inheritance, you establish “is-a” relationships between types. The “is-a” relationship is termed inheritance.
您可以将图 5-2 中的图表解读为“六边形是一个形状,即一个对象”。当您具有通过这种继承形式关联的类时,您将在类型之间建立“is-a”关系。“is-a”关系称为继承。

Here, you can assume that Shape defines some number of members that are common to all descendants (maybe a value to represent the color to draw the shape and other values to represent the height and width). Given that the Hexagon class extends Shape, it inherits the core functionality defined by Shape and Object, as well as defines additional hexagon-related details of its own (whatever those may be).
在这里,您可以假设 Shape 定义了所有后代共有的一些成员数(可能是一个值来表示绘制形状的颜色,其他值来表示高度和宽度)。鉴于 Hexagon 类扩展了 Shape,它继承了 Shape 和 Object 定义的核心功能,并定义了自己的其他与六边形相关的细节(无论这些细节是什么)。

■ Note Under the .neT/.neT Core platforms, System.Object is always the topmost parent in any class hierarchy, which defines some general functionality for all types (fully described in Chapter 6).
注意 在.neT/.neT Core平台下,System.Object 始终是任何类层次结构中最顶层的父级,它为所有类型定义了一些通用功能(在第 6 章中有完整描述)。

There is another form of code reuse in the world of OOP: the containment/delegation model also known as the “has-a” relationship or aggregation. This form of reuse is not used to establish parent-child relationships. Rather, the “has-a” relationship allows one class to define a member variable of another class and expose its functionality (if required) to the object user indirectly.
在 OOP 世界中还有另一种形式的代码重用:包含/委派模型,也称为“has-a”关系或聚合。这种形式的重用不用于建立父子关系。相反,“has-a”关系允许一个类定义另一个类的成员变量,并间接向对象用户公开其功能(如果需要)。

For example, assume you are again modeling an automobile. You might want to express the idea that a car “has-a” radio. It would be illogical to attempt to derive the Car class from a Radio or vice versa (a Car “is-a” Radio? I think not!). Rather, you have two independent classes working together, where the Car class creates and exposes the Radio’s functionality.
例如,假设您再次对汽车进行建模。您可能想表达汽车“有”收音机的想法。试图从收音机中派生 Car 类是不合逻辑的,反之亦然(汽车“是”收音机?我认为不是!相反,您有两个独立的类一起工作,其中 Car 类创建并公开无线电的功能。

namespace OopExamples; class Radio
{
public void Power(bool turnOn)
{
Console.WriteLine("Radio on: {0}", turnOn);
}
}

namespace OopExamples; class Car
{
// Car ‘has-a’ Radio.
private Radio myRadio = new Radio(); public void TurnOnRadio(bool onOff)
{
// Delegate call to inner object. myRadio.Power(onOff);
}
}

Notice that the object user has no clue that the Car class is using an inner Radio object.
请注意,对象用户不知道 Car 类正在使用内部 Radio 对象。

using OopExamples;

Console.WriteLine("— Fun with OOP examples —");

// Call is forwarded to Radio internally. Car viper = new Car(); viper.TurnOnRadio(false);

Understanding the Role of Polymorphism

了解多态性的作用

The final pillar of OOP is polymorphism. This trait captures a language’s ability to treat related objects in a similar manner. Specifically, this tenant of an object-oriented language allows a base class to define aset of members (formally termed the polymorphic interface) that are available to all descendants. A class’s polymorphic interface is constructed using any number of virtual or abstract members (see Chapter 6 for full details).
OOP的最后一个支柱是多态性。此特征捕获了语言以类似方式处理相关对象的能力。具体来说,面向对象语言的这个租户允许基类定义可供所有后代使用的成员集(正式称为多态接口)。类的多态接口是使用任意数量的虚拟或抽象成员构造的(有关完整详细信息,请参阅第 6 章)。

In a nutshell, a virtual member is a member in a base class that defines a default implementation that may be changed (or more formally speaking, overridden) by a derived class. In contrast, an abstract method is a member in a base class that does not provide a default implementation but does provide a signature.
When a class derives from a base class defining an abstract method, it must be overridden by a derived type. In either case, when derived types override the members defined by a base class, they are essentially redefining how they respond to the same request.
OOP的最后一个支柱是多态性。此特征捕获了语言以类似方式处理相关对象的能力。具体来说,面向对象语言的这个租户允许基类定义可供所有后代使用的成员集(正式称为多态接口)。类的多态接口是使用任意数量的虚拟或抽象成员构造的(有关完整详细信息,请参阅第 6 章)。

To preview polymorphism, let’s provide some details behind the shapes hierarchy shown in Figure 5-3.
为了预览多态性,让我们提供图 5-3 中所示的形状层次结构背后的一些详细信息。

Assume that the Shape class has defined a virtual method named Draw() that takes no parameters. Given that every shape needs to render itself in a unique manner, subclasses such as Hexagon and Circle are free to override this method to their own liking (see Figure 5-3).
假设 Shape 类定义了一个名为 Draw() 的虚拟方法,该方法不带任何参数。鉴于每个形状都需要以独特的方式呈现自身,Hexagon 和 Circle 等子类可以根据自己的喜好自由覆盖此方法(参见图 5-3)。

Alt text

Figure 5-3. Classical polymorphism
图 5-3。 经典多态性

After a polymorphic interface has been designed, you can begin to make various assumptions in your code. For example, given that Hexagon and Circle derive from a common parent (Shape), an array of Shape types could contain anything deriving from this base class. Furthermore, given that Shape defines
a polymorphic interface to all derived types (the Draw() method in this example), you can assume each member in the array has this functionality.
设计多态接口后,可以开始在代码中做出各种假设。例如,假设 Hexagon 和 Circle 派生自公共父级 (Shape),则 Shape 类型的数组可以包含从此基类派生的任何内容。此外,鉴于形状定义所有派生类型的多态接口(此示例中的 Draw() 方法),可以假定数组中的每个成员都具有此功能。

Consider the following code, which instructs an array of Shape-derived types to render themselves using the Draw() method:
请考虑以下代码,该代码指示 Shape 派生类型的数组使用 Draw() 方法呈现自身:

Shape[] myShapes = new Shape[3]; myShapes[0] = new Hexagon(); myShapes[1] = new Circle(); myShapes[2] = new Hexagon();

foreach (Shape s in myShapes)
{
// Use the polymorphic interface! s.Draw();
}
Console.ReadLine();

This wraps up our brisk overview of the pillars of OOP. Now that you have the theory in your mind, the remainder of this chapter explores further details of how encapsulation is handled under C#, starting with a look at access modifiers. Chapter 6 will tackle the details of inheritance and polymorphism.
以上总结了我们对 OOP 支柱的快速概述。现在您已经掌握了该理论,本章的其余部分将探讨如何在 C# 下处理封装的更多详细信息,首先介绍访问修饰符。第6章将讨论遗传和多态性的细节。

Understanding C# Access Modifiers (Updated 7.2)

了解 C# 访问修饰符(7.2 更新)

When working with encapsulation, you must always consider which aspects of a type are visible to various parts of your application. Specifically, types (classes, interfaces, structures, enumerations, and delegates) as well as their members (properties, methods, constructors, and fields) are defined using a specific keyword to control how “visible” the item is to other parts of your application. Although C# defines numerous keywords to control access, they differ on where they can be successfully applied (type or member). Table 5-1 documents the role of each access modifier and where it may be applied.
使用封装时,必须始终考虑类型的哪些方面对应用程序的各个部分可见。具体而言,类型(类、接口、结构、枚举和委托)及其成员(属性、方法、构造函数和字段)是使用特定关键字定义的,以控制项对应用程序其他部分的“可见”程度。尽管 C# 定义了许多关键字,为了控制访问,它们在成功应用的位置(类型或成员)上有所不同。表 5-1 记录了每个访问修饰符的角色及其应用位置。

Table 5-1. C# Access Modifiers
表 5-1. C# 访问修饰符

C# Access Modifier
C# 访问修饰符
May Be Applied To
可应用于
Meaning in Life
意义
public Types or type members
类型或类型成员
Public items have no access restrictions. A public member can be accessed from an object, as well as any derived class. A public type can be accessed from other external assemblies.
公共项目没有访问限制。可以从对象以及任何派生类访问公共成员。可以从其他外部程序集访问公共类型。
private Type members or nested types
类型成员或嵌套类型
Private items can be accessed only by the class (or structure) that defines the item.
私有项只能由定义该项的类(或结构)访问。
protected Type members or nested types
类型成员或嵌套类型
Protected items can be used by the class that defines it and any child class. They cannot be accessed from outside the inheritance chain.
受保护项可由定义它的类和任何子类使用。不能从继承链外部访问它们。
internal Types or type members
类型或类型成员
Internal items are accessible only within the current assembly. Other assemblies can be explicitly granted permission to see the internal items.
内部项只能在当前程序集内访问。可以显式授予其他程序集查看内部项的权限。
protected internal Type members or nested types
类型成员或嵌套类型
When the protected and internal keywords are combined on an item, the item is accessible within the defining assembly, within the defining class, and by derived classes inside or outside of the defining assembly.
在项上组合受保护关键字和内部关键字时,可以在定义程序集内、定义类内以及定义程序集内部或外部的派生类访问该项。
private protected (new 7.2) Type members or nested types
类型成员或嵌套类型
When the private and protected keywords are combined on an item, the item is accessible within the defining class and by derived classes in the same assembly.
在项上组合私有关键字和受保护关键字时,可以在定义类中访问该项,也可以由同一程序集中的派生类访问该项。

In this chapter, you are concerned only with the public and private keywords. Later chapters will examine the role of the internal and protected internal modifiers (useful when you build code libraries and unit tests) and the protected modifier (useful when you are creating class hierarchies).
在本章中,您只关注公共和私有关键字。后面的章节将研究内部和受保护的内部修饰符(在构建代码库和单元测试时很有用)和受保护修饰符(在创建类层次结构时很有用)的作用。

Using the Default Access Modifiers

使用默认访问修饰符

By default, type members are implicitly private, while types are implicitly internal. Thus, the following class definition is automatically set to internal, while the type’s default constructor is automatically set to private (however, as you would suspect, there are few times you would want a private class constructor):
默认情况下,类型成员是隐式私有的,而类型是隐式内部的。因此,以下类定义自动设置为内部,而类型的默认构造函数自动设置为 private(但是,正如您所怀疑的那样,很少需要私有类构造函数):

// An internal class with a private default constructor. class Radio
{
Radio(){}
}

If you want to be explicit, you could add these keywords yourself with no ill effect (beyond a few additional keystrokes).
如果你想明确一点,你可以自己添加这些关键字,而不会产生任何不良影响(除了几个额外的击键)。

// An internal class with a private default constructor. internal class Radio
{
private Radio(){}
}

To allow other parts of a program to invoke members of an object, you must define them with the public keyword (or possibly with the protected keyword, which you will learn about in the next chapter). As well, if you want to expose the Radio to external assemblies (again, useful when building larger solutions or code libraries), you will need to add the public modifier.
要允许程序的其他部分调用对象的成员,您必须使用 public 关键字(或者可能使用 protected 关键字,您将在下一章中了解)定义它们。此外,如果要向外部程序集公开 Radio(同样,在生成较大的解决方案或代码库时很有用),则需要添加 public 修饰符。

// A public class with a public default constructor. public class Radio
{
public Radio(){}
}

Using Access Modifiers and Nested Types

使用访问修饰符和嵌套类型

As mentioned in Table 5-1, the private, protected, protected internal, and private protected access modifiers can be applied to a nested type. Chapter 6 will examine nesting in detail. What you need to know at this point, however, is that a nested type is a type declared directly within the scope of class or structure. By way of example, here is a private enumeration (named CarColor) nested within a public class (named SportsCar):
如表 5-1 中所述,私有、受保护、受保护的内部和私有受保护的访问修饰符可以应用于嵌套类型。第6章将详细研究嵌套。但是,此时您需要知道的是,嵌套类型是直接在类或结构范围内声明的类型。例如,下面是嵌套在公共类(名为 SportsCar)中的私有枚举(名为 CarColor):

namespace OopExamples; public class SportsCar
{
// OK! Nested types can be marked private. private enum CarColor
{
Red, Green, Blue
}
}

Here, it is permissible to apply the private access modifier on the nested type. However, non-nested types (such as the SportsCar) can be defined only with the public or internal modifier. Therefore, the following class definition is illegal:
在这里,允许在嵌套类型上应用专用访问修饰符。但是,非嵌套类型(如 SportsCar)只能使用公共或内部修饰符进行定义。因此,以下类定义是非法的:

// Error! Non-nested types cannot be marked private! private class SportsCar
{}

Understanding the First Pillar: C#’s Encapsulation Services

了解第一个支柱:C# 的封装服务

The concept of encapsulation revolves around the notion that an object’s data should not be directly accessible from an object instance. Rather, class data is defined as private. If the object user wants to alter the state of an object, it does so indirectly using public members. To illustrate the need for encapsulation services, assume you have created the following class definition:
封装的概念围绕着这样一个概念,即不应从对象实例直接访问对象的数据。相反,类数据被定义为私有的。如果对象用户想要更改对象的状态,它将使用公共成员间接执行此操作。为了说明对封装服务的需求,假设您已经创建了以下类定义:

// A class with a single public field. class Book
{
public int numberOfPages;
}

The problem with public data is that the data itself has no ability to “understand” whether the current value to which it is assigned is valid with regard to the current business rules of the system. As you know, the upper range of a C# int is quite large (2,147,483,647). Therefore, the compiler allows the following assignment:
公共数据的问题在于,数据本身无法“理解”它所赋值的当前值对于系统的当前业务规则是否有效。如您所知,C# int 的上限非常大 (2,147,483,647)。因此,编译器允许以下赋值:

// Humm. That is one heck of a mini-novel! Book miniNovel = new Book(); miniNovel.numberOfPages = 30_000_000;

Although you have not overflowed the boundaries of an int data type, it should be clear that a mini- novel with a page count of 30,000,000 pages is a bit unreasonable. As you can see, public fields do not provide a way to trap logical upper (or lower) limits. If your current system has a business rule that states a book must be between 1 and 1,000 pages, you are at a loss to enforce this programmatically. Because of this, public fields typically have no place in a production-level class definition.
虽然你没有溢出 int 数据类型的界限,但应该清楚的是,页数为 30,000,000 页的迷你小说有点不合理。如您所见,公共字段不提供捕获逻辑上限(或下限)的方法。如果您当前的系统具有规定书籍必须介于 1 到 1,000 页之间的业务规则,则您无法以编程方式强制执行此规则。因此,公共字段在生产级类定义中通常没有位置。

■ Note To be more specific, members of a class that represent an object’s state should not be marked as public. as you will see later in this chapter, public constants and public read-only fields are quite useful.
注意 更具体地说,表示对象状态的类的成员不应标记为公共。 正如您将在本章后面看到的,公共常量和公共只读字段非常有用。

Encapsulation provides a way to preserve the integrity of an object’s state data. Rather than defining public fields (which can easily foster data corruption), you should get in the habit of defining private data, which is indirectly manipulated using one of two main techniques.
封装提供了一种保持对象状态数据完整性的方法。与其定义公共字段(这很容易助长数据损坏),不如养成定义私有数据的习惯,这是使用两种主要技术之一间接操作的。

•You can define a pair of public accessor (get) and mutator (set) methods.
您可以定义一对公共访问器 (get) 和突变器 (set) 方法。
•You can define a public property.
您可以定义公共属性。

Whichever technique you choose, the point is that a well-encapsulated class should protect its data and hide the details of how it operates from the prying eyes of the outside world. This is often termed black- box programming. The beauty of this approach is that an object is free to change how a given method is implemented under the hood. It does this without breaking any existing code making use of it, provided that the parameters and return values of the method remain constant.
无论您选择哪种技术,关键是封装良好的类应该保护其数据,并隐藏其操作方式的细节,以免被外界窥探。这通常被称为黑盒编程。这种方法的优点在于,对象可以自由地更改给定方法在后台的实现方式。它这样做不会破坏任何使用它的现有代码,前提是该方法的参数和返回值保持不变。

Encapsulation Using Traditional Accessors and Mutators

使用传统访问器和突变器进行封装

Over the remaining pages in this chapter, you will be building a fairly complete class that models a general employee. To get the ball rolling, create a new Console Application project named EmployeeApp and create a new class file named Employee.cs. Update the Employee class with the following namespace, fields, methods, and constructors:
在本章的其余页面中,您将构建一个相当完整的类来模拟一般员工。若要使球滚动,请创建一个名为 EmployeeApp 的新控制台应用程序项目,并创建一个名为 Employee.cs 的新类文件。使用以下命名空间、字段、方法和构造函数更新 Employee 类:

namespace EmployeeApp; class Employee
{
// Field data.
private string _empName; private int _empId; private float _currPay;

// Constructors.
public Employee() {}
public Employee(string name, int id, float pay)

{
_empName = name;
_empId = id;
_currPay = pay;
}

// Methods.
public void GiveBonus(float amount) => _currPay += amount; public void DisplayStats()
{
Console.WriteLine("Name: {0}", _empName);
Console.WriteLine("ID: {0}", _empId);
Console.WriteLine("Pay: {0}", _currPay);
}
}

Notice that the fields of the Employee class are currently defined using the private keyword. Given this, the _empName, _empId, and _currPay fields are not directly accessible from an object variable. Therefore, the following logic in your code would result in compiler errors:
请注意,Employee 类的字段当前是使用私钥定义的。鉴于此,无法从对象变量直接访问 _empName、_empId 和 _currPay 字段。因此,代码中的以下逻辑将导致编译器错误:

Employee emp = new Employee();
// Error! Cannot directly access private members
// from an object!
emp._empName = "Marv";

If you want the outside world to interact with a worker’s full name, a traditional approach is to define an accessor (get method) and a mutator (set method). The role of a get method is to return to the caller the current value of the underlying state data. A set method allows the caller to change the current value of the underlying state data, as long as the defined business rules are met.
如果希望外部世界与工作人员的全名进行交互,传统的方法是定义访问器(get 方法)和突变器(set 方法)。get 方法的作用是将基础状态数据的当前值返回给调用方。只要满足定义的业务规则,set 方法就允许调用方更改基础状态数据的当前值。

To illustrate, let’s encapsulate the empName field. To do so, add the following public methods to the Employee class. Notice that the SetName() method performs a test on the incoming data to ensure the string is 15 characters or less. If it is not, an error prints to the console and returns without making a change to the empName field.
为了说明这一点,让我们封装 empName 字段。为此,请将以下公共方法添加到 Employee 类中。请注意,SetName() 方法对传入数据执行测试,以确保字符串不超过 15 个字符。如果不是,则会将错误打印到控制台并返回,而不对 empName 字段进行更改。

■ Note if this were a production-level class, you would probably also check the character length for an employee’s name within your constructor logic. ignore this detail for the time being, as you will clean up this code in just a bit when you examine property syntax.
请注意,如果这是一个生产级类,您可能还会在构造函数逻辑中检查员工姓名的字符长度。 暂时忽略此细节,因为在检查属性语法时,您将稍微清理一下此代码。

class Employee
{
// Field data.
private string _empName;

// Accessor (get method).
public string GetName() => _empName;

// Mutator (set method).
public void SetName(string name)

{
// Do a check on incoming value
// before making assignment. if (name.Length > 15)
{
Console.WriteLine("Error! Name length exceeds 15 characters!");
}
else
{
_empName = name;
}
}
}
This technique requires two uniquely named methods to operate on a single data point. To test your new methods, update your code method as follows:
此技术需要两个唯一命名的方法才能对单个数据点进行操作。若要测试新方法,请按如下所示更新代码方法:

using EmployeeApp;

Console.WriteLine(" Fun with Encapsulation \n");
Employee emp = new Employee("Marvin", 456, 30_000); emp.GiveBonus(1000);
emp.DisplayStats();

// Use the get/set methods to interact with the object’s name. emp.SetName("Marv");
Console.WriteLine("Employee is named: {0}", emp.GetName()); Console.ReadLine();

Because of the code in your SetName() method, if you attempted to specify more than 15 characters (see the following), you would find the hard-coded error message printed to the console:
由于 SetName() 方法中的代码,如果您尝试指定超过 15 个字符(请参阅以下内容),您会发现硬编码的错误消息打印到控制台:

// Longer than 15 characters! Error will print to console.
Employee emp2 = new Employee(); emp2.SetName("Xena the warrior princess");

Console.ReadLine();

So far, so good. You have encapsulated the private empName field using two public methods named GetName() and SetName(). If you were to further encapsulate the data in the Employee class, you would need to add various additional methods (such as GetID(), SetID(), GetCurrentPay(), SetCurrentPay()). Each of the mutator methods could also have various lines of code to check for additional business rules. While this could certainly be done, the C# language has a useful alternative notation to encapsulate class data.
目前为止,一切都好。您已经使用名为 GetName() 和 SetName() 的两个公共方法封装了私有 empName 字段。如果要进一步将数据封装在 Employee 类中,则需要添加各种其他方法(例如 GetID()、SetID()、GetCurrentPay()、SetCurrentPay())。 每个突变器方法还可以有各种代码行来检查其他业务规则。虽然这当然可以做到,但 C# 语言有一个有用的替代表示法来封装类数据。

Encapsulation Using Properties

使用属性封装

Although you can encapsulate a piece of field data using traditional get and set methods, .NET Core languages prefer to enforce data encapsulation state data using properties. First, understand that properties are just a container for “real” accessor and mutator methods, named get and set, respectively. Therefore, as a class designer, you are still able to perform any internal logic necessary before making the value assignment (e.g., uppercase the value, scrub the value for illegal characters, check the bounds of a numerical value, etc.).
尽管可以使用传统的 get 和 set 方法封装一段字段数据,但 .NET Core 语言更喜欢使用属性强制实施数据封装状态数据。首先,了解属性只是“真正的”访问器和突变器方法的容器,分别命名为 get 和 set。因此,作为类设计者,您仍然可以在进行值赋值之前执行任何必要的内部逻辑(例如,大写值、清除非法字符的值、检查数值的边界等)。

Here is the updated Employee class, now enforcing encapsulation of each field using property syntax rather than traditional get and set methods:
下面是更新的 Employee 类,现在使用属性语法而不是传统的 get 和 set 方法强制封装每个字段:

class Employee
{
// Field data.
private string _empName; private int _empId; private float _currPay;
// Properties! public string Name
{
get { return _empName; } set
{
if (value.Length > 15)
{
Console.WriteLine("Error! Name length exceeds 15 characters!");
}
else
{
_empName = value;
}
}
}
// We could add additional business rules to the sets of these properties;
// however, there is no need to do so for this example.
public int Id
{
get { return _empId; } set { _empId = value; }
}
public float Pay
{

}

}

get { return _currPay; } set { _currPay = value; }

A C# property is composed by defining a get scope (accessor) and set scope (mutator) directly within the property itself. Notice that the property specifies the type of data it is encapsulating by what appears to be a return value. Also take note that, unlike a method, properties do not make use of parentheses (not even empty parentheses) when being defined. Consider the following commentary on your current Id property:
C# 属性是通过直接在属性本身中定义 get 范围(访问器)和设置范围(突变器)来组成的。请注意,该属性指定它通过看似返回值的内容封装的数据类型。另请注意,与方法不同,属性在定义时不使用括号(甚至不使用空括号)。请考虑以下有关当前 Id 属性的注释:

// The ‘int’ represents the type of data this property encapsulates. public int Id // Note lack of parentheses.
{
get { return _empId; } set { _empID = value; }
}

Within a set scope of a property, you use a token named value, which is used to represent the incoming value used to assign the property by the caller. This token is not a true C# keyword but is what is known as
a contextual keyword. When the token value is within the set scope of the property, it always represents the value being assigned by the caller, and it will always be the same underlying data type as the property itself. Thus, notice how the Name property can still test the range of the string as so:
在属性的设置范围内,使用名为 value 的令牌,该令牌用于表示调用方用于分配属性的传入值。此标记不是真正的 C# 关键字,而是所谓的

上下文关键字。当令牌值在属性的设置范围内时,它始终表示调用方分配的值,并且它将始终与属性本身相同的基础数据类型。因此,请注意 Name 属性如何仍可以测试字符串的范围,如下所示:

public string Name
{
get { return _empName; } set
{
// Here, value is really a string. if (value.Length > 15)
{ Console.WriteLine("Error! Name length exceeds 15 characters!");
}
else
{
empName = value;
}
}
}

After you have these properties in place, it appears to the caller that it is getting and setting a public point of data; however, the correct get and set block is called behind the scenes to preserve encapsulation.
拥有这些属性后,调用方似乎正在获取和设置公共数据点;但是,在后台调用正确的获取和设置块以保留封装。

using EmployeeApp;
Console.WriteLine(" Fun with Encapsulation \n"); Employee emp = new Employee("Marvin", 456, 30000); emp.GiveBonus(1000);
emp.DisplayStats();

// Reset and then get the Name property.
emp.Name = "Marv";
Console.WriteLine("Employee is named: {0}", emp.Name); Console.ReadLine();

Properties (as opposed to accessor and mutator methods) also make your types easier to manipulate, in that properties are able to respond to the intrinsic operators of C#. To illustrate, assume that the Employee class type has an internal private member variable representing the age of the employee. Here is the relevant update (notice the use of constructor chaining):
属性(与访问器和突变器方法相反)也使类型更易于操作,因为属性能够响应 C# 的内部运算符。为了说明这一点,假定 Employee 类类型具有一个表示雇员年龄的内部私有成员变量。以下是相关更新(请注意构造函数链接的使用):

class Employee
{

// New field and property. private int _empAge; public int Age
{
get { return _empAge; } set { _empAge = value; }
}

// Updated constructors.
public Employee() {}
public Employee(string name, int id, float pay)
:this(name, 0, id, pay){}

public Employee(string name, int age, int id, float pay)
{
_empName = name;
_empId = id;
_empAge = age;
_currPay = pay;
}

// Updated DisplayStats() method now accounts for age.
public void DisplayStats()
{
Console.WriteLine("Name: {0}", _empName);
Console.WriteLine("ID: {0}", _empId);
Console.WriteLine("Age: {0}", _empAge);
Console.WriteLine("Pay: {0}", _currPay);
}
}

Now assume you have created an Employee object named joe. On his birthday, you want to increment the age by one. Using traditional accessor and mutator methods, you would need to write code such as the following:
现在假设您已经创建了一个名为 joe 的 Employee 对象。在他生日那天,你想把年龄增加一个。使用传统的访问器和突变器方法,您需要编写如下代码:

Employee joe = new Employee(); joe.SetAge(joe.GetAge() + 1);

However, if you encapsulate empAge using a property named Age, you are able to simply write this:
但是,如果使用名为 Age 的属性封装 empAge,则可以简单地编写以下内容:

Employee joe = new Employee(); joe.Age++;

Properties As Expression-Bodied Members (New 7.0)

作为表达式主体成员的属性(新 7.0)
As mentioned previously, property get and set accessors can also be written as expression-bodied members. The rules and syntax are the same: single-line methods can be written using the new syntax. So, the Age property could be written like this:
如前所述,属性 get 和 set 访问器也可以编写为表达式体成员。规则和语法是相同的:可以使用新语法编写单行方法。因此,Age 属性可以这样写:

public int Age
{
get => empAge;
set => empAge = value;
}

Both syntaxes compile down to the same IL, so which syntax you use is completely up to you. In this text, you will see a mix of both styles to keep visibility on them, not because I am adhering to a specific code style.
这两种语法都编译为同一个 IL,因此使用哪种语法完全取决于您。在本文中,您将看到两种样式的混合,以保持它们的可见性,而不是因为我坚持特定的代码样式。

Using Properties Within a Class Definition

在类定义中使用属性

Properties, specifically the set portion of a property, are common places to package up the business rules of your class. Currently, the Employee class has a Name property that ensures the name is no more than 15 characters. The remaining properties (ID, Pay, and Age) could also be updated with any relevant logic.
While this is well and good, also consider what a class constructor typically does internally. It will take the incoming parameters, check for valid data, and then make assignments to the internal private fields.
属性(特别是属性的集合部分)是打包类的业务规则的常用位置。目前,Employee 类具有一个 Name 属性,该属性可确保名称不超过 15 个字符。其余属性(ID、支付和年龄)也可以使用任何相关逻辑进行更新。虽然这很好,但也要考虑类构造函数通常在内部做什么。它将采用传入的参数,检查有效数据,然后分配给内部私有字段。

Currently, your master constructor does not test the incoming string data for a valid range, so you could update this member as so:
目前,主构造函数不会测试有效范围的传入字符串数据,因此可以按如下所示更新此成员:

public Employee(string name, int age, int id, float pay)
{
// Humm, this seems like a problem… if (name.Length > 15)
{
Console.WriteLine("Error! Name length exceeds 15 characters!");
}
else
{
_empName = name;
}
_empId = id;
_empAge = age;
_currPay = pay;
}

I am sure you can see the problem with this approach. The Name property and your master constructor are performing the same error checking. If you were also making checks on the other data points, you would have a good deal of duplicate code. To streamline your code and isolate all of your error checking to a central location, you will do well if you always use properties within your class whenever you need to get or set the values. Consider the following updated constructor:
我相信你可以看到这种方法的问题。Name 属性和主构造函数正在执行相同的错误检查。如果您还对其他数据点进行检查,则会有大量重复的代码。为了简化代码并将所有错误检查隔离到一个中心位置,如果在需要获取或设置值时始终使用类中的属性,则会做得很好。请考虑以下更新的构造函数:

public Employee(string name, int age, int id, float pay)
{
// Better! Use properties when setting class data.
// This reduces the amount of duplicate error checks. Name = name;
Age = age;
ID = id;
Pay = pay;
}

Beyond updating constructors to use properties when assigning values, it is good practice to use properties throughout a class implementation to ensure your business rules are always enforced. In many cases, the only time when you directly refer to the underlying private piece of data is within the property itself. With this in mind, here is your updated Employee class:
除了在分配值时更新构造函数以使用属性之外,最好在整个类实现中使用属性,以确保始终强制实施业务规则。在许多情况下,直接引用基础私有数据的唯一时间是在属性本身内。考虑到这一点,以下是您更新的员工类:

class Employee
{
// Field data.
private string _empName; private int _empId;

private float _currPay; private int _empAge;
// Constructors.
public Employee() { }
public Employee(string name, int id, float pay)
:this(name, 0, id, pay){}
public Employee(string name, int age, int id, float pay)
{
Name = name;
Age = age;
ID = id;
Pay = pay;
}
// Methods.
public void GiveBonus(float amount) => Pay += amount;

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

// Properties as before…

}

Read-Only Properties

只读属性
When encapsulating data, you might want to configure a read-only property. To do so, simply omit the set block. For example, assume you have a new property named SocialSecurityNumber, which encapsulates a private string variable named empSSN. If you want to make this a read-only property, you could write this:
封装数据时,可能需要配置只读属性。为此,只需省略设置块即可。例如,假设您有一个名为 SocialSecurityNumber 的新属性,该属性封装了一个名为 empSSN 的私有字符串变量。如果要将其设置为只读属性,可以编写以下内容:

public string SocialSecurityNumber
{
get { return _empSSN; }
}

Properties that only have a getter can also be simplified using expression body members. The following line is equivalent to the previous code block:
只有 getter 的属性也可以使用表达式主体成员进行简化。以下行等效于前面的代码块:

public string SocialSecurityNumber => _empSSN;

Now assume your class constructor has a new parameter to let the caller set the SSN of the object. Since the SocialSecurityNumber property is read-only, you cannot set the value as so:
现在假设您的类构造函数有一个新参数,让调用方设置对象的 SSN。由于 SocialSecurityNumber 属性是只读的,因此不能按以下方式设置该值:

public Employee(string name, int age, int id, float pay, string ssn)
{
Name = name;
Age = age;

ID = id;
Pay = pay;

// OOPS! This is no longer possible if the property is read only. SocialSecurityNumber = ssn;
}

Unless you are willing to redesign the property as read-write (which you will do soon), your only choice with read-only properties would be to use the underlying empSSN member variable within your constructor logic as so:
除非您愿意将属性重新设计为读写(您很快就会这样做),否则使用只读属性的唯一选择是在构造函数逻辑中使用底层 empSSN 成员变量,如下所示:

public Employee(string name, int age, int id, float pay, string ssn)
{

// Check incoming ssn parameter as required and then set the value. empSSN = ssn;
}

Write-Only Properties

只写属性

If you want to configure your property as a write-only property, omit the get block, like this:
如果要将属性配置为只写属性,请省略 get 块,如下所示:

public int Id
{
set { _empId = value; }
}

Mixing Private and Public Get/Set Methods on Properties

在属性上混合使用私有和公共获取/设置方法

When defining properties, the access level for the get and set methods can be different. Revisiting the Social Security number, if the goal is to prevent the modification of the number from outside the class, then declare the get method as public but the set method as private, like this:
定义属性时,get 和 set 方法的访问级别可以不同。重新访问社会保险号,如果目标是防止从类外部修改号码,则将 get 方法声明为公共,但将 set 方法声明为私有,如下所示:

public string SocialSecurityNumber
{
get => _empSSN;
private set => _empSSN = value;
}

Note that this changes the property from read-only to read-write. The difference is that the write is hidden from anything outside the defining class.
请注意,这会将属性从只读更改为读写。不同之处在于,写入对定义类之外的任何内容都是隐藏的。

Revisiting the static Keyword: Defining Static Properties

重新访问静态关键字:定义静态属性

Earlier in this chapter, you examined the role of the static keyword. Now that you understand the use of C# property syntax, you can formalize static properties. In the StaticDataAndMembers project created earlier in this chapter, your SavingsAccount class had two public static methods to get and set the interest rate.
在本章前面,您研究了静态关键字的作用。了解 C# 属性语法的用法后,可以形式化静态属性。在本章前面创建的 StaticDataAndMembers 项目中,Savings Account 类有两个公共静态方法来获取和设置利率。

However, it would be more standard to wrap this data point in a static property. Here is an example (note the use of the static keyword):
但是,将此数据点包装在静态属性中会更标准。下面是一个示例(请注意静态关键字的使用):

// A simple savings account class. class SavingsAccount
{
// Instance-level data. public double currBalance;

// A static point of data.
private static double _currInterestRate = 0.04;

// A static property.
public static double InterestRate
{

}

}

get { return _currInterestRate; } set { _currInterestRate = value; }

If you want to use this property in place of the previous static methods, you could update your code as so:
如果要使用此属性代替以前的静态方法,可以按如下所示更新代码:

// Print the current interest rate via property. Console.WriteLine("Interest Rate is: {0}", SavingsAccount.InterestRate);

Pattern Matching with Property Patterns (New 8.0)

模式与属性模式匹配(新 8.0)

The property pattern matches an expression when an expression result is non-null and every nested pattern matches the corresponding property or field of the expression result. In other words, the property pattern enables you to match on properties of an object. To set up the example, add a new file (EmployeePayTypeEnum.cs) to the EmployeeApp project for an enumeration of employee pay types, as follows:
当表达式结果为非 null 并且每个嵌套模式都与表达式结果的相应属性或字段匹配时,属性模式与表达式匹配。换句话说,属性模式使您能够匹配对象的属性。若要设置该示例,请将一个新文件 (EmployeePayTypeEnum.cs) 添加到 EmployeeApp 项目中,以枚举员工付薪类型,如下所示:

namespace EmployeeApp;
public enum EmployeePayTypeEnum
{
Hourly, Salaried, Commission
}

Update the Employee class with a property for the pay type and initialize it from the constructor. The relevant code changes are listed here:
使用支付类型的属性更新 Employee 类,并从构造函数初始化它。下面列出了相关的代码更改:

private EmployeePayTypeEnum _payType; public EmployeePayTypeEnum PayType
{
get => _payType;
set => _payType = value;
}
public Employee(string name, int id, float pay, string empSsn)
: this(name,0,id,pay, empSsn, EmployeePayTypeEnum.Salaried)

{
}
public Employee(string name, int age, int id,
float pay, string empSsn, EmployeePayTypeEnum payType)
{
Name = name;
Id = id;
Age = age;
Pay = pay;
SocialSecurityNumber = empSsn;
PayType = payType;
}

Now that all of the pieces are in place, the GiveBonus() method can be updated based on the pay type
of the employee. Commissioned employees get 10 percent of the bonus, hourly get the equivalent of 40 hours of the prorated bonus, and salaried get the entered amount. The updated GiveBonus() method is listed here:
现在所有部分都已就绪,可以根据支付类型更新 GiveBonus() 方法。的员工。委托员工获得奖金的 10%,每小时获得相当于 40 小时按比例分配的奖金,受薪员工获得输入的金额。更新后的 GiveBonus() 方法如下所示:

public void GiveBonus(float amount)
{
Pay = this switch
{
{PayType: EmployeePayTypeEnum.Commission }
=> Pay += .10F amount,
{PayType: EmployeePayTypeEnum.Hourly }
=> Pay += 40F
amount/2080F,
{PayType: EmployeePayTypeEnum.Salaried }
=> Pay += amount,
_ => Pay+=0
};
}
As with other switch statements that use pattern matching, either there must be a catchall case statement or the switch statement must throw an exception if none of the case statements is met.
与其他使用模式匹配的 switch 语句一样,要么必须有一个包罗万象的情况语句或 switch 语句必须引发异常,如果不满足任何 case 语句。

To test this, add the following code to the top-level statements:
若要对此进行测试,请将以下代码添加到顶级语句中:

Employee emp = new Employee("Marvin",45,123,1000,"111-11-1111",EmployeePayTypeEnum. Salaried);
Console.WriteLine(emp.Pay); emp.GiveBonus(100); Console.WriteLine(emp.Pay);

More than one property can be used in the pattern. Suppose you wanted to make sure each of the employees getting a bonus was older than the age of 18. You can update the method to the following:
模式中可以使用多个属性。假设您想确保每位获得奖金的员工都超过 18 岁。可以将该方法更新为以下内容:

public void GiveBonus(float amount)
{
Pay = this switch
{
{Age: >= 18, PayType: EmployeePayTypeEnum.Commission }
=> Pay += .10F * amount,
{ Age: >= 18, PayType: EmployeePayTypeEnum.Hourly }

=> Pay += 40F * amount/2080F,
{ Age: >= 18, PayType: EmployeePayTypeEnum.Salaried }
=> Pay += amount,
_ => Pay+=0
};
}
Property patterns can be nested to navigate down the property chain. To demonstrate this, add a public property for the HireDate, like this:
可以嵌套属性模式以沿属性链向下导航。若要演示这一点,请添加公共ireDate 的属性,如下所示:

private DateTime _hireDate; public DateTime HireDate
{
get => _hireDate;
set => _hireDate = value;
}
Next, update the switch statement to check to make sure each employee’s hire year was after 2020 to qualify for the bonus:
接下来,更新 switch 语句以检查以确保每个员工的雇用年份在 2020 年之后到有资格获得奖金:

public void GiveBonus(float amount)
{
Pay = this switch
{
{Age: >= 18, PayType: EmployeePayTypeEnum.Commission , HireDate: { Year: > 2020 }}
=> Pay += .10F amount,
{ Age: >= 18, PayType: EmployeePayTypeEnum.Hourly , HireDate: { Year: > 2020 } }
=> Pay += 40F
amount/2080F,
{ Age: >= 18, PayType: EmployeePayTypeEnum.Salaried , HireDate: { Year: > 2020 } }
=> Pay += amount,
_ => Pay+=0
};
}

Extended Property Patterns (New 10.0)

扩展属性模式(新 10.0)

New in C# 10, extended property patterns can be used instead of nesting downstream properties. This update cleans up the previous example, as shown here:
作为 C# 10 中的新增功能,可以使用扩展属性模式来代替嵌套下游属性。此更新清理了前面的示例,如下所示:

public void GiveBonus(float amount)
{
Pay = this switch
{
{ Age: >= 18, PayType: EmployeePayTypeEnum.Commission, HireDate.Year: > 2020 }
=> Pay += .10F amount,
{ Age: >= 18, PayType: EmployeePayTypeEnum.Hourly, HireDate.Year: > 2020 }
=> Pay += 40F
amount / 2080F,
{ Age: >= 18, PayType: EmployeePayTypeEnum.Salaried, HireDate.Year: > 2020 }
=> Pay += amount,
_ => Pay += 0
};
}

Understanding Automatic Properties

了解自动属性

When you are building properties to encapsulate your data, it is common to find that the set scopes have code to enforce business rules of your program. However, in some cases, you may not need any implementation logic beyond simply getting and setting the value. This means you can end up with a lot of code looking like the following:
在生成属性以封装数据时,通常会发现设置的范围具有强制实施程序业务规则的代码。但是,在某些情况下,您可能不需要任何实现逻辑不仅仅是获取和设置值。这意味着您最终可能会得到如下所示的大量代码:

// An Employee Car type using standard property
// syntax. class Car
{
private string carName = ""; public string PetName
{
get { return carName; } set { carName = value; }
}
}

In these cases, it can become rather verbose to define private backing fields and simple property definitions multiple times. By way of an example, if you are modeling a class that requires nine private points of field data, you end up authoring nine related properties that are little more than thin wrappers for encapsulation services.
在这些情况下,多次定义私有支持字段和简单属性定义可能会变得相当冗长。例如,如果您正在对一个需要九个私有字段数据的类进行建模,则最终将创作九个相关属性,这些属性只不过是封装服务的精简包装器。

To streamline the process of providing simple encapsulation of field data, you may use automatic property syntax. As the name implies, this feature will offload the work of defining a private backing field and the related C# property member to the compiler using a new bit of syntax. To illustrate, create a new Console Application project named AutoProps and add a new class file named Car.cs. Now, consider this reworking of the Car class, which uses this syntax to quickly create three properties:
为了简化提供字段数据简单封装的过程,可以使用自动属性语法。顾名思义,此功能将使用新语法将定义私有支持字段和相关 C# 属性成员的工作卸载到编译器。为了说明这一点,请创建一个名为 AutoProps 的新控制台应用程序项目,并添加一个名为 Car.cs 的新类文件。现在,考虑对 Car 类的重新设计,它使用此语法快速创建三个属性:

namespace AutoProps; class Car
{
// Automatic properties! No need to define backing fields. public string PetName { get; set; }
public int Speed { get; set; } public string Color { get; set; }
}

■ Note Visual studio and Visual studio Code provide the prop code snippet. if you type prop inside a class definition and press the Tab key twice, the ide will generate starter code for a new automatic property. You can then use the Tab key to cycle through each part of the definition to fill in the details. give it a try!
注意 Visual Studio 和 Visual Studio Code 提供了 prop 代码片段。如果在类定义中键入 prop 并按两次 Tab 键,IDE 将为新的自动属性生成起始代码。然后,可以使用 Tab 键循环浏览定义的每个部分以填写详细信息。试一试!

When defining automatic properties, you simply specify the access modifier, underlying data type, property name, and empty get/set scopes. At compile time, your type will be provided with an autogenerated private backing field and a fitting implementation of the get/set logic.
定义自动属性时,只需指定访问修饰符、基础数据类型、属性名称和空的 get/set 范围。在编译时,将为您的类型提供一个自动生成的私有支持字段和一个合适的 get/set 逻辑实现。

■ Note The name of the autogenerated private backing field is not visible within your C# code base. The only way to see it is to make use of a tool such as ildasm.exe.
注意 自动生成的私有支持字段的名称在 C# 代码库中不可见。看到它的唯一方法是使用诸如ildasm.exe之类的工具。

Since C# version 6, it is possible to define a “read-only automatic property” by omitting the set scope. Read-only auto properties can be set only in the constructor. However, it is not possible to define a write- only property. To solidify, consider the following:
从 C# 版本 6 开始,可以通过省略设置的范围来定义“只读自动属性”。只读自动属性只能在构造函数中设置。但是,无法定义只写属性。要巩固,请考虑以下事项:

// Read-only property? This is OK! public int MyReadOnlyProp { get; }

// Write only property? Error! public int MyWriteOnlyProp { set; }

Interacting with Automatic Properties

与自动属互

Because the compiler will define the private backing field at compile time (and given that these fields are not directly accessible in C# code), the class-defining automatic properties will always need to use property syntax to get and set the underlying value. This is important to note because many programmers make direct use of the private fields within a class definition, which is not possible in this case. For example, if the Car class were to provide a DisplayStats() method, it would need to implement this method using the property name.
由于编译器将在编译时定义私有支持字段(并且鉴于这些字段在 C# 代码中无法直接访问),因此类定义自动属性将始终需要使用属性语法来获取和设置基础值。这一点很重要,因为许多程序员直接使用类定义中的私有字段,这在这种情况下是不可能的。例如,如果 Car 类要提供 DisplayStats() 方法,则需要使用属性名称实现此方法。

class Car
{
// Automatic properties!
public string PetName { get; set; } public int Speed { get; set; } public string Color { get; set; }

public void DisplayStats()
{
Console.WriteLine("Car Name: {0}", PetName); Console.WriteLine("Speed: {0}", Speed);
Console.WriteLine("Color: {0}", Color);
}
}

When you are using an object defined with automatic properties, you will be able to assign and obtain the values using the expected property syntax.
使用通过自动属性定义的对象时,您将能够使用预期的属性语法分配和获取值。

using AutoProps;
Console.WriteLine(" Fun with Automatic Properties \n"); Car c = new Car();
c.PetName = "Frank"; c.Speed = 55; c.Color = "Red";

Console.WriteLine("Your car is named {0}? That’s odd…", c.PetName);
c.DisplayStats();

Console.ReadLine();

Automatic Properties and Default Values

自动属性和默认值

When you use automatic properties to encapsulate numerical or Boolean data, you are able to use the autogenerated type properties straightaway within your code base, as the hidden backing fields will be assigned a safe default value (false for Booleans and 0 for numerical data). However, be aware that if you use automatic property syntax to wrap another class variable, the hidden private reference type will also be set to a default value of null (which can prove problematic if you are not careful).
使用自动属性封装数值或布尔数据时,可以直接在代码库中使用自动生成的类型属性,因为隐藏的支持字段将被分配一个安全的默认值(对于布尔值为 false,对于数值数据为 0)。但是,请注意,如果使用自动属性语法包装另一个类变量,则隐藏的私有引用类型也将设置为默认值 null(如果不小心,这可能会有问题)。

Let’s insert into your current project a new class file named Garage.cs, which makes use of two automatic properties (of course, a real garage class might maintain a collection of Car objects; however,
ignore that detail here).
让我们在当前项目中插入一个名为 Garage.cs 的新类文件,该文件使用两个自动属性(当然,真正的 garage 类可能会维护 Car 对象的集合;但是,忽略此处的细节)。

namespace AutoProps; class Garage
{
// The hidden int backing field is set to zero! public int NumberOfCars { get; set; }

// The hidden Car backing field is set to null! public Car MyAuto { get; set; }
}

Given C#’s default values for field data, you would be able to print out the value of NumberOfCars as is (as it is automatically assigned the value of zero), but if you directly invoke MyAuto, you will receive a “null reference exception” at runtime, as the Car member variable used in the background has not been assigned to a new object.
给定 C# 字段数据的默认值,您将能够按原样打印出 NumberOfCars 的值(因为它会自动分配值零),但如果直接调用 MyAuto,您将在运行时收到“空引用异常”,因为后台使用的 Car 成员变量尚未分配给新对象。

Garage g = new Garage();

// OK, prints default value of zero. Console.WriteLine("Number of Cars: {0}", g.NumberOfCars);

// Runtime error! Backing field is currently null! Console.WriteLine(g.MyAuto.PetName); Console.ReadLine();

To solve this problem, you could update the class constructors to ensure the object comes to life in a safe manner. Here is an example:
若要解决此问题,可以更新类构造函数,以确保对象以安全的方式生成。下面是一个示例:

class Garage
{
// The hidden backing field is set to zero! public int NumberOfCars { get; set; }
// The hidden backing field is set to null! public Car MyAuto { get; set; }
// Must use constructors to override default
// values assigned to hidden backing fields. public Garage()
{
MyAuto = new Car();
NumberOfCars = 1;
}

public Garage(Car car, int number)
{
MyAuto = car;
NumberOfCars = number;
}
}

With this modification, you can now place a Car object into the Garage object as so:
通过此修改,您现在可以将 Car 对象放入车库对象中,如下所示:

using AutoProps;

Console.WriteLine(" Fun with Automatic Properties \n");

// Make a car.
Car c = new Car(); c.PetName = "Frank"; c.Speed = 55; c.Color = "Red"; c.DisplayStats();

// Put car in the garage. Garage g = new Garage(); g.MyAuto = c;
Console.WriteLine("Number of Cars in garage: {0}", g.NumberOfCars); Console.WriteLine("Your car is named: {0}", g.MyAuto.PetName);

Console.ReadLine();

Initializing Automatic Properties

初始化自动属性

While the previous approach works, since the release of C# 6, you are provided with a language feature that can simplify how an automatic property receives its initial value assignment. Recall from the onset of this chapter, a data field of a class can be directly assigned an initial value upon declaration. Here is an example:
虽然前面的方法有效,但自 C# 6 发布以来,为您提供了一种语言功能,可以简化自动属性接收其初始值赋值的方式。回想一下,从本章一开始,类的数据字段可以在声明时直接赋值。下面是一个示例:

class Car
{
private int numberOfDoors = 2;
}

In a similar manner, C# now allows you to assign an initial value to the underlying backing field generated by the compiler. This alleviates you from the hassle of adding code statements in class constructors to ensure property data comes to life as intended.

以类似的方式,C# 现在允许您将初始值分配给编译器生成的基础支持字段。这减轻了在类构造函数中添加代码语句以确保属性数据按预期进行生活的麻烦。

Here is an updated version of the Garage class that is initializing automatic properties to fitting values.
下面是 Garage 类的更新版本,该类将自动属性初始化为适合值。

Note you no longer need to add logic to your default class constructor to make safe assignments. In this iteration, you are directly assigning a new Car object to the MyAuto property.
请注意,您不再需要向默认类构造函数添加逻辑来进行安全赋值。在此迭代中,您将直接将新的 Car 对象分配给 MyAuto 属性。

class Garage
{
// The hidden backing field is set to 1. public int NumberOfCars { get; set; } = 1;

// The hidden backing field is set to a new Car object. public Car MyAuto { get; set; } = new Car();

public Garage(){}
public Garage(Car car, int number)
{
MyAuto = car;
NumberOfCars = number;
}
}

As you may agree, automatic properties are a nice feature of the C# programming language, as you can define a number of properties for a class using a streamlined syntax. Be aware of course that if you are building a property that requires additional code beyond getting and setting the underlying private field (such as data validation logic, writing to an event log, communicating with a database, etc.), you will be required to define a “normal” .NET Core property type by hand. C# automatic properties never do more than provide simple encapsulation for an underlying piece of (compiler-generated) private data.
您可能同意,自动属性是 C# 编程语言的一个很好的功能,因为您可以使用简化的语法为类定义许多属性。当然请注意,如果您正在构建的属性除了获取和设置基础私有字段(例如数据验证逻辑、写入事件日志、与数据库通信等)之外还需要其他代码,您将需要手动定义“普通”.NET Core 属性类型。C# 自动属性所做的只是为基础部分(编译器生成的)私有数据提供简单的封装。

Understanding Object Initialization

了解对象初始化
As shown throughout this chapter, a constructor allows you to specify startup values when creating a new object. On a related note, properties allow you to get and set underlying data in a safe manner. When you are working with other people’s classes, including the classes found within the .NET Core base class library, it is not too uncommon to discover that there is not a single constructor that allows you to set every piece of underlying state data. Given this point, a programmer is typically forced to pick the best constructor possible, after which the programmer makes assignments using a handful of provided properties.
如本章所示,构造函数允许您在创建新对象时指定启动值。在相关说明中,属性允许您以安全的方式获取和设置基础数据。当您使用其他人的类(包括在 .NET Core 基类库中找到的类)时,发现没有一个构造函数允许您设置每个部分的情况并不少见的基础状态数据。鉴于这一点,程序员通常被迫选择最好的构造函数,之后程序员使用少数提供的属性进行赋值。

Looking at the Object Initialization Syntax

查看对象初始化语法

To help streamline the process of getting an object up and running, C# offers object initializer syntax. Using this technique, it is possible to create a new object variable and assign a slew of properties and/or public fields in a few lines of code. Syntactically, an object initializer consists of a comma-delimited list of specified values, enclosed by the { and } tokens. Each member in the initialization list maps to the name of a public field or public property of the object being initialized.
为了帮助简化启动和运行对象的过程,C# 提供了对象初始值设定项语法。使用这种技术,可以创建一个新的对象变量,并在几行代码中分配大量属性和/或公共字段。从语法上讲,对象初始值设定项由逗号分隔的指定值列表组成,由 { 和 } 标记括起来。初始化列表中的每个成员都映射到要初始化的对象的公共字段或公共属性的名称。
To see this syntax in action, create a new Console Application project named ObjectInitializers. Now, consider a simple class named Point, created using automatic properties (which is not mandatory for object initialization syntax but helps you write some concise code).
若要查看此语法的实际效果,请创建一个名为 ObjectInitializers 的新控制台应用程序项目。现在,考虑一个名为 Point 的简单类,它使用自动属性创建(对于对象初始化语法不是必需的,但可以帮助您编写一些简洁的代码)。

class Point
{
public int X { get; set; } public int Y { get; set; }

public Point(int xVal, int yVal)
{
X = xVal;
Y = yVal;
}
public Point() { }
public void DisplayStats()

{
Console.WriteLine("[{0}, {1}]", X, Y);
}
}

Now consider how you can make Point objects using any of the following approaches:
现在考虑如何使用以下任一方法创建 Point 对象:

using ObjectInitializers;

Console.WriteLine(" Fun with Object Init Syntax \n");

// Make a Point by setting each property manually. Point firstPoint = new Point();
firstPoint.X = 10;
firstPoint.Y = 10; firstPoint.DisplayStats();

// Or make a Point via a custom constructor. Point anotherPoint = new Point(20, 20); anotherPoint.DisplayStats();

// Or make a Point using object init syntax. Point finalPoint = new Point { X = 30, Y = 30 }; finalPoint.DisplayStats();
Console.ReadLine();

The final Point variable is not making use of a custom constructor (as one might do traditionally) but is rather setting values to the public X and Y properties. Behind the scenes, the type’s default constructor is invoked, followed by setting the values to the specified properties. To this end, object initialization syntax is just shorthand notation for the syntax used to create a class variable using a default constructor and to set the state data property by property.
最后一个 Point 变量不使用自定义构造函数(传统上可能会这样做),而是将值设置为公共 X 和 Y 属性。在后台,调用类型的默认构造函数,然后将值设置为指定的属性。为此,对象初始化语法只是用于使用默认构造函数创建类变量和逐个属性设置状态数据属性的语法的简写表示法。

■ Note it’s important to remember that the object initialization process is using the property setter implicitly. if the property setter is marked private, this syntax cannot be used.
请注意,请务必记住,对象初始化过程隐式使用属性 setter。如果属性资源库标记为私有,则不能使用此语法。

Using init-Only Setters (New 9.0)

使用仅初始化设置器(新版 9.0)

A new feature added in C# 9.0 is init-only setters. These setters enable a property to have its value set during initialization, but after construction is complete on the object, the property becomes read-only. These types of properties are call immutable. Add a new class file named ReadOnlyPointAfterCreation.cs to your project, and add the following code:
C# 9.0 中添加的一项新功能是仅初始化资源库。这些资源库使属性能够在初始化期间设置其值,但在对象上完成构造后,该属性将变为只读。这些类型的属性称为不可变。 将一个名为 ReadOnlyPointAfterCreation.cs 的新类文件添加到项目中,并添加以下代码:

namespace ObjectInitializers; class PointReadOnlyAfterCreation
{
public int X { get; init; } public int Y { get; init; }

public void DisplayStats()
{
Console.WriteLine("InitOnlySetter: [{0}, {1}]", X, Y);
}
public PointReadOnlyAfterCreation(int xVal, int yVal)
{
X = xVal;
Y = yVal;
}
public PointReadOnlyAfterCreation() { }
}

Use the following code to take this new class for a test-drive:
使用以下代码将此新类用于体验版:

//Make readonly point after construction
PointReadOnlyAfterCreation firstReadonlyPoint = new PointReadOnlyAfterCreation(20, 20); firstReadonlyPoint.DisplayStats();

// Or make a Point using object init syntax.
PointReadOnlyAfterCreation secondReadonlyPoint = new PointReadOnlyAfterCreation { X = 30, Y = 30 };
secondReadonlyPoint.DisplayStats();

Notice nothing has changed from the code that you wrote for the Point class, except of course the class name. The difference is that the values for X or Y cannot be modified once the class is created. For example, the following code will not compile:
请注意,您为 Point 类编写的代码没有任何变化,当然类名除外。不同之处在于,创建类后,无法修改 X 或 Y 的值。例如,以下代码将无法编译:

//The next two lines will not compile secondReadonlyPoint.X = 10;
secondReadonlyPoint.Y = 10;

Calling Custom Constructors with Initialization Syntax

使用初始化语法调用自定义构造函数

The previous examples initialized Point types by implicitly calling the default constructor on the type.
前面的示例通过隐式调用类型的默认构造函数来初始化 Point 类型。

// Here, the default constructor is called implicitly. Point finalPoint = new Point { X = 30, Y = 30 };

If you want to be clear about this, it is permissible to explicitly call the default constructor as follows:
如果要明确这一点,可以显式调用默认构造函数,如下所示:

// Here, the default constructor is called explicitly. Point finalPoint = new Point() { X = 30, Y = 30 };

Be aware that when you are constructing a type using initialization syntax, you are able to invoke any constructor defined by the class. Your Point type currently defines a two-argument constructor to set the (x, y) position. Therefore, the following Point declaration results in an X value of 100 and a Y value of 100, regardless of the fact that the constructor arguments specified the values 10 and 16:
请注意,使用初始化语法构造类型时,可以调用该类定义的任何构造函数。Point 类型当前定义了一个双参数构造函数来设置 (x, y) 位置。因此,以下 Point 声明将导致 X 值为 100,Y 值为 100,而不考虑构造函数参数指定值 10 和 16 的事实:

// Calling a custom constructor.
Point pt = new Point(10, 16) { X = 100, Y = 100 };

Given the current definition of your Point type, calling the custom constructor while using initialization syntax is not terribly useful (and more than a bit verbose). However, if your Point type provides a new constructor that allows the caller to establish a color (via a custom enum named PointColor), the combination of custom constructors and object initialization syntax becomes clear.
鉴于 Point 类型的当前定义,在使用初始化语法时调用自定义构造函数并不是非常有用(而且有点冗长)。但是,如果您的点类型提供允许调用方建立颜色的新构造函数(通过名为 PointColor 的自定义枚举),自定义构造函数和对象初始化语法的组合变得清晰。

Add a new class named PointColorEnum.cs to your project, and add the following code to create an enum for the color:
将一个名为 PointColorEnum.cs 的新类添加到项目中,并添加以下代码以创建颜色的枚举:

namespace ObjectInitializers; enum PointColorEnum
{
LightBlue, BloodRed, Gold
}

Now, update the Point class as follows:
现在,更新 Point 类,如下所示:

class Point
{
public int X { get; set; } public int Y { get; set; }
public PointColorEnum Color{ get; set; }

public Point(int xVal, int yVal)
{
X = xVal;
Y = yVal;
Color = PointColorEnum.Gold;
}

public Point(PointColorEnum ptColor)
{
Color = ptColor;
}

public Point() : this(PointColorEnum.BloodRed){ }

public void DisplayStats()
{
Console.WriteLine("[{0}, {1}]", X, Y); Console.WriteLine("Point is {0}", Color);
}
}

With this new constructor, you can now create a gold point (positioned at 90, 20) as follows:
使用此新构造函数,您现在可以创建一个黄金点(位于 90, 20),如下所示:

// Calling a more interesting custom constructor with init syntax. Point goldPoint = new Point(PointColorEnum.Gold){ X = 90, Y = 20 }; goldPoint.DisplayStats();

Initializing Data with Initialization Syntax

使用初始化语法初始化数据

As briefly mentioned earlier in this chapter (and fully examined in Chapter 6), the “has-a” relationship allows you to compose new classes by defining member variables of existing classes. For example, assume you now have a Rectangle class, which makes use of the Point type to represent its upper-left/bottom-right coordinates. Since automatic properties set all fields of class variables to null, you will implement this new class using “traditional” property syntax.
如本章前面简要提到的(并在第6章中进行了全面研究),“has-a”关系允许您通过定义现有类的成员变量来组合新类。例如,假设您现在有一个 Rectangle 类,该类使用 Point 类型来表示其左上角/右下角坐标。由于自动属性将类变量的所有字段设置为 null,因此您将使用“传统”属性语法实现此新类。

namespace ObjectInitializers; class Rectangle
{
private Point topLeft = new Point(); private Point bottomRight = new Point();

public Point TopLeft
{
get { return topLeft; } set { topLeft = value; }
}
public Point BottomRight
{
get { return bottomRight; } set { bottomRight = value; }
}

public void DisplayStats()
{
Console.WriteLine("[TopLeft: {0}, {1}, {2} BottomRight: {3}, {4}, {5}]", topLeft.X, topLeft.Y, topLeft.Color,
bottomRight.X, bottomRight.Y, bottomRight.Color);
}
}

Using object initialization syntax, you could create a new Rectangle variable and set the inner Points as follows:
使用对象初始化语法,您可以创建一个新的 Rectangle 变量并设置内部点s,如下所示:

// Create and initialize a Rectangle. Rectangle myRect = new Rectangle
{
TopLeft = new Point { X = 10, Y = 10 }, BottomRight = new Point { X = 200, Y = 200}
};

Again, the benefit of object initialization syntax is that it basically decreases the number of keystrokes (assuming there is not a suitable constructor). Here is the traditional approach to establishing a similar Rectangle:
同样,对象初始化语法的好处是它基本上减少了击键次数(假设没有合适的构造函数)。以下是建立类似矩形的传统方法:

// Old-school approach. Rectangle r = new Rectangle(); Point p1 = new Point();
p1.X = 10;
p1.Y = 10;

r.TopLeft = p1;
Point p2 = new Point(); p2.X = 200;
p2.Y = 200;
r.BottomRight = p2;

While you might feel object initialization syntax can take a bit of getting used to, once you get comfortable with the code, you will be quite pleased at how quickly you can establish the state of a new object with minimal fuss and bother.
虽然您可能觉得对象初始化语法可能需要一些时间来适应,但一旦您熟悉了代码,您就会对以最少的大惊小怪和麻烦快速建立新对象的状态感到非常满意。

Working with Constant and Read-Only Field Data

使用常量和只读字段数据

Sometimes you need a property that you do not want changed at all, also known as immutable, either from the time it was compiled or after it was set during construction. We have already explored one example with init-only setters. Now we will examine constants and read-only fields.
有时,您需要一个根本不想更改的属性,也称为不可变属性,无论是从编译时还是在构造期间设置之后。我们已经用仅初始化的 setter 探索了一个示例。现在我们将检查常量和只读字段。

Understanding Constant Field Data

了解常量场数据

C# offers the const keyword to define constant data, which can never change after the initial assignment. As you might guess, this can be helpful when you are defining a set of known values for use in your applications that are logically connected to a given class or structure.
C# 提供了 const 关键字来定义常量数据,该常量数据在初始赋值后永远不会更改。正如您可能猜到的那样,当您定义一组已知值以在逻辑上连接到给定类或结构的应用程序中使用时,这会很有帮助。

Assume you are building a utility class named MyMathClass that needs to define a value for pi (which you will assume to be 3.14 for simplicity). Begin by creating a new Console Application project named ConstData and add a file named MyMathClass.cs. Given that you would not want to allow other developers to change this value in code, pi could be modeled with the following constant:
假设您正在构建一个名为 MyMathClass 的实用程序类,该实用程序类需要为 pi 定义一个值(为简单起见,您将假设该值为 3.14)。首先创建一个名为 ConstData 的新控制台应用程序项目,并添加一个名为 MyMathClass.cs 的文件。鉴于您不希望允许其他开发人员在代码中更改此值,则可以使用以下常量对 pi 进行建模:

//MyMathClass.cs namespace ConstData; class MyMathClass
{
public const double PI = 3.14;
}

Update the code in the Program.cs file to match this:
更新程序.cs文件中的代码以匹配以下内容:

using ConstData;
Console.WriteLine(" Fun with Const \n"); Console.WriteLine("The value of PI is: {0}", MyMathClass.PI);
// Error! Can’t change a constant!
// MyMathClass.PI = 3.1444; Console.ReadLine();

Notice that you are referencing the constant data defined by MyMathClass using a class name prefix (i.e.,
MyMathClass.PI). This is because constant fields of a class are implicitly static. However, it is permissible to define and access a local constant variable within the scope of a method or property. Here is an example:
请注意,您正在使用类名前缀(即MyMathClass.PI)。 这是因为类的常量字段是隐式静态的。但是,允许在方法或属性范围内定义和访问局部常量变量。下面是一个示例:

static void LocalConstStringVariable()
{
// A local constant data point can be directly accessed.

const string fixedStr = "Fixed string Data"; Console.WriteLine(fixedStr);

// Error!
// fixedStr = "This will not work!";
}

Regardless of where you define a constant piece of data, the one point to always remember is that the initial value assigned to the constant must be specified at the time you define the constant. Assigning the value of pi in a class constructor, as shown in the following code, produces a compilation error:
无论在何处定义常量数据,始终要记住的一点是,必须在定义常量时指定分配给常量的初始值。在类构造函数中分配 pi 的值(如以下代码所示)会产生编译错误:

class MyMathClass
{
// Try to set PI in constructor? public const double PI;

public MyMathClass()
{
// Not possible- must assign at time of declaration. PI = 3.14;
}
}

The reason for this restriction has to do with the fact that the value of constant data must be known at compile time. Constructors (or any other method), as you know, are invoked at runtime.
此限制的原因与常量数据的值必须在编译时已知这一事实有关。如您所知,构造函数(或任何其他方法)是在运行时调用的。

Constant Interpolated Strings (New 10.0)

常量内插字符串(新 10.0)

Introduced in C# 10, const string values can use string interpolation in their assignment statements as long as all of the components that are used are also const strings. As a trivial example, add the following code to the top-level statements:
在 C# 10 中引入的 const 字符串值可以在其赋值语句中使用字符串内插,只要使用的所有组件也是 const 字符串。作为一个简单的示例,将以下代码添加到顶级语句中:

Console.WriteLine("=> Constant String Interpolation:"); const string foo = "Foo";
const string bar = "Bar";
const string foobar = $"{foo}{bar}"; Console.WriteLine(foobar);

Understanding Read-Only Fields

了解只读字段
Closely related to constant data is the notion of read-only field data (which should not be confused with a read-only property). Like a constant, a read-only field cannot be changed after the initial assignment or you will receive a compile-time error. However, unlike a constant, the value assigned to a read-only field can be determined at runtime and, therefore, can legally be assigned within the scope of a constructor but nowhere else.
与常量数据密切相关的是只读字段数据的概念(不应将其与只读属性混淆)。与常量一样,只读字段在初始赋值后无法更改,否则您将收到编译时错误。但是,与常量不同,分配给只读字段的值可以在运行时确定,因此可以合法地在构造函数的范围内分配,但在其他地方不能。
This can be helpful when you do not know the value of a field until runtime, perhaps because you need to read an external file to obtain the value but want to ensure that the value will not change after that point. For the sake of illustration, assume the following update to MyMathClass:
当您在运行时之前不知道字段的值时,这可能会很有帮助,这可能是因为您需要读取外部文件以获取该值,但希望确保该值在该点之后不会更改。为了便于说明,假设MyMathClass进行了以下更新:

class MyMathClass
{
// Read-only fields can be assigned in constructors,
// but nowhere else. public readonly double PI;

public MyMathClass ()
{
PI = 3.14;
}
}

Again, any attempt to make assignments to a field marked readonly outside the scope of a constructor results in a compiler error.
同样,任何尝试在构造函数范围之外对标记为只读的字段进行赋值都会导致编译器错误。

class MyMathClass
{
public readonly double PI; public MyMathClass ()
{
PI = 3.14;
}

// Error!
public void ChangePI()
{ PI = 3.14444;}
}

Understanding Static Read-Only Fields

了解静态只读字段

Unlike a constant field, read-only fields are not implicitly static. Thus, if you want to expose PI from the class level, you must explicitly use the static keyword. If you know the value of a static read-only field at compile time, the initial assignment looks similar to that of a constant (however, in this case, it would be easier to simply use the const keyword in the first place, as you are assigning the data field at the time of declaration).
与常量字段不同,只读字段不是隐式静态的。因此,如果要从类级别公开 PI,则必须显式使用 static 关键字。如果您在编译时知道静态只读字段的值,则初始赋值看起来类似于常量的值(但是,在这种情况下,首先简单地使用 const 关键字会更容易,因为您在声明时分配数据字段)。

class MyMathClass
{
public static readonly double PI = 3.14;
}

//Program.cs using ConstData;

Console.WriteLine(" Fun with Const "); Console.WriteLine("The value of PI is: {0}", MyMathClass.PI); Console.ReadLine();

However, if the value of a static read-only field is not known until runtime, you must use a static constructor as described earlier in this chapter.
但是,如果静态只读字段的值在运行时之前是未知的,则必须使用本章前面所述的静态构造函数。

class MyMathClass
{
public static readonly double PI;

static MyMathClass()
{ PI = 3.14; }
}

Understanding Partial Classes

了解分部类

When working with classes, it is important to understand the role of the C# partial keyword. The partial keyword allows for a single class to be partitioned across multiple code files. When you scaffold Entity Framework Core classes from a database, the created classes are all created as partial classes. This way, any code that you have written to augment those files is not overwritten, presuming your code is in separate class files marked with the partial keyword. Another reason is that maybe your class has grown over time into something difficult to manage, and as an intermediate step toward refactoring that class, you can split it up into partials.
使用类时,了解 C# 分部关键字的作用非常重要。partial 关键字允许跨多个代码文件对单个类进行分区。从数据库搭建实体框架核心类的基架时,创建的类都将创建为分部类。这样,您为扩充这些文件而编写的任何代码都不会被覆盖,前提是您的代码位于标有 partial 关键字的单独类文件中。另一个原因是,随着时间的推移,您的类可能已经发展成为难以管理的东西,作为重构该类的中间步骤,您可以将其拆分为部分。

In C#, you can partition a single class across multiple code files to isolate the boilerplate code from more readily useful (and complex) members. To illustrate where partial classes could be useful, open the EmployeeApp project you created previously in this chapter in Visual Studio or Visual Studio Code and then open the Employee.cs file for editing. As you recall, this single file contains code of all aspects of the class.
在 C# 中,可以跨多个代码文件对单个类进行分区,以将样板代码与更有用(和复杂)的成员隔离开来。若要说明分部类在哪些方面可能有用,请打开您之前在本章中创建的 Visual Studio 或 Visual Studio Code 中的 EmployeeApp 项目,然后打开 Employee.cs 文件进行编辑。您还记得,这个文件包含类的所有方面的代码。

class Employee
{
// Field Data

// Constructors

// Methods

// Properties
}

Using partial classes, you could choose to move (for example) the properties, constructors, and field data into a new file named Employee.Core.cs (the name of the file is irrelevant). The first step is to add the partial keyword to the current class definition and cut the code to be placed into the new file.
使用分部类,可以选择将属性、构造函数和字段数据(例如)移动到名为 Employee.Core.cs 的新文件中(文件名无关紧要)。第一步是将 partial 关键字添加到当前类定义中,并剪切要放入新文件中的代码。

// Employee.cs
partial class Employee
{
// Methods

// Properties
}

Next, assuming you have inserted a new class file into your project, you can move the data fields and properties to the new file using a simple cut-and-paste operation. In addition, you must add the partial keyword to this aspect of the class definition. Here is an example:
接下来,假设您已将新的类文件插入到项目中,则可以使用简单的剪切和粘贴操作将数据字段和属性移动到新文件中。此外,还必须将 partial 关键字添加到类定义的这一方面。下面是一个示例:

// Employee.Core.cs
partial class Employee
{
// Field data

// Properties
}

■ Note remember that each of the partial classes must be marked with the partial keyword!
请注意,每个分部类都必须标有分部关键字!

After you compile the modified project, you should see no difference whatsoever. The whole idea of a partial class is realized only during design time. After the application has been compiled, there is just a single, unified class within the assembly. The only requirement when defining partial types is that the type’s name (Employee in this case) is identical and defined within the same .NET Core namespace.
编译修改后的项目后,您应该看不到任何区别。分部类的整个思想仅在设计时实现。应用程序编译完成后,只有一个程序集中的单个统一类。定义分部类型时的唯一要求是类型的名称(在本例中为 Employee)相同,并且在同一 .NET Core 命名空间中定义。
Recall from the discussion of top-level statements, any methods in top-level statements must be a local function. The top-level statements are implicitly defined in a partial Program class, allowing for the creation of another partial Program class to hold regular methods.
回想一下顶级语句的讨论,顶级语句中的任何方法都必须是局部函数。顶级语句在分部 Program 类中隐式定义,允许创建另一个分部 Program 类来保存常规方法。

Create a new console application named FunWithPartials and add a new class file named Program.
创建一个名为 FunWithPartss 的新控制台应用程序,并添加一个名为 Program 的新类文件。

Partial.cs. Update the code to the following:

public partial class Program
{
public static string SayHello() => return "Hello";
}

Now you can call that method from your top-level statements in the Program.cs file, like this:
现在,您可以从 Program.cs 文件中的顶级语句调用该方法,如下所示:

Console.WriteLine(SayHello());

Which method you use is a matter of preference.
您使用哪种方法是一个偏好问题。

Records (New 9.0)

记录(新 9.0)

New in C# 9.0, record types are a special reference type that provide synthesized methods for equality using value semantics and data encapsulation. Record types can be created with immutable or standard properties. To start experimenting with records, create a new console application named FunWithRecords. Consider the following Car class, modified from the examples earlier in the chapter:
记录类型是 C# 9.0 中的新增功能,是一种特殊的引用类型,它使用值语义和数据封装提供相等的综合方法。可以使用不可变或标准属性创建记录类型。若要开始试验记录,请创建一个名为 FunWithRecords 的新控制台应用程序。请考虑以下 Car 类,该类是从本章前面的示例修改而来的:

class Car
{
public string Make { get; set; } public string Model { get; set; } public string Color { get; set; }

public Car() {}

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

Model = model;
Color = color;
}
}

As you well know by now, once you create an instance of this class, you can change any of the properties at run time. If the properties for the previous Car class need to be immutable, you can change its property definitions to use init-only setters, like this:
众所周知,一旦创建了此类的实例,就可以在运行时更改任何属性。如果以前的 Car 类的属性需要不可变,则可以将其属性定义更改为使用仅初始化资源库,如下所示:

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

To exercise this new class, the following code creates two instances of the Car class, one through object initialization and the other through the custom constructor. Update the Program.cs file to the following:
为了练习这个新类,下面的代码创建 Car 类的两个实例,一个通过对象初始化,另一个通过自定义构造函数。将程序.cs文件更新为以下内容:

using FunWithRecords; Console.WriteLine("Fun with Records!");
//Use object initialization Car myCar = new Car
{
Make = "Honda", Model = "Pilot", Color = "Blue"
};
Console.WriteLine("My car: "); DisplayCarStats(myCar); Console.WriteLine();
//Use the custom constructor
Car anotherMyCar = new Car("Honda", "Pilot", "Blue"); Console.WriteLine("Another variable for my car: "); DisplayCarStats(anotherMyCar);
Console.WriteLine();

//Compile error if property is changed
//myCar.Color = "Red"; Console.ReadLine();
static void DisplayCarStats(Car c)
{
Console.WriteLine("Car Make: {0}", c.Make); Console.WriteLine("Car Model: {0}", c.Model); Console.WriteLine("Car Color: {0}", c.Color);
}

As expected, both methods of object creation work, properties get displayed, and trying to change a property after construction raises a compilation error.
正如预期的那样,两种对象创建方法都有效,属性都会显示,并且在构造后尝试更改属性会引发编译错误。

Immutable Record Types with Standard Property Syntax

具有标准属性语法的不可变记录类型

Creating an immutable Car record type using standard property syntax is similar to creating classes with immutable properties. To see this in action, add a new file named (CarRecord.cs) to your project and add the following code:
使用标准属性语法创建不可变的 Car 记录类型类似于创建具有不可变属性的类。若要查看此操作的实际效果,请将名为 (CarRecord.cs) 的新文件添加到项目中,并添加以下代码:

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;
}
}

■ Note record types allow using the class keyword to help distinguish them from record structs, but the keyword is optional. Therefore record class and record mean the same thing.
注意记录类型允许使用 class 关键字来帮助将它们与记录结构s 区分开来,但关键字是可选的。因此,记录类和记录的含义相同。

You can confirm that the behavior is the same as the Car class with init-only settings by running the following code in Program.cs:
您可以通过在 Program.cs 中运行以下代码来确认该行为与具有仅初始化设置的 Car 类相同:

using FunWithRecords;

Console.WriteLine("* RECORDS ***");
//Use object initialization
CarRecord myCarRecord = new CarRecord
{
Make = "Honda", Model = "Pilot", Color = "Blue"
};
Console.WriteLine("My car: "); DisplayCarRecordStats(myCarRecord); Console.WriteLine();

//Use the custom constructor
CarRecord anotherMyCarRecord = new CarRecord("Honda", "Pilot", "Blue"); Console.WriteLine("Another variable for my car: "); Console.WriteLine(anotherMyCarRecord.ToString());
Console.WriteLine();

//Compile error if property is changed
//myCarRecord.Color = "Red";

Console.ReadLine();

static void DisplayCarStats(Car c)
{
Console.WriteLine("Car Make: {0}", c.Make); Console.WriteLine("Car Model: {0}", c.Model); Console.WriteLine("Car Color: {0}", c.Color);
}

While we have not covered equality (next section) or inheritance (next chapter) with records, this first look at records does not seem like much of a benefit. The current Car example includes all of the plumbing code that we have come to expect. With one notable difference on the output: the ToString() method is fancied up for record types, as shown in this following sample output:
虽然我们还没有介绍记录的平等(下一节)或继承(下一章),但第一次看记录似乎并没有多大好处。当前的 Car 示例包括我们期望的所有管道代码。输出上有一个显着的区别:ToString() 方法适用于记录类型,如以下示例输出所示:

* RECORDS *** My car:
CarRecord { Make = Honda, Model = Pilot, Color = Blue } Another variable for my car:
CarRecord { Make = Honda, Model = Pilot, Color = Blue }

Immutable Record Types with Positional Syntax

具有位置语法的不可变记录类型

Consider this updated (and much abbreviated) definition for the Car record:
考虑一下汽车记录的更新(和缩写)定义:

record CarRecord(string Make, string Model, string Color);

Referred to as a positional record type, the constructor defines the properties on the record, and all of the other plumbing code has been removed. There are three considerations when using this syntax: the first is that you cannot use object initialization of record types using the compact definition syntax, the second is that the record must be constructed with the properties in the correct position, and the third is that the casing of the properties in the constructor is directly translated to the casing of the properties on the record type.

构造函数称为位置记录类型,它定义记录上的属性,并且已删除所有其他管道代码。使用此语法时有三个注意事项:第一个是不能使用紧凑定义语法对记录类型使用对象初始化,第二个是必须使用位于正确位置的属性构造记录,第三个是构造函数中属性的大小写直接转换为记录类型上属性的大小写。
We can confirm that the Make, Model, and Color are all init-only properties on the Car record by looking at an abbreviated listing of the IL. Notice that there are backing fields for each of the parameters passed into the constructor, and each has the private and initonly modifiers.
我们可以通过查看 IL 的缩写列表来确认 Make、Model 和 Color 都是 Car 记录上的仅初始化属性。 请注意,传递给构造函数的每个参数都有支持字段,并且每个参数都有私有和 initonly 修饰符。

.class private auto ansi beforefieldinit FunWithRecords.CarRecord extends [System.Runtime]System.Object
implements class [System.Runtime]System.IEquatable`1
{
.field private initonly string ‘k BackingField’
.field private initonly string ‘k BackingField’
.field private initonly string ‘k BackingField’

}

When using the positional syntax, record types provide a primary constructor that matches the positional parameters on the record declaration.
使用位置语法时,记录类型提供与记录声明上的位置参数匹配的主构造函数。

Deconstructing Mutable Record Types

解构可变记录类型

Record types using positional parameters also provide a Deconstruct() method with an out parameter for each positional parameter in the declaration. The following code creates a new record using the supplied constructor and then deconstructs the properties into separate variables:
使用位置参数的记录类型还提供一个 Deconstruct() 方法,该方法为声明中的每个位置参数提供一个 out 参数。下面的代码使用提供的构造函数创建新记录,然后将属性解构为单独的变量:

CarRecord myCarRecord = new CarRecord("Honda", "Pilot", "Blue"); myCarRecord.Deconstruct(out string make, out string model, out string color); Console.WriteLine($"Make: {make} Model: {model} Color: {color}");

Note that while the public properties on the record match the casing of the declaration, the out variables in the Deconstruct() method only have to match the position of the parameters. Changing the names of the variables in the Deconstruct() method still returns Make, Model, and Color, in that order:
请注意,虽然记录上的公共属性与声明的大小写匹配,但 Deconstruct() 方法中的 out 变量只需要与参数的位置匹配。在 Deconstruct() 方法中更改变量的名称仍按该顺序返回 Make、Model 和 Color:

myCarRecord.Deconstruct(out string a, out string b, out string c); Console.WriteLine($"Make: {a} Model: {b} Color: {c}");

The tuple syntax can also be used when deconstructing records. Note the following addition to the example:
解构记录时也可以使用元组语法。请注意示例中的以下补充:

var (make2, model2, color2) = myCarRecord;
Console.WriteLine($"Make: {make2} Model: {model2} Color: {color2}");

Mutable Record Types

可变记录类型
C# also supports mutable record types by using standard (not init-only) setters. The following is an example of this:
C# 还通过使用标准(不是仅初始化)资源库来支持可变记录类型。下面是一个示例:

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

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

While this syntax is supported, the record types are intended to be used for immutable data models.
虽然支持此语法,但记录类型旨在用于不可变数据模型。

Value Equality with Record Types

记录类型的值相等

In the Car class example, the two Car instances were created with the same data. One might think that these two classes are equal, as the following line of code tests:
在 Car 类示例中,两个 Car 实例是使用相同的数据创建的。有人可能会认为这两个类是相等的,因为下面的代码行测试:

Console.WriteLine($"Cars are the same? {myCar.Equals(anotherMyCar)}");

However, they are not equal. Recall that record types are a specialized type of class, and classes are reference types. For two reference types to be equal, they have to point to the same object in memory. As a further test, check to see if the two Car objects point to the same object:
但是,它们并不相等。回想一下,记录类型是类的专用类型,类是引用类型。要使两个引用类型相等,它们必须指向内存中的同一对象。作为进一步的测试,请检查两个 Car 对象是否指向同一对象:

Console.WriteLine($"Cars are the same reference? {ReferenceEquals(myCar, anotherMyCar)}");

Running the program again produces this (abbreviated) result:
再次运行程序会产生以下(缩写)结果:

Cars are the same? False CarRecords are the same? False

Record types behave differently. Record types implicitly override Equals, ==, and !=, and two record types are considered equal if the hold the same values and are the same type, just as if the instances are value types. Consider the following code and the subsequent results:
记录类型的行为不同。记录类型隐式覆盖 Equals、== 和 !=,如果两个记录类型保存相同的值并且是相同的类型,则认为这两个记录类型相等,就像实例是值类型一样。请考虑以下代码和后续结果:

Console.WriteLine($"CarRecords are the same? {myCarRecord.Equals(anotherMyCarRecord)}"); Console.WriteLine($"CarRecords are the same reference? {ReferenceEquals(myCarRecord,another MyCarRecord)}");
Console.WriteLine($"CarRecords are the same? {myCarRecord == anotherMyCarRecord}"); Console.WriteLine($"CarRecords are not the same? {myCarRecord != anotherMyCarRecord}");

/* RECORDS ***/ My car:
CarRecord { Make = Honda, Model = Pilot, Color = Blue } Another variable for my car:
CarRecord { Make = Honda, Model = Pilot, Color = Blue }

CarRecords are the same? True
CarRecords are the same reference? false CarRecords are the same? True
CarRecords are not the same? False

Notice that they are considered equal, even though the variables point to two different variables in memory.
请注意,即使变量指向内存中的两个不同变量,它们也被视为相等。

Copying Record Types Using with Expressions

复制记录类型 与表达式一起使用

With record types, assigning a record type instance to a new variable creates a pointer to the same reference, which is the same behavior as classes. The following code demonstrates this:
对于记录类型,将记录类型实例分配给新变量会创建一个指向同一引用的指针,这与类的行为相同。以下代码对此进行了演示:

CarRecord carRecordCopy = anotherMyCarRecord; Console.WriteLine("Car Record copy results");
Console.WriteLine($"CarRecords are the same? {carRecordCopy.Equals(anotherMyCarRecord)}"); Console.WriteLine($"CarRecords are the same? {ReferenceEquals(carRecordCopy, anotherMyCarRecord)}");

When executed, both tests return true, proving that they are the same in value and reference.
执行时,两个测试都返回 true,证明它们在值和引用上相同。

To create a true copy of a record with one or more properties modified (referred to as nondestructive mutation), C# 9.0 introduces with expressions. In the with construct, any properties that need to be updated are specified with their new values, and any properties not listed are shallow copied exactly. Examine the following example:
若要创建修改了一个或多个属性(称为非破坏性突变)的记录的真实副本,C# 9.0 引入了表达式。 在 with 构造中,需要更新的任何属性都使用其新值指定,并且未列出的任何属性都是完全浅拷贝的。检查以下示例:

CarRecord ourOtherCar = myCarRecord with {Model = "Odyssey"}; Console.WriteLine("My copied car:"); Console.WriteLine(ourOtherCar.ToString());

Console.WriteLine("Car Record copy using with expression results"); Console.WriteLine($"CarRecords are the same? {ourOtherCar.Equals(myCarRecord)}"); Console.WriteLine($"CarRecords are the same? {ReferenceEquals(ourOtherCar, myCarRecord)}");

The code creates a new instance of the CarRecord type, copying the Make and Color values of the
myCarRecord instance and setting Model to the string Odyssey. The results of this code is shown here:
该代码创建 CarRecord 类型的新实例,复制myCarRecord 实例并将模型设置为字符串 Odyssey。此代码的结果如下所示:

/* RECORDS ***/ My copied car:
CarRecord { Make = Honda, Model = Odyssey, Color = Blue }

Car Record copy using with expression results CarRecords are the same? False
CarRecords are the same? False

Using with expressions, you can easily take complex record types into new record type instances with updated property values.
使用 with 表达式,您可以轻松地将复杂的记录类型放入具有更新属性值的新记录类型实例中。

Record Structs (New 10.0)

记录结构(新 10.0)

New in C# 10.0, record structs are the value type equivalent of record types. Record structs can also use positional parameters or standard property syntax and provide value equality, nondestructive mutation, and built-in display formatting. To start experimenting with records, create a new console application named FunWithRecordStructs. One major difference between record structs and records is that a record struct is mutable by default. To make a record struct immutable, you must use add the readonly modifier.
作为 C# 10.0 中的新增功能,记录结构是等效于记录类型的值类型。记录结构还可以使用位置参数或标准属性语法,并提供值相等、非破坏性突变和内置显示格式。若要开始试验记录,请创建一个名为 FunWithRecordStructs 的新控制台应用程序。记录结构和记录之间的一个主要区别是,默认情况下,记录结构是可变的。若要使记录结构不可变,必须使用添加只读修饰符。

Mutable Record Structs

可变记录结构

To create a record struct, let’s revisit our friend the Point struct. The following shows how to create two different record struct types, one using the positional syntax and the other using standard properties:
要创建一个记录结构,让我们重新访问我们的朋友 Point 结构。下面演示如何创建两种不同的记录结构类型,一种使用位置语法,另一种使用标准属性:

public record struct Point(double X, double Y, double Z); public record struct PointWithPropertySyntax()
{
public double X { get; set; } = default; public double Y { get; set; } = default; public double Z { get; set; } = default;

public PointWithPropertySyntax(double x, double y, double z) : this()
{
X = x; Y = y; Z = z;
}
};

The following code demonstrates the mutability of the two record struct types as well as the improved ToString() method:
下面的代码演示了两种记录结构类型的可变性以及改进的ToString() 方法:

Console.WriteLine(" Fun With Record Structs "); var rs = new Point(2, 4, 6); Console.WriteLine(rs.ToString());
rs.X = 8;
Console.WriteLine(rs.ToString());

var rs2 = new PointWithPropertySyntax(2, 4, 6); Console.WriteLine(rs2.ToString());
rs2.X = 8;
Console.WriteLine(rs2.ToString());

Immutable Record Structs

不可变的记录结构

The previous two record struct examples can be made immutable by adding the readonly keyword:
前两个记录结构示例可以通过添加只读关键字使它们不可变:

public readonly record struct ReadOnlyPoint(double X, double Y, double Z); public readonly record struct ReadOnlyPointWithPropertySyntax()
{
public double X { get; init; } = default; public double Y { get; init; } = default; public double Z { get; init; } = default;
public ReadOnlyPointWithPropertySyntax(double x, double y, double z) : this()
{
X = x; Y = y; Z = z;
}
};

You can confirm that they are now immutable (enforced by the compiler) with the following code:
您可以使用以下代码确认它们现在是不可变的(由编译器强制执行):

var rors = new ReadOnlyPoint(2, 4, 6);
//Compiler Error:
//rors.X = 8;

var rors2 = new ReadOnlyPointWithPropertySyntax(2, 4, 6);
//Compiler Error:
//rors2.X = 8;

Value equality and copying using with expressions work the same as record types.
值相等和与表达式一起使用的复制与记录类型的工作方式相同。

Deconstructing Record Structs

解构记录结构

Like record types, record structs that use positional syntax also provide a Deconstruct() method. The behavior is the same as mutable and immutable record structs. The following code creates a new record using the supplied constructor and then deconstructs the properties into separate variables:
与记录类型一样,使用位置语法的记录结构也提供 Deconstruct() 方法。该行为与可变和不可变记录结构相同。下面的代码使用提供的构造函数创建新记录,然后将属性解构为单独的变量:

Console.WriteLine("Deconstruction: "); var (x1, y1, z1) = rs;
Console.WriteLine($"X: {x1} Y: {y1} Z: {z1}"); var (x2, y2, z2) = rors; Console.WriteLine($"X: {x2} Y: {y2} Z: {z2}");
rs.Deconstruct(out double x3,out double y3,out double z3); Console.WriteLine($"X: {x3} Y: {y3} Z: {z3}"); rors.Deconstruct(out double x4,out double y4,out double z4); Console.WriteLine($"X: {x4} Y: {y4} Z: {z4}");

Summary
总结

The point of this chapter was to introduce you to the role of the C# class type and the new C# 9.0 record type. As you have seen, classes can take any number of constructors that enable the object user to establish the state of the object upon creation. This chapter also illustrated several class design techniques (and related keywords). The this keyword can be used to obtain access to the current object. The static keyword allows you to define fields and members that are bound at the class (not object) level. The const keyword, readonly modifier, and init-only setters allow you to define a point of data that can never change after the initial assignment or object construction. Record types are a special type of class that are immutable (by default) and behave like value types when comparing a record type with another instance of the same record type. Record structs are value types that are mutable (by default) and provide the same equality and nondestructive mutation capabilities as record types.
本章的重点是向您介绍 C# 类类型和新的 C# 9.0 记录类型的作用。如您所见,类可以采用任意数量的构造函数,这些构造函数使对象用户能够在创建时建立对象的状态。本章还说明了几种类设计技术(和相关关键字)。this 关键字可用于获取对当前对象的访问权限。静态关键字允许您定义绑定在类(而非对象)级别的字段和成员。const 关键字、只读修饰符和仅初始化资源库允许您定义在初始赋值或对象构造后永远不会更改的数据点。记录类型是不可变的特殊类型的类(默认情况下)并且在将记录类型与同一记录类型的另一个实例进行比较时的行为类似于值类型。记录结构是可变的值类型(默认情况下),并提供与记录类型相同的相等和非破坏性突变功能。

The bulk of this chapter dug into the details of the first pillar of OOP: encapsulation. You learned about the access modifiers of C# and the role of type properties, object initialization syntax, and partial classes.
本章的大部分内容深入探讨了 OOP 的第一个支柱:封装。您了解了 C# 的访问修饰符以及类型属性、对象初始化语法和分部类的作用。

With this behind you, you are now able to turn to the next chapter where you will learn to build a family of related classes using inheritance and polymorphism.
有了这个,你现在可以进入下一章,你将学习使用继承和多态性构建一个相关类的家族。

发表评论