Pro C#10 CHAPTER 4 Core C# Programming Constructs, Part 2

CHAPTER 4

第4章

Core C# Programming Constructs, Part 2

核心 C# 编程 构造,第 2 部分

This chapter picks up where Chapter 3 left off and completes your investigation of the core aspects of the C# programming language. You will start with an investigation of the details behind manipulating arrays using the syntax of C# and get to know the functionality contained within the related System.Array class type.
从第三章结束的地方开始,完成对C#编程语言核心方面的研究。从C#操作数据背后的细节开始,了解System.Array相关的功能。

Next, you will examine various details regarding the construction of C# methods, exploring the out, ref, and params keywords. Along the way, you will also examine the role of optional and named parameters. I finish the discussion on methods with a look at method overloading.
接下来,研究C#方法的各种细节,探讨out,ref关键字和参数。在此过程中,探讨可选参数和命名参数的作用。最后,以方法重载来结束对方法的讨论。

Next, this chapter discusses the construction of enumeration and structure types, including a detailed examination of the distinction between a value type and a reference type. This chapter wraps up by examining the role of nullable data types and the related operators.
接下来,讨论枚举和结构类型的构造,包括值类型和引用类型之间的区别。最后介绍了nullable数据类型和相关运算符的作用。

After you have completed this chapter, you will be in a perfect position to learn the object-oriented capabilities of C#, beginning in Chapter 5.
完成本章后,从第5章开始学习面向对象功能。

Understanding C# Arrays

了解 C# 数组

As I would guess you are already aware, an array is a set of data items, accessed using a numerical index. More specifically, an array is a set of contiguous data points of the same type (an array of ints, an array of strings, an array of SportsCars, etc.). Declaring, filling, and accessing an array with C# are all quite straightforward. To illustrate, create a new Console Application project named FunWithArrays that contains a helper method named SimpleArrays(); as follows:
我想您已经知道了,数组是一组数据项,使用数字索引进行访问。更具体地说,数组是一组相同类型的连续数据点(整数数组、字符串数组、SportsCars数组等)。使用C#声明、填充和访问数组都很简单。为了说明这一点,创建一个新控制台应用程序项目,名为FunWithArrays。该项目包含名为SimpleArrays()的辅助方法(helper)。如下所示:

Console.WriteLine("***** Fun with Arrays *****");
SimpleArrays();
Console.ReadLine();
static void SimpleArrays()
{
    Console.WriteLine("=> Simple Array Creation.");
    // Create and fill an array of 3 integers
    // 创建一个数据,填充3个整数
    int[] myInts = new int[3];

    // Create a 100 item string array, indexed 0 - 99
    // 创建一个100项的字符串数组,索引为0-99
    string[] booksOnDotNet = new string[100];

    Console.WriteLine();
}

Look closely at the previous code comments. When declaring a C# array using this syntax, the number used in the array declaration represents the total number of items, not the upper bound. Also note that the lower bound of an array always begins at 0. Thus, when you write int[] myInts = new int[3], you end up with an array holding three elements, indexed at positions 0, 1, and 2.
仔细查看代码注释。使用此语法声明C#数组时,数组声明中使用的数字表示项目的总数,而不是上限。还要注意,数组的下限(索引)总是从0开始。因此,当您编写int[] myInts = new int[3]时,您最终会得到一个包含三个元素的数组,这些元素在位置0、1和2进行索引。

After you have defined an array variable, you are then able to fill the elements index by index, as shown here in the updated SimpleArrays() method:
定义了数组变量后,就可以按索引填充元素,如更新后的SimpleArrays() 方法所示:

Console.WriteLine("***** Fun with Arrays *****");
SimpleArrays();


static void SimpleArrays()
{
    Console.WriteLine("=> Simple Array Creation.");
    // Create and fill an array of 3 Integers
    int[] myInts = new int[3];
    myInts[0] = 100;
    myInts[1] = 200;
    myInts[2] = 300;

    // Now print each value.
    // 现在,打印每个元素的值
    foreach (int i in myInts)
    {
        Console.WriteLine(i);
    }
    Console.WriteLine();
}

■ Note Do be aware that if you declare an array but do not explicitly fill each index, each item will be set to the default value of the data type (e.g., an array of bools will be set to false or an array of ints will be set to 0).
注意 请注意,如果您声明了一个数组,但没有显式填充每个索引,则每个项都将设置为数据类型的默认值(例如,布尔数组将设置为false,int数组将设置成0)。

Looking at the C# Array Initialization Syntax

查看 C# 数组初始化语法

In addition to filling an array element by element, you can fill the items of an array using C# array initialization syntax. To do so, specify each array item within the scope of curly brackets ({}). This syntax can be helpful when you are creating an array of a known size and want to quickly specify the initial values. For example, consider the following alternative array declarations:
除了逐个元素填充数组元素外,还可以使用C#数组初始化语法填充数组的项。为此,请指定花括号({})范围内的每个数组项。当您创建一个已知大小的数组并希望快速指定初始值时,此语法会很有用。例如,考虑以下替代数组声明:

Console.WriteLine("***** Fun with Arrays *****");
ArrayInitialization();

static void ArrayInitialization()
{
    Console.WriteLine("=> Array Initialization.");
    // Array initialization syntax using the new keyword.
    // 使用 new 关键字的数组初始化语法。
    string[] stringArray = new string[] { "one", "two", "three" };
    Console.WriteLine("stringArray has {0} elements", stringArray.Length);

    // Array initialization syntax without using the new keyword.
    // 不使用 new 关键字的数组初始化语法。
    bool[] boolArray = { false, false, true };
    Console.WriteLine("boolArray has {0} elements", boolArray.Length);

    // Array initialization with new keyword and size.
    // 使用新的关键字和大小进行数组初始化。
    int[] intArray = new int[4] { 20, 22, 23, 0 };
    Console.WriteLine("intArray has {0} elements", intArray.Length);
    Console.WriteLine();
}

Notice that when you make use of this “curly-bracket” syntax, you do not need to specify the size of the array (seen when constructing the stringArray variable), given that this will be inferred by the number of items within the scope of the curly brackets. Also notice that the use of the new keyword is optional (shown when constructing the boolArray type).
请注意,当您使用这种“花括号”语法时,您不需要指定数组的大小(在构造stringArray变量时可以看出来),因为这将由花括号范围内的项数推断。还要注意,new关键字是可选的(在构造boolArray类型时显示)。

In the case of the intArray declaration, again recall the numeric value specified represents the number of elements in the array, not the value of the upper bound. If there is a mismatch between the declared size and the number of initializers (whether you have too many or too few initializers), you are issued a compile- time error. The following is an example:
在intArray声明的情况下,再次调用指定的数值表示数组中元素的数量,而不是上界的值。如果声明的大小和初始化程序的数量不匹配(无论初始化程序太多还是太少),都会发出编译时错误。以下是一个示例:

// OOPS! Mismatch of size and elements!
// 哎呀!数据大小和元素数量不匹配!
int[] intArray = new int[2] { 20, 22, 23, 0 };

Understanding Implicitly Typed Local Arrays

了解隐式类型化本地数组

In Chapter 3, you learned about the topic of implicitly typed local variables. Recall that the var keyword allows you to define a variable, whose underlying type is determined by the compiler. In a similar vein, the var keyword can be used to define implicitly typed local arrays. Using this technique, you can allocate a new array variable without specifying the type contained within the array itself (note you must use the new keyword when using this approach).
在第3章,已经了解了隐式类型局部变量的主题。回想一下,var关键字允许定义一个变量,其基本类型由编译器确定。同样,var关键字可以定义隐式类型的局部数组。使用此技术,可以分配一个新的数组变量,而无需指定数组本身包含的类型(注意,使用此方法时必须使用new关键字)。

Console.WriteLine("***** Fun with Arrays *****");
DeclareImplicitArrays();

static void DeclareImplicitArrays()
{
    Console.WriteLine("=> Implicit Array Initialization.");
    // a is really int[].
    // a 实际上是整数数组
    var a = new[] { 1, 10, 100, 1000 };
    Console.WriteLine("a is a: {0}", a.ToString());

    // b is really double[].
    // b 实际上是double数组
    var b = new[] { 1, 1.5, 2, 2.5 };
    Console.WriteLine("b is a: {0}", b.ToString());

    // c is really string[].
    // c 实际上是字符串数组
    var c = new[] { "hello", null, "world" };
    Console.WriteLine("c is a: {0}", c.ToString());
    Console.WriteLine();
}

Of course, just as when you allocate an array using explicit C# syntax, the items in the array’s initialization list must be of the same underlying type (e.g., all ints, all strings, or all SportsCars). Unlike what you might be expecting, an implicitly typed local array does not default to System.Object; thus, the following generates a compile-time error:
当然,就像使用显式C#语法分配数组一样,数组初始化列表中的项必须具有相同的类型(例如,全部是整数、全部是字符串或全部是SportsCars类的类型)。与您可能期望的不同,隐式类型的本地数组不会默认为System.Object。因此,以下生成编译时错误:

// Error! Mixed types!
// 错误!混合类型!
var d = new[] { 1, "one", 2, "two", false };

Defining an Array of Objects

定义对象数组

In most cases, when you define an array, you do so by specifying the explicit type of item that can be within the array variable. While this seems quite straightforward, there is one notable twist. As you will come to understand in Chapter 6, System.Object is the ultimate base class to every type (including fundamental data types) in the .NET Core type system. Given this fact, if you were to define an array of System.Object data types, the subitems could be anything at all. Consider the following ArrayOfObjects() method:
在大多数情况下,定义数组时,可以通过指定数组变量中的显式项类型来实现。虽然这看起来很简单,但有一个值得注意的转折点。您将在第6章中了解到,System.Object是.NET所有类型(包括基本数据类型)的基类。考虑到这个事实,如果您要定义System.Object数据类型的数组,那么子项可以是任何东西。考虑以下ArrayOfObjects() 方法:

Console.WriteLine("***** Fun with Arrays *****");
ArrayOfObjects();

static void ArrayOfObjects()
{
    Console.WriteLine("=> Array of Objects.");
    // An array of objects can be anything at all.
    // 对象数组可以是任何东西。
    // 这里是定义了4个对象。每个对象的数值类型都不一样。
    object[] myObjects = new object[4];
    myObjects[0] = 10;
    myObjects[1] = false;
    myObjects[2] = new DateTime(1969, 3, 24);
    myObjects[3] = "Form & Void";
    foreach (object obj in myObjects)
    {
        // Print the type and value for each item in array.
        // 打印数组中每个项目的类型和值。
        Console.WriteLine("Type: {0}, Value: {1}", obj.GetType(), obj);
    }
    Console.WriteLine();
}

Here, as you are iterating over the contents of myObjects, you print the underlying type of each item using the GetType() method of System.Object, as well as the value of the current item. Without going into too much detail regarding System.Object.GetType() at this point in the text, simply understand that this method can be used to obtain the fully qualified name of the item (Chapter 17 examines the topic of type information and reflection services in detail). The following output shows the result of calling ArrayOfObjects():
在这里,迭代myObjects的内容时,可以使用System.Object的GetType()方法打印每个项的底层类型,以及当前项的值。现在,不需要太多关注System.Object.GetType()的细节,只需理解这种方法可以用来获得项目的完全限定名称(第17章详细介绍了类型信息和反射的主题)。以下输出是调用ArrayOfObjects()的结果:

***** Fun with Arrays *****
=> Array of Objects.
Type: System.Int32, Value: 10
Type: System.Boolean, Value: False
Type: System.DateTime, Value: 1969/3/24 0:00:00
Type: System.String, Value: Form & Void

Working with Multidimensional Arrays

使用多维数组

In addition to the single dimension arrays you have seen thus far, C# supports two varieties of multidimensional arrays. The first of these is termed a rectangular array, which is simply an array of multiple dimensions, where each row is of the same length. To declare and fill a multidimensional rectangular array, proceed as follows:
除了您迄今为止看到的一维数组之外,C#还支持两种类型的多维数组。第一种类型被称为矩形阵列(矩形数组),它只是一个多个维度的阵列,其中每行的长度相同。要声明和填充多维矩形数组,请执行以下操作:

Console.WriteLine("***** Fun with Arrays *****");
RectMultidimensionalArray();

static void RectMultidimensionalArray()
{
    Console.WriteLine("=> Rectangular multidimensional array.");
    // A rectangular multidimensional array.
    // 矩形多维数组。
    int[,] myMatrix;
    myMatrix = new int[3, 4];
    // Populate (3 * 4) array.
    // 填充 (3 * 4) 数组阵列
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            myMatrix[i, j] = i * j;
        }
    }

    // Print (3 * 4) array.
    // 打印
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            Console.Write(myMatrix[i, j] + "\t");
        }
        Console.WriteLine();
    }
    Console.WriteLine();
}

The second type of multidimensional array is termed a jagged array. As the name implies, jagged arrays contain some number of inner arrays, each of which may have a different upper limit. Here is an example:
第二种类型的多维数组称为锯齿状数组。顾名思义,锯齿状数组包含一定数量的内部数组,每个内部数组可能有不同的上限。以下是一个示例:

Console.WriteLine("***** Fun with Arrays *****");
JaggedMultidimensionalArray();

static void JaggedMultidimensionalArray()
{
    Console.WriteLine("=> Jagged multidimensional array.");
    // A jagged MD array (i.e., an array of arrays).
    // 交错的多维数组(即数组的数组)。
    // Here we have an array of 5 different arrays.
    // 里我们有一个包含 5 个不同数组的数组。
    int[][] myJagArray = new int[5][];
    // Create the jagged array.
    // 创建交错数组。
    for (int i = 0; i < myJagArray.Length; i++)
    {
        myJagArray[i] = new int[i + 7];
    }

    // Print each row (remember, each element is defaulted to zero!).
    // 打印每一行(请记住,每个元素默认为零!)
    for (int i = 0; i < 5; i++)
    {
        for (int j = 0; j < myJagArray[i].Length; j++)
        {
            Console.Write(myJagArray[i][j] + " ");
        }
        Console.WriteLine();
    }
    Console.WriteLine();
}

The output of calling each of the RectMultidimensionalArray() and JaggedMultidimensionalArray() methods is shown next:
调用RectMultidimensionalArray()和JaggedMultidimensionalArray()两个方法的输出如下:

***** Fun with Arrays *****
=> Rectangular multidimensional array.
0       0       0       0
0       1       2       3
0       2       4       6

=> Jagged multidimensional array.
0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0

Using Arrays As Arguments or Return Values

使用数组作为参数或返回值

After you have created an array, you are free to pass it as an argument or receive it as a member return value. For example, the following PrintArray() method takes an incoming array of ints and prints each member to the console, while the GetStringArray() method populates an array of strings and returns it to the caller:

After you have created an array, you are free to pass it as an argument or receive it as a member return value. For example, the following PrintArray() method takes an incoming array of ints and prints each member to the console, while the GetStringArray() method populates an array of strings and returns it to the caller:
创建数组后,可以将其作为参数传递或作为方法返回值。例如,以下PrintArray()方法获取一个传入的int数组并将每个成员打印到控制台,而GetStringArray()方法填充一个字符串数组并将其返回给调用方:

static void PrintArray(int[] myInts)
{
    for (int i = 0; i < myInts.Length; i++)
    {
        Console.WriteLine("Item {0} is {1}", i, myInts[i]);
    }
}

static string[] GetStringArray()
{
    string[] theStrings = { "Hello", "from", "GetStringArray" };
    return theStrings;
}

These methods can be invoked as you would expect.
可以按预期调用这些方法。

Console.WriteLine("***** Fun with Arrays *****");
PassAndReceiveArrays();

static void PassAndReceiveArrays()
{
    Console.WriteLine("=> Arrays as params and return values.");
    // Pass array as parameter.
    // 将数组作为参数传递。
    int[] ages = { 20, 22, 23, 0 };
    PrintArray(ages);
    // Get array as return value.
    // 获取数组作为返回值。
    string[] strs = GetStringArray();
    foreach (string s in strs)
    {
        Console.WriteLine(s);
    }
    Console.WriteLine();
}

// 接受数组参数,遍历数据并在控制台打印
static void PrintArray(int[] myInts)
{
    for (int i = 0; i < myInts.Length; i++)
    {
        Console.WriteLine("Item {0} is {1}", i, myInts[i]);
    }
}

// 返回值是数组
static string[] GetStringArray()
{
    string[] theStrings = { "Hello", "from", "GetStringArray" };
    return theStrings;
}

At this point, you should feel comfortable with the process of defining, filling, and examining the contents of a C# array variable. To complete the picture, let’s now examine the role of the System.Array class.
此时,您应该对定义、填充数组变量的过程感到满意。为了完成这幅拼图,现在让我们探讨一下System.Array类的作用。

Using the System.Array Base Class

使用 System.Array 基类

Every array you create gathers much of its functionality from the System.Array class. Using these common members, you can operate on an array using a consistent object model. Table 4-1 gives a rundown of some of the more interesting members (be sure to check the documentation for full details).
您创建的每个数组都从System.array类中获得其大部分功能。使用这些通用成员(方法或属性),可以使用一致的对象模型对数组进行操作。表4-1列出了一些更有趣的成员(请务必查看文档以获取完整详细信息)。

Table 4-1. Select Members of System.Array
表4-1. System.Array的成员

Member of Array Class Meaning in Life
Clear() This static method sets a range of elements in the array to empty values (0 for numbers,null for object references, false for Booleans).
此静态方法将数组中的元素范围设置为空值(数字设置为0,对象设置为null,布尔值设置为false)。
CopyTo() This method is used to copy elements from the source array into the destination array.
此方法用于将元素从源数组复制到目标数组中。
Length This property returns the number of items within the array.
此属性返回数组中的项数 。
Rank This property returns the number of dimensions of the current array.
此属性返回当前数组的维数 。
Reverse() This static method reverses the contents of a one-dimensional array.
此静态方法反转一维数组的内容 。
Sort() This static method sorts a one-dimensional array of intrinsic types. If the elements in the array implement the IComparer interface, you can also sort your custom types (see Chapters 8 and 10).
此静态方法对内部类型的一维数组进行排序。如果数组中的元素实现IComparer接口,您也可以对自定义类型进行排序(请参阅第8章和第10章)。

Let’s see some of these members in action. The following helper method makes use of the static Reverse() and Clear() methods to pump out information about an array of string types to the console:
让我们看看其中一些成员的操作。以下helper方法使用静态Reverse()和Clear()方法将字符串类型数组的信息输出到控制台:

Console.WriteLine("***** Fun with Arrays *****");
SystemArrayFunctionality();

static void SystemArrayFunctionality()
{
    Console.WriteLine("=> Working with System.Array.");
    // Initialize items at startup.
    // 在启动时初始化数组每一项。
    string[] gothicBands = { "Tones on Tail", "Bauhaus", "Sisters of Mercy" };

    // Print out names in declared order.
    // 按声明(定义的)顺序打印姓名。
    Console.WriteLine("-> Here is the array:");
    for (int i = 0; i < gothicBands.Length; i++)
    {
        // Print a name.
        Console.Write(gothicBands[i] + ", ");
    }
    Console.WriteLine("\n");

    // Reverse them...
    // 反转他们
    Array.Reverse(gothicBands);
    Console.WriteLine("-> The reversed array");

    // ... and print them.
    // 并打印它们
    for (int i = 0; i < gothicBands.Length; i++)
    {
        // Print a name.
        // 打印姓名
        Console.Write(gothicBands[i] + ", ");
    }
    Console.WriteLine("\n");

    // Clear out all but the first member.
    // 清除第一个成员之外的所有成员。
    Console.WriteLine("-> Cleared out all but one...");
    Array.Clear(gothicBands, 1, 2);
    for (int i = 0; i < gothicBands.Length; i++)
    {
        // Print a name.
        // 打印姓名
        Console.Write(gothicBands[i] + ", ");
    }
    Console.WriteLine();
}

If you invoke this method, you will get the output shown here:
如果调用此方法, 将获得如下所示的输出 :

***** Fun with Arrays *****
=> Working with System.Array.
-> Here is the array:
Tones on Tail, Bauhaus, Sisters of Mercy,

-> The reversed array
Sisters of Mercy, Bauhaus, Tones on Tail,

-> Cleared out all but one...
Sisters of Mercy, , ,

Notice that many members of System.Array are defined as static members and are, therefore, called at the class level (e.g., the Array.Sort() and Array.Reverse() methods). Methods such as these are passed in the array you want to process. Other members of System.Array (such as the Length property) are bound at the object level; thus, you can invoke the member directly on the array.
请注意,System.Array的许多成员都被定义为静态成员(例如,Array.Sort()和Array.Reverse()方法),因此可以在类级别被调用,传递要处理的数组。System.Array的其他成员(如Length属性)在对象级别绑定;因此,可以直接在数组上调用该成员。

Using Indices and Ranges (New 8.0, Updated 10.0)

使用索引和范围(新版 8.0,更新的 10.0)

To simplify working with sequences (including arrays), C# 8 introduces two new types and two new operators for use when working with arrays.
为了简化对序列(包括数组)的处理,C#8引入了两种新类型和两种新运算符,以便在处理数组时使用。

  • System.Index represents an index into a sequence.
    System.Index 表示序列中的索引。
  • System.Range represents a subrange of indices.
    System.Range 表示索引的子范围。
  • The index from end operator (^) specifies that the index is relative to the end of the sequence.
    结束运算符(^)指定索引相对于序列的结束。
  • The range operator (..) specifies the start and end of a range as its operands.
    范围运算符(..)指定范围的开始和结束作为其操作数。

■ Note indices and ranges can be used with arrays, strings, Span, ReadOnlySpan, and (added in .net 6/C# 10) IEnumerable.
注意 索引和范围可以与arrays, strings, Span, ReadOnlySpan, IEnumerable一起使用。

As you have already seen, arrays are indexed beginning with zero (0). The end of a sequence is the length of the sequence – 1. The previous for loop that printed the gothicBands array can be updated to the following:
正如您已经看到的,数组的索引从零(0)开始。序列的末尾是序列的长度-1。之前打印gothicBands数组的for循环可以更新为以下内容:

Console.WriteLine("***** Fun with Arrays *****");

string[] gothicBands = { "Tones on Tail", "Bauhaus", "Sisters of Mercy" };

for (int i = 0; i < gothicBands.Length; i++)
{
    Index idx = i;
    // Print a name
    Console.Write(gothicBands[idx] + ", ");
}

The index from end operator lets you specify how many positions from the end of sequence, starting with the length. Remember that the last item in a sequence is one less than the actual length, so ^0 would cause an error. The following code prints the array in reverse:
索引起始点运算符用于指定从序列末尾开始的位置数,从长度开始。请记住,序列中的最后一项比实际长度少一项,因此^0会导致错误。以下代码反向打印数组:

Console.WriteLine("***** Fun with Arrays *****");

string[] gothicBands = { "Tones on Tail", "Bauhaus", "Sisters of Mercy" };

for (int i = 1; i <= gothicBands.Length; i++)
{
    Index idx = ^i;
    // Print a name
    Console.Write(gothicBands[idx] + ", ");
}

The range operator specifies a start and end index and allows for access to a subsequence within a list. The start of the range is inclusive, and the end of the range is exclusive. For example, to pull out the first two members of the array, create ranges from 0 (the first member) to 2 (one more than the desired index position).
范围运算符指定开始和结束索引,并允许访问列表中的子序列。范围的开始是包含的,范围的结束是排除的。例如,要拉出数组的前两个成员,请创建从0(第一个成员)到2(比所需索引位置多一个)的范围。

Console.WriteLine("***** Fun with Arrays *****");

string[] gothicBands = { "Tones on Tail", "Bauhaus", "Sisters of Mercy" };

foreach (var itm in gothicBands[0..2])
{
    // Print a name
    Console.Write(itm + ", ");
}
Console.WriteLine("\n");

Ranges can also be passed to a sequence using the new Range data type, as shown here:
也可以使用新的范围数据类型将范围传递给序列,如下所示:

Console.WriteLine("***** Fun with Arrays *****");

string[] gothicBands = { "Tones on Tail", "Bauhaus", "Sisters of Mercy" };

Range r = 0..2;
//  the end of the range is exclusive
// 范围的末尾是排除的
foreach (var itm in gothicBands[r])
{
    // Print a name
    Console.Write(itm + ", ");
}
Console.WriteLine("\n");

Ranges can be defined using integers or Index variables. The same result will occur with the following code:
可以使用整数或索引变量定义范围。以下代码将出现相同的结果:

Console.WriteLine("***** Fun with Arrays *****");

string[] gothicBands = { "Tones on Tail", "Bauhaus", "Sisters of Mercy" };

Index idx1 = 0;
Index idx2 = 2;
Range r = idx1..idx2; 
//the end of the range is exclusive
foreach (var itm in gothicBands[r])
{
    // Print a name
    Console.Write(itm + ", ");
}
Console.WriteLine("\n");

If the beginning of the range is left off, the beginning of the sequence is used. If the end of the range is left off, the length of the range is used. This does not cause an error, since the value at the end of the range is exclusive. For the previous example of three items in an array, all the ranges represent the same subset.
如果不指定范围的开头,则使用序列的开头。如果不指定范围的末尾,则使用范围的长度。这不会导致错误,因为范围末尾的值是独占的。对于前面的数组中三个项目的示例,所有范围都表示相同的子集。

gothicBands[..]
gothicBands[0..^0]
gothicBands[0..3]

The ElementAt() extension method (in the System.Linq namespace) retrieves the element from the array at the specified location. New in .NET 6/C# 10, using the index from end operator is supported to get an element the specified distance from the end of the array. The following code gets the second-to-last band from the list:
ElementAt()扩展方法从指定位置的数组中检索元素,是NET 6/C#10中的新增功能,支持使用index from end运算符来获取距离数组末尾指定距离的元素。以下代码从列表中获取倒数第二个元素:

Console.WriteLine("***** Fun with Arrays *****");

string[] gothicBands = { "Tones on Tail", "Bauhaus", "Sisters of Mercy" };

var band = gothicBands.ElementAt(^2);
Console.WriteLine(band);

■ Note support for using Index and Range parameters has been added to LinQ. see Chapter 13 for more information.
注意 LinQ 中添加了对使用索引和范围参数的支持。有关详细信息,请参阅第 13 章。

Understanding Methods

了解方法

Let’s examine the details of defining methods. Methods are defined by an access modifier and return type (or void for no return type) and may or may not take parameters. A method that returns a value to the caller is commonly referred to as a function, while methods that do not return a value are commonly referred to as methods.
让我们研究一下定义方法的细节。方法由访问修饰符和返回类型(或void表示无返回类型)定义,可以接受也可以不接受参数。将值返回给调用方的方法通常称为函数,而不返回值的方法通常被称为方法。

■Note access modifiers for methods (and classes) are covered in Chapter 5. method parameters are covered in the next section.
注意 方法(和类)的访问修饰符在第5章中介绍。方法参数将在下一节中介绍。

At this point in the text, each of your methods has the following basic format:
每个方法都具有以下基本格式:

// Recall that static methods can be called directly
// 静态方法可以直接调用
// without creating a class instance.
// 无需创建类实例。

// static returnType MethodName(parameter list) { /* Implementation */ }
// 静态 返回类型 方法名称(参数列表) { /* 具体实现方法的语句 */ }

static int Add(int x, int y)
{
    return x + y;
}

As you will see over the next several chapters, methods can be implemented within the scope of classes, structures, or (new in C# 8) interfaces.
正如您将在接下来的几章中看到的,方法可以在类、结构或(C# 8 中的新功能)接口的范围内实现。

Understanding Expression-Bodied Members

理解Expression Bodied成员

You already learned about simple methods that return values, such as the Add() method. C# 6 introduced expression-bodied members that shorten the syntax for single-line methods. For example, Add() can be rewritten using the following syntax:
您已经了解了返回值的简单方法,例如Add()方法。C#引入了表达式体成员,缩短了单行方法的语法。例如,可以使用以下语法重写Add():
Expression-bodied 方法是能简化代码的特性,将LAMBDA表达式的用法扩展到方法上。

static int Add(int x, int y) => x + y;

This is what is commonly referred to as syntactic sugar, meaning that the generated IL is no different. It is just another way to write the method. Some find it easier to read, and others do not, so the choice is yours (or your team’s) which style you prefer.
这就是通常所说的句法糖,两种写法生成的IL是一样的。这只是编写方法的另一种方式。有些人觉得它更容易阅读,而另一些人则不然,所以你可以选择你喜欢的风格。

■ Note Don’t be alarmed by the => operator. this is a lambda operation, which is covered in detail in Chapter 12. that chapter also explains exactly how expression-bodied members work. For now, just consider them a shortcut to writing single-line statements.
注意 不要被 => 运算符吓到。 这是一个 lambda 操作,第 12 章将详细介绍。 这一章还确切地解释了Expression-bodied成员是如何工作的。现在,只需将它们视为编写单行语句的快捷方式。

Understanding Local Functions (New 7.0, Updated 9.0)

了解本地函数

A feature introduced in C# 7.0 is the ability to create methods within methods, referred to officially as local functions. A local function is a function declared inside another function, must be private, with C# 8.0 can be static (see the next section), and does not support overloading. Local functions do support nesting: a local function can have a local function declared inside it.
C#7.0中引入的一个功能是能够在方法中创建方法,正式称为本地函数。本地函数是在另一个函数内部声明的函数,必须是私有的,使用C#8.0可以是静态的(请参阅下一节),并且不支持重载。本地函数支持嵌套:本地函数内部可以声明一个本地函数。

To see how this works, create a new Console Application project named FunWithLocalFunctions. As an example, let’s say you want to extend the Add() example used previously to include validation of the inputs. There are many ways to accomplish this, and one simple way is to add the validation directly into the Add() method. Let’s go with that and update the previous example to the following (the comment representing validation logic):
要了解这是如何工作的,请创建一个名为FunWithLocalFunctions的新控制台应用程序项目。举个例子,假设您想扩展以前使用的Add()示例,增加输入的验证。有很多方法可以实现这一点,其中一种简单的方法是将验证直接添加到add()方法中。让我们继续,并将前面的示例更新为以下内容(表示验证逻辑的注释):

static int Add(int x, int y)
{
    // Do some validation here
    // 这里做一些验证

    // 然后返回值(输出结果)
    return x + y;
}

As you can see, there are no big changes. There is just a comment indicating that real code should do something. What if you wanted to separate the actual reason for the method (returning the sum of the arguments) from the validation of the arguments? You could create additional methods and call them from the Add() method. But that would require creating another method just for use by one other method. Maybe that’s overkill. Local functions allow you to do the validation first and then encapsulate the real goal of the method defined inside the AddWrapper() method, as shown here:
如您所见,没有什么大的变化。只有一条注释表明真正的代码应该做些什么。如果您想将方法的实际原因(返回参数的总和)与参数的验证分开,该怎么办?您可以创建其他方法并从Add()方法调用它们。但这将需要创建另一个方法,只供另一种方法使用。也许这是矫枉过正。本地函数允许您首先进行验证,然后将定义的实际目标的方法封装AddWrapper()方法中,如下所示:

static int AddWrapper(int x, int y)
{
    //Do some validation here
    // 这里做一些验证

    return Add();
    int Add()
    {
        return x + y;
    }
}

The contained Add() method can be called only from the wrapping AddWrapper() method. So, the question I am sure you are thinking is, “What did this buy me?” The answer for this specific example, quite simply, is little (if anything). But what if AddWrapper() needed to execute the Add() function from multiple places? Now you should start to see the benefit of having a local function for code reuse that is not exposed outside of where it is needed. You will see even more benefit gained with local functions when we cover custom iterator methods (Chapter 8) and asynchronous methods (Chapter 15).
只能从包装AddWrapper()方法调用包含的Add()方法。所以,我相信你在想的问题是,“这给我买了什么?”这个具体例子的答案很简单,很少(如果有的话)。但是,如果AddWrapper()需要从多个位置执行Add()函数,该怎么办?现在,您应该开始看到拥有一个用于代码重用的本地函数的好处,该函数不会在需要它的地方之外公开。当我们介绍自定义迭代器方法(第8章)和异步方法(第15章)时,您将看到本地函数获得的更多好处。

■ Note the AddWrapper() local function is an example of local function with a nested local function. recall that functions declared in top-level statements are created as local functions. the Add() local function is in the AddWrapper() local function. this capability typically is not used outside of teaching examples, but if you ever need to nest local functions, you know that C# supports it.
AddWrapper()本地函数是一个带有嵌套本地函数的局部函数示例。请记住,在顶级语句中声明的函数是作为本地函数创建的。Add()局部函数位于AddWrapper()局部功能中。这种功能通常不会在教学示例之外使用,但如果您需要嵌套本地函数,您知道C#支持它。

C# 9.0 updated local functions to allow for adding attributes to a local function, its parameters, and its type parameters, as in the following example (do not worry about the NotNullWhen attribute, which will be covered later in this chapter):
C#9.0更新了本地函数,允许向本地函数、其参数和类型参数添加属性,如下例所示(不要担心NotNullWhen属性,这将在本章稍后介绍):

C# 9.0 更新了本地函数,以允许向本地函数、其参数和类型参数添加属性,如以下示例所示(不用担心 NotNullWhen 属性,本章稍后将介绍该属性):

#nullable enable
private static void Process(string?[] lines, string mark)
{
    foreach (var line in lines)
    {
        if (IsValid(line))
        {
            // Processing logic...
        }
    }
    bool IsValid([NotNullWhen(true)] string? line)
    {
        return !string.IsNullOrEmpty(line) && line.Length >= mark.Length;
    }
}

Understanding Static Local Functions (New 8.0)

了解静态本地函数(新 8.0)

An improvement to local functions that was introduced in C# 8 is the ability to declare a local function as static. In the previous example, the local Add() function was referencing the variables from the main function directly. This could cause unexpected side effects, since the local function can change the values of the variables.
C#8中引入一个改进的本地函数。能够将本地函数声明为静态函数。在前面的示例中,本地Add()函数直接引用主函数中的变量。这可能会导致意外的副作用,因为本地函数可以更改变量的值。

To see this in action, create a new method called AddWrapperWithSideEffect(), as shown here:
若要查看此操作的实际效果,请创建一个名为AddWrapperWithSideEffect()的新方法,如下所示:

static int AddWrapperWithSideEffect(int x, int y)
{
    //Do some validation here
    return Add();
    int Add()
    {
        x += 1;
        return x + y;
    }
}

Of course, this example is so simple, it probably would not happen in real code. To prevent this type of mistake, add the static modifier to the local function. This prevents the local function from accessing the parent method variables directly, and this causes the compiler exception CS8421, “A static local function cannot contain a reference to ‘.’”
当然,这个例子很简单,它可能不会发生在真实的代码中。为了防止这种类型的错误,请将静态修饰符添加到本地函数中。这会阻止本地函数直接访问父方法变量,并导致编译器异常CS8421,“静态本地函数不能包含对’‘的引用。”

The improved version of the previous method is shown here:
以前方法的改进版本如下所示:

static int AddWrapperWithStatic(int x, int y)
{
    //Do some validation here
    return Add(x, y);
    static int Add(int x, int y)
    {
        return x + y;
    }
}

Understanding Method Parameters

了解方法参数

Method parameters are used to pass data into a method call. Over the next several sections, you will learn the details of how methods (and their callers) treat parameters.
方法参数用于将数据传递到调用方法中。在接下来的几节中,您将详细了解方法(及其调用方)如何处理参数。

Understanding Method Parameter Modifiers

了解方法参数修饰符

The default way a parameter is sent into a function is by value. Simply put, if you do not mark an argument with a parameter modifier, a copy of the data is passed into the function. As explained later in this chapter, exactly what is copied will depend on whether the parameter is a value type or a reference type.
将参数发送到函数的默认方式是按值发送。简单地说,如果不使用参数修饰符标记参数,则会将数据的副本传递到函数中。如本章稍后所述,复制的内容将取决于参数是值类型还是引用类型。

While the definition of a method in C# is quite straightforward, you can use a handful of methods to control how arguments are passed to a method, as listed in Table 4-2.
虽然C#中方法的定义非常简单,但您可以使用一些方法来控制参数如何传递给方法,如表4-2所示。

Table 4-2. C# Parameter Modifiers
表 4-2. C# 参数修饰符

Parameter Modifier Meaning in Life
(None) If a value type parameter is not marked with a modifier, it is assumed to be passed by value, meaning the called method receives a copy of the original data. Reference types without a modifier are passed in by reference.
如果值类型参数未标有修饰符,则假定它是按值传递的,这意味着被调用的方法接收原始数据的副本。不带修饰符的引用类型通过引用传入。
out Output parameters must be assigned by the method being called and, therefore, are passed by reference. If the called method fails to assign output parameters, you are issued a compiler error.
输出参数必须由被调用的方法分配,因此通过引用传递。如果调用的方法无法分配输出参数,则会向您发出编译器错误。
ref The value is initially assigned by the caller and may be optionally modified by the called method (as the data is also passed by reference). No compiler error is generated if the called method fails to assign a ref parameter.
该值最初由调用者分配,并且可以由被调用的方法选择性地修改(因为数据也是通过引用传递的)。如果被调用的方法未能分配ref参数,则不会生成编译器错误。
in New in C# 7.2, the in modifier indicates that a ref parameter is read-only by the called method.
C# 7.2 中的新增功能,in 修饰符指示 ref 参数被调用的方法只读。
params This parameter modifier allows you to send in a variable number of arguments as a single logical parameter. A method can have only a single params modifier, and it must be the final parameter of the method. You might not need to use the params modifier all too often;
however, be aware that numerous methods within the base class libraries do make use of this C# language feature.
此参数修饰符允许您将可变数量的参数作为单个逻辑参数发送。一个方法只能有一个params修饰符,并且它必须是该方法的最终参数。您可能不需要经常使用params修饰符;但是,请注意,基类库中的许多方法确实使用了这个特性。

To illustrate the use of these keywords, create a new Console Application project named FunWithMethods. Now, let’s walk through the role of each keyword.
为了说明这些关键字的用法,请创建一个名为 FunWithMethods 的新控制台应用程序项目。现在,让我们来看看每个关键字的作用。

Understanding the Default Parameter-Passing Behavior

了解默认参数传递行为

When a parameter does not have a modifier, the behavior for value types is to pass in the parameter by value and for reference types is to pass in the parameter by reference.
当参数没有修饰符时,值类型的行为是按值传入参数,引用类型的行为是按引用传入参数。

■ Note Value types and reference types are covered later in this chapter.
注释 本章稍后将介绍值类型和引用类型。

The Default Behavior for Value Types

值类型的默认行为

The default way a value type parameter is sent into a function is by value. Simply put, if you do not mark the argument with a modifier, a copy of the data is passed into the function. Add the following method to the Program.cs file that operates on two numerical data types passed by value:
将值类型参数发送到函数的默认方式是按值发送。简单地说,如果不使用修饰符标记参数,则会将数据的副本传递到函数中。将以下方法添加到Program.cs文件中,对通过值传递的两种数值数据类型进行操作:

// Value type arguments are passed by value by default.
// 默认情况下,值类型参数按值传递。
static int Add(int x, int y)
{
    int ans = x + y;
    // Caller will not see these changes
    // 调用者看不到这些
    // as you are modifying a copy of the original data.
    // 你修改的原始数据副本

    x = 10000;
    y = 88888;
    return ans;
}

Numerical data falls under the category of value types. Therefore, if you change the values of the parameters within the scope of the member, the caller is blissfully unaware, given that you are changing the values on a copy of the caller’s original data.
数值数据属于值类型的范畴。因此,如果您在成员的范围内更改参数的值,那么调用者不知道,因为您正在更改调用者原始数据副本上的值。

Console.WriteLine("***** Fun with Methods *****\n");

// Pass two variables in by value.
// 按值传入两个变量。
int x = 9, y = 10;
Console.WriteLine("Before call: X: {0}, Y: {1}", x, y);
Console.WriteLine("Answer is: {0}", Add(x, y));
Console.WriteLine("After call: X: {0}, Y: {1}", x, y); 
Console.ReadLine();

// Value type arguments are passed by value by default.
// 默认情况下,值类型参数按值传递。
static int Add(int x, int y)
{
    int ans = x + y;
    // Caller will not see these changes
    // 调用者看不到这些
    // as you are modifying a copy of the original data.
    // 你修改的原始数据副本

    x = 10000;
    y = 88888;
    return ans;
}

As you would hope, the values of x and y remain identical before and after the call to Add(), as shown in the following output, as the data points were sent in by value. Thus, any changes on these parameters within the Add() method are not seen by the caller, as the Add() method is operating on a copy of the data.
如您所愿,在调用Add()之前和之后,x和y的值保持不变,如以下输出所示,因为数据点是按值发送的。因此,调用程序看不到Add()方法中这些参数的任何更改,因为Add()法是在数据副本上操作的。

***** Fun with Methods *****

Before call: X: 9, Y: 10
Answer is: 19
After call: X: 9, Y: 10

The Default Behavior for Reference Types

引用类型的默认行为

The default way a reference type parameter is sent into a function is by reference for its properties, but by value for itself. This is covered in detail later in this chapter, after the discussion of value types and reference types.
将引用类型参数发送到函数的默认方式是通过对其属性的引用,而不是通过对其自身的值。在讨论了值类型和引用类型之后,本章稍后将对此进行详细介绍。

■ Note even though the string data type is technically a reference type, as discussed in Chapter 3, it’s a special case. When a string parameter does not have a modifier, it is passed in by value.
请注意,尽管字符串数据类型在技术上是一种引用类型,如第3章所述,但这是一种特殊情况。当字符串参数没有修饰符时,它会通过值传入。

Using the out Modifier (Updated 7.0)

使用 out 修饰符(7.0 更新)

Next, you have the use of output parameters. Methods that have been defined to take output parameters (via the out keyword) are under obligation to assign them to an appropriate value before exiting the method scope (if you fail to do so, you will receive compiler errors).
接下来,您可以使用输出参数。已定义为采用输出参数(通过 out 关键字)的方法有在退出方法范围之前将它们分配给适当的值(如果不这样做,您将收到编译器错误)。

To illustrate, here is an alternative version of the Add() method that returns the sum of two integers using the C# out modifier (note the physical return value of this method is now void):
为了说明这一点,下面是Add()方法的另一个版本,它使用out修饰符返回两个整数的和(请注意,该方法的返回值现在为void):

// Output parameters must be assigned by the called method.
// 输出参数必须由调用的方法分配。
static void AddUsingOutParam(int x, int y, out int ans)
{
    ans = x + y;
}

Calling a method with output parameters also requires the use of the out modifier. However, the local variables that are passed as output variables are not required to be assigned before passing them in as output arguments (if you do so, the original value is lost after the call). The reason the compiler allows you to send in seemingly unassigned data is because the method being called must make an assignment. To call the updated Add method, create a variable of type int, and use the out modifier in the call, like this:
使用带有输出参数的方法还需要使用out修饰符。但是,作为输出变量传递的局部变量在作为输出参数传递之前不需要赋值(如果这样做,则原始值在调用后丢失)。编译器允许您发送看似未分配的数据的原因是,被调用的方法必须进行赋值。若要调用更新后的Add方法,请创建一个int类型的变量,并在调用中使用out修饰符,如下所示:

int ans;
AddUsingOutParam(90, 90, out ans);

Starting with C# 7.0, out parameters do not need to be declared before using them. In other words, they can be declared inside the method call, like this:
从C#7.0开始,out参数在使用之前不需要声明。换句话说,它们可以在方法调用中声明,如下所示:

AddUsingOutParam(90, 90, out int ans);

The following code is an example of calling a method with an inline declaration of the out parameter:
下面的代码是使用 out 参数的内联声明调用方法的示例:

Console.WriteLine("***** Fun with Methods *****");
// No need to assign initial value to local variables
// 无需为局部变量赋值
// used as output parameters, provided the first time
// 用作输出参数,首次提供
// you use them is as output arguments.
// 将它们用作输出参数。
// C# 7 allows for out parameters to be declared in the method call
// C# 7 允许在方法调用
AddUsingOutParam(90, 90, out int ans);
Console.WriteLine("90 + 90 = {0}", ans);
Console.ReadLine();

// Output parameters must be assigned by the called method.
// 输出参数必须由调用的方法分配。
static void AddUsingOutParam(int x, int y, out int ans)
{
    ans = x + y;
}

The previous example is intended to be illustrative in nature; you really have no reason to return the value of your summation using an output parameter. However, the C# out modifier does serve a useful purpose: it allows the caller to obtain multiple outputs from a single method invocation.
前面的例子本质上是为了说明问题;您确实没有理由使用输出参数返回求和的值。然而,out修饰符确实有一个有用的用途:它允许调用者从单个方法调用中获得多个输出。

// Returning multiple output parameters.
// // 返回多个输出参数。
static void FillTheseValues(out int , out string b, out bool c)
{
    a = 9;
    b = "Enjoy your string.";
    c = true;
}

The caller would be able to invoke the FillTheseValues() method. Remember that you must use the out modifier when you invoke the method, as well as when you implement the method.
调用方将能够调用FillTheseValue()方法。请记住,在调用该方法时以及实现该方法时,必须使用out修饰符。

Console.WriteLine("***** Fun with Methods *****");
FillTheseValues(out int i, out string str, out bool b);
Console.WriteLine("Int is: {0}", i);
Console.WriteLine("String is: {0}", str);
Console.WriteLine("Boolean is: {0}", b);
Console.ReadLine();


// Returning multiple output parameters.
// // 返回多个输出参数。
static void FillTheseValues(out int a, out string b, out bool c)
{
    a = 9;
    b = "Enjoy your string.";
    c = true;
}

■Note C# 7 also introduced tuples, which are another way to return multiple values out of a method call. You will learn more about that later in this chapter.
注意 C# 7 还引入了元组,元组是从方法调用中返回多个值的另一种方法。您将在本章后面对此进行详细介绍。

Always remember that a method that defines output parameters must assign the parameter to a valid value before exiting the method scope. Therefore, the following code will result in a compiler error, as the output parameter has not been assigned within the method scope:
请始终记住,定义输出参数的方法必须在退出方法范围之前将参数分配给有效值。因此,以下代码将导致编译器错误,因为尚未在方法范围内分配输出参数:

static void ThisWontCompile(out int a)
{
    Console.WriteLine("Error! Forgot to assign output arg!");
}

Discarding out Parameters (New 7.0)

丢弃参数(新 7.0)

If you do not care about the value of an out parameter, you can use a discard as a placeholder. Discards are temporary, dummy variables that are intentionally unused. They are unassigned, do not have a value, and might not even allocate any memory. This can provide a performance benefit as well as make your code more readable. Discards can be used with out parameters, with tuples (later in this chapter), with pattern matching (Chapters 6 and 8), or even as stand-alone variables.
如果不关心out参数的值,可以使用discard作为占位符。丢弃是故意未使用的临时伪变量。它们是未分配的,没有值,甚至可能不会分配任何内存。这可以提供性能优势,并使代码更具可读性。Discards可以与out参数、元组(本章稍后部分)、模式匹配(第6章和第8章)一起使用,甚至可以作为独立变量使用。

For example, if you want to get the value for the int in the previous example but do not care about the second two parameters, you can write the following code:
例如,如果您想获得上一个示例中int的值,但不关心后两个参数,则可以编写以下代码:

Console.WriteLine("***** Fun with Methods *****");

//This only gets the value for a, and ignores the other two parameters
//  这只获取 a 的值,并忽略其他两个参数
FillTheseValues(out int a, out _, out _);
Console.WriteLine("Int is: {0}", a);

Console.ReadLine();

// Returning multiple output parameters.
// // 返回多个输出参数。
static void FillTheseValues(out int a, out string b, out bool c)
{
    a = 9;
    b = "Enjoy your string.";
    c = true;
}

Note that the called method is still doing the work setting the values for all three parameters; it is just that the last two parameters are being discarded when the method call returns.
请注意,被调用的方法仍在为所有三个参数设置值;只是当方法调用返回时,最后两个参数将被丢弃。

The out Modifier in Constructors and Initializers (New 7.3)

构造函数和初始值设定项中的 out 修饰符(新 7.3)

C# 7.3 extended the allowable locations for using the out parameter. In addition to methods, parameters for constructors, field and property initializers, and query clauses can all be decorated with the out modifier. Examples of these will be examined later in this book.
C#7.3扩展了使用out参数的允许位置。除了方法之外,构造函数的参数、字段和属性初始值设定项以及查询子句都可以用out修饰符进行修饰。这些例子将在本书后面进行研究。

Using the ref Modifier

使用 ref 修饰符

Now consider the use of the C# ref parameter modifier. Reference parameters are necessary when you want to allow a method to operate on (and usually change the values of) various data points declared in the caller’s scope (such as a sorting or swapping routine). Note the distinction between output and reference parameters:
现在考虑使用ref参数修饰符。当您希望允许方法对调用方作用域中声明的各种数据点(如排序或交换例程)进行操作(通常更改其值)时,引用参数是必要的。注意输出参数和引用参数之间的区别:

  • Output parameters do not need to be initialized before they are passed to the method. The reason for this is that the method must assign output parameters before exiting.
    输出参数在传递给方法之前不需要初始化。原因是该方法必须在退出之前分配输出参数。
  • Reference parameters must be initialized before they are passed to the method. The reason for this is that you are passing a reference to an existing variable.
    引用参数必须先初始化,然后才能传递给方法。这样做的原因是您正在传递对现有变量的引用。

If you do not assign it to an initial value, that would be the equivalent of operating on an unassigned local variable.
如果不将其分配给初始值,则等效于对未赋值的局部变量进行操作。

Let’s check out the use of the ref keyword by way of a method that swaps two string variables (of course, any two data types could be used here, including int, bool, float, etc.).
让我们通过交换两个字符串变量的方法检查一下ref 关键字的使用(当然,这里可以使用任何两种数据类型,包括 int、bool、float 等)。

// Reference parameters.
// 引用参数。
public static void SwapStrings(ref string s1, ref string s2)
{
    string tempStr = s1;
    s1 = s2;
    s2 = tempStr;
}

// NET7控制台顶级语句模板中提示public无效,去掉即可。

This method can be called as follows:
可以按如下方式调用此方法:

Console.WriteLine("***** Fun with Methods *****");
string str1 = "Flip";
string str2 = "Flop";
Console.WriteLine("Before: {0}, {1} ", str1, str2);
SwapStrings(ref str1, ref str2);
Console.WriteLine("After: {0}, {1} ", str1, str2);
Console.ReadLine();


// Reference parameters.
// 引用参数。
static void SwapStrings(ref string s1, ref string s2)
{
    string tempStr = s1;
    s1 = s2;
    s2 = tempStr;
}

Here, the caller has assigned an initial value to local string data (str1 and str2). After the call to SwapStrings() returns, str1 now contains the value "Flop", while str2 reports the value "Flip".
在这里,调用方为本地字符串数据(str1 和 str2)分配了一个初始值。调用SwapStrings()返回,str1 现在是 “Flop”,而 str2 是 “Flip”。

***** Fun with Methods *****
Before: Flip, Flop
After: Flop, Flip

Using the in Modifier (New 7.2)

使用 in 修饰符(新版 7.2)

The in modifier passes a value by reference (for both value and reference types) and prevents the called method from modifying the values. This clearly states a design intent in your code, as well as potentially reducing memory pressure. When value types are passed by value, they are copied (internally) by the called method. If the object is large (such as a large struct), the extra overhead of making a copy for local use can be significant. Also, even when reference types are passed without a modifier, they can be modified by the called method. Both issues can be resolved using the in modifier.
in修饰符通过引用传递值(对于值和引用类型),并阻止被调用的方法修改值。这清楚地说明了代码中的设计意图,并可能降低内存压力。当值类型通过值传递时,它们会被调用的方法(内部)复制。如果对象很大(比如一个大结构),那么为本地使用制作副本的额外开销可能会很大。此外,即使在传递引用类型时不带修饰符,它们也可以通过调用的方法进行修改。这两个问题都可以使用in修饰符来解决。

Revisiting the Add() method from earlier, there are two lines of code that modify the parameters, but do not affect the values for the calling method. The values are not affected because the Add() method makes a copy of the variables x and y to use locally. While the calling method does not have any adverse side effects, what if the Add() method was changed to the following code?
重新访问前面的Add()方法,有两行代码可以修改参数,但不会影响调用方法的值。这些值不受影响,因为Add()方法生成变量x和y的副本以供本地使用。虽然调用方法没有任何不利的副作用,但如果Add()方法被更改为以下代码呢?

static int Add2(int x, int y)
{
    x = 10000;
    y = 88888;
    int ans = x + y;
    return ans;
}

Running this code then returns 98888, regardless of the numbers sent into the method. This is obviously a problem. To correct this, update the method to the following:
运行此代码将返回98888,而与发送到方法中的数字无关。这显然是个问题。要更正此问题,请将方法更新为以下内容:

static int AddReadOnly(in int x, in int y)
{
    //Error CS8331 Cannot assign to variable 'in int' because it is a readonly variable
    //x = 10000;
    //y = 88888;
    int ans = x + y;
    return ans;
}

When the code attempts to change the values of the parameters, the compiler raises the CS8331 error, indicating that the values cannot be modified because of the in modifier.
当代码尝试更改参数的值时,编译器会引发 CS8331 错误,指示由于 in 修饰符而无法修改这些值。

Using the params Modifier

使用参数修饰符

C# supports the use of parameter arrays using the params keyword. The params keyword allows you to pass into a method a variable number of identically typed parameters (or classes related by inheritance) as a single logical parameter. As well, arguments marked with the params keyword can be processed if the caller sends in a strongly typed array or a comma-delimited list of items. Yes, this can be confusing! To clear things up, assume you want to create a function that allows the caller to pass in any number of arguments and return the calculated average.
C#支持使用params关键字的参数数组。params关键字允许您将数量可变的同类型参数(或通过继承相关的类)作为单个逻辑参数传递到方法中。此外,如果调用方发送强类型数组或逗号分隔的项目列表,则可以处理用params关键字标记的参数。是的,这可能会让人困惑!为了解决问题,假设您想要创建一个函数,允许调用方传入任意数量的参数并返回计算出的平均值。

If you were to prototype this method to take an array of doubles, this would force the caller to first define the array, then fill the array, and finally pass it into the method. However, if you define CalculateAverage() to take a params of double[] data types, the caller can simply pass a comma-delimited list of doubles. The list of doubles will be packaged into an array of doubles behind the scenes.
如果您要将此方法原型设计为一个double数组,这将强制调用方首先定义数组,然后填充数组,最后将其传递到方法中。但是,如果您将CalculateAverage()定义为采用double[]数据类型的参数,则调用者可以简单地传递一个逗号分隔的doubles列表。替身列表将被打包为一组幕后替身。

// Return average of "some number" of doubles.
// 返回“一些数量”的双精度的平均值。
static double CalculateAverage(params double[] values)
{
    Console.WriteLine("You sent me {0} doubles.", values.Length);
    double sum = 0;
    if (values.Length == 0)
    {
        return sum;
    }
    for (int i = 0; i < values.Length; i++)
    {
        sum += values[i];
    }
    return (sum / values.Length);
}

This method has been defined to take a parameter array of doubles. What this method is in fact saying is “Send me any number of doubles (including zero), and I’ll compute the average.” Given this, you can call CalculateAverage() in any of the following ways:
此方法已被定义为接受一个doubles数组。事实上,这个方法的意思是“给我发送任意数量的doubles(包括零),我会计算平均值。”考虑到这一点,你可以用以下任何一种方式调用CalculateAverage():

Console.WriteLine("***** Fun with Methods *****");
// Pass in a comma-delimited list of doubles...
// 传入以逗号分隔的doubles列表...
double average;
average = CalculateAverage(4.0, 3.2, 5.7, 64.22, 87.2);
Console.WriteLine("Average of data is: {0}", average);

// ...or pass an array of doubles.
// 或者传入doubles数组
double[] data = { 4.0, 3.2, 5.7 };
average = CalculateAverage(data);
Console.WriteLine("Average of data is: {0}", average);

// Average of 0 is 0!
Console.WriteLine("Average of data is: {0}", CalculateAverage());
Console.ReadLine();

// Return average of "some number" of doubles.
// 返回“一些数量”的双精度的平均值。
static double CalculateAverage(params double[] values)
{
    Console.WriteLine("You sent me {0} doubles.", values.Length);
    double sum = 0;
    if (values.Length == 0)
    {
        return sum;
    }
    for (int i = 0; i < values.Length; i++)
    {
        sum += values[i];
    }
    return (sum / values.Length);
}

If you did not make use of the params modifier in the definition of CalculateAverage(), the first invocation of this method would result in a compiler error, as the compiler would be looking for a version of CalculateAverage() that took five double arguments.
如果在CalculateAverage()的定义中没有使用params修饰符,则此方法的第一次调用将导致编译器错误,因为编译器将查找包含五个doubles的CalculateEverage() 版本。

■Note to avoid any ambiguity, C# demands a method support only a single params argument, which must be the final argument in the parameter list.
请注意,为了避免任何歧义,C#要求方法只支持一个params参数,该参数必须是参数列表中的最后一个参数。

As you might guess, this technique is nothing more than a convenience for the caller, given that the array is created by the .NET Core Runtime as necessary. By the time the array is within the scope of the method being called, you can treat it as a full-blown .NET Core array that contains all the functionality of the System.Array base class library type. Consider the following output:
正如您可能猜测的那样,考虑到数组是由.NET Core Runtime根据需要创建的,这种技术只不过是为调用程序提供了便利。当数组位于被调用方法的范围内时,您可以将其视为一个完整的.NET Core数组,它包含System.array基类库类型的所有功能。考虑以下输出:

***** Fun with Methods *****
You sent me 5 doubles.
Average of data is: 32.864
You sent me 3 doubles.
Average of data is: 4.3
You sent me 0 doubles.
Average of data is: 0

Defining Optional Parameters

定义可选参数

C# allows you to create methods that can take optional arguments. This technique allows the caller to invoke a single method while omitting arguments deemed unnecessary, provided the caller is happy with the specified defaults.
C#允许您创建可以采用可选参数的方法。这种技术允许调用方调用单个方法,同时省略被认为不必要的参数,前提是调用方对指定的默认值满意。

To illustrate working with optional arguments, assume you have a method named EnterLogData(), which defines a single optional parameter.
为了说明如何使用可选参数,假设您有一个名为EnterLogData()的方法,该方法定义了单个可选参数。

static void EnterLogData(string message, string owner = "Programmer")
{
    Console.WriteLine("Error: {0}", message);
    Console.WriteLine("Owner of Error: {0}", owner);
}

Here, the final string argument has been assigned the default value of "Programmer" via an assignment within the parameter definition. Given this, you can call EnterLogData() in two ways.
在这里,最终的字符串参数已通过参数定义中的赋值分配了默认值“Programmer”。 鉴于此,您可以通过两种方式调用 EnterLogData()。

Console.WriteLine("***** Fun with Methods *****");

EnterLogData("Oh no! Grid can't find data");
EnterLogData("Oh no! I can't find the payroll data", "CFO");
Console.ReadLine();

static void EnterLogData(string message, string owner = "Programmer")
{
    Console.WriteLine("Error: {0}", message);
    Console.WriteLine("Owner of Error: {0}", owner);
}

Because the first invocation of EnterLogData() did not specify a second string argument, you would find that the programmer is the one responsible for losing data for the grid, while the CFO misplaced the payroll data (as specified by the second argument in the second method call).
由于EnterLogData()的第一次调用没有指定第二个字符串参数,因此您会发现Programmer是负责丢失网格数据的人,而 CFO 错误地放置了payroll data(由第二个方法调用中的第二个参数指定)。

One important thing to be aware of is that the value assigned to an optional parameter must be known at compile time and cannot be resolved at runtime (if you attempt to do so, you will receive compile-time errors!). To illustrate, assume you want to update EnterLogData() with the following extra optional parameter:
需要注意的一件重要事情是,分配给可选参数的值必须在编译时已知,并且不能在运行时解析(如果尝试这样做,将收到编译时错误!)。为了进行说明,假设您想使用以下额外的可选参数更新nterLogData():

// Error! The default value for an optional arg must be known at compile time!
// 错误!在编译时必须知道可选参数的默认值

static void EnterLogData(string message, string owner = "Programmer", DateTime timeStamp = DateTime.Now)
{
    Console.WriteLine("Error: {0}", message);
    Console.WriteLine("Owner of Error: {0}", owner);
    Console.WriteLine("Time of Error: {0}", timeStamp);
}

This will not compile because the value of the Now property of the DateTime class is resolved at runtime, not compile time.
这不会编译,因为 DateTime 类的 Now 属性的值是在运行时解析的,而不是在编译时解析的。

■Note to avoid ambiguity, optional parameters must always be placed at the end of a method signature. it is a compiler error to have optional parameters listed before nonoptional parameters.
注意 为避免歧义,可选参数必须始终放在方法签名的末尾。在非可选参数之前列出可选参数会出现编译器错误。

Using Named Arguments (Updated 7.2)

使用命名参数(7.2 更新)

Another language feature found in C# is support for named arguments. Named arguments allow you to invoke a method by specifying parameter values in any order you choose. Thus, rather than passing parameters solely by position (as you will do in most cases), you can choose to specify each argument by name using a colon operator. To illustrate the use of named arguments, assume you have added the following method to the Program.cs file:
C#中的另一个语言特性是支持命名参数。命名实参允许您通过按选择的任何顺序指定参数值来调用方法。因此,您可以选择使用冒号运算符按名称指定每个参数,而不是像在大多数情况下那样仅按位置传递参数。为了说明命名参数的使用,假设您已将以下方法添加到Program.cs文件中:

static void DisplayFancyMessage(ConsoleColor textColor,
    ConsoleColor backgroundColor, string message)
{
    // Store old colors to restore after message is printed.
    // 存储旧颜色以在打印消息后恢复。
    ConsoleColor oldTextColor = Console.ForegroundColor;
    ConsoleColor oldbackgroundColor = Console.BackgroundColor;
    // Set new colors and print message.
    // 设置新颜色并打印消息。
    Console.ForegroundColor = textColor;
    Console.BackgroundColor = backgroundColor;
    Console.WriteLine(message);

    // Restore previous colors.
    // 恢复以前的颜色。
    Console.ForegroundColor = oldTextColor;
    Console.BackgroundColor = oldbackgroundColor;
}

Now, the way DisplayFancyMessage() was written, you would expect the caller to invoke this method by passing two ConsoleColor variables followed by a string type. However, using named arguments, the following calls are completely fine:
现在,按照DisplayFancyMessage()的编写方式,你会期望调用方通过传递两个 ConsoleColor 变量后跟一个字符串类型来调用此方法。但是,使用命名参数,以下调用完全没问题:

Console.WriteLine("***** Fun with Methods *****");
DisplayFancyMessage(message: "Wow! Very Fancy indeed!",textColor: ConsoleColor.DarkRed,backgroundColor: ConsoleColor.White);
DisplayFancyMessage(backgroundColor: ConsoleColor.Green,message: "Testing...",textColor: ConsoleColor.DarkBlue);
Console.ReadLine();

static void DisplayFancyMessage(ConsoleColor textColor,
ConsoleColor backgroundColor, string message)
{
    // Store old colors to restore after message is printed.
    // 存储旧颜色以在打印消息后恢复。
    ConsoleColor oldTextColor = Console.ForegroundColor;
    ConsoleColor oldbackgroundColor = Console.BackgroundColor;
    // Set new colors and print message.
    // 设置新颜色并打印消息。
    Console.ForegroundColor = textColor;
    Console.BackgroundColor = backgroundColor;
    Console.WriteLine(message);

    // Restore previous colors.
    // 恢复以前的颜色。
    Console.ForegroundColor = oldTextColor;
    Console.BackgroundColor = oldbackgroundColor;
}

The rules for using named arguments were updated slightly with C# 7.2. Prior to 7.2, if you begin to invoke a method using positional parameters, you must list them before any named parameters. With 7.2 and later versions of C#, named and unnamed parameters can be mingled if the parameters are in the correct position.
使用命名参数的规则在 C# 7.2 中略有更新。在 7.2 之前,如果开始使用位置参数调用方法,则必须在任何命名参数之前列出它们。对于 7.2 及更高版本的 C#,如果参数位于正确的位置,则可以混合命名和未命名参数。

■Note Just because you can mix and match named arguments with positional arguments in C# 7.2 and later, it’s not considered a good idea. Just because you can does not mean you should!
注意 仅仅因为您可以在 C# 7.2 及更高版本中将命名参数与位置参数混合和匹配,就不是一个好主意。可以这样做但没必要这样做。

The following code is an example:
以下代码是一个示例:

// This is OK, as positional args are listed before named args.
// 这是正确的,因为位置参数列在命名参数之前。
DisplayFancyMessage(ConsoleColor.Blue,message: "Testing...",backgroundColor: ConsoleColor.White);

// This is OK, all arguments are in the correct order
// 这是正确的,所有参数的顺序都正确
DisplayFancyMessage(textColor: ConsoleColor.White, backgroundColor: ConsoleColor.Blue, "Testing...");

// This is an ERROR, as positional args are listed after named args.
// 这是一个错误,因为位置参数列在命名参数之后。
DisplayFancyMessage(message: "Testing...",backgroundColor: ConsoleColor.White,ConsoleColor.Blue);

This restriction aside, you might still be wondering when you would ever want to use this language feature. After all, if you need to specify three arguments to a method, why bother flipping around their positions?
撇开此限制不谈,您可能仍然想知道何时要使用此语言功能。毕竟,如果您需要为一个方法指定三个参数,为什么要费心翻转它们的位置呢?

Well, as it turns out, if you have a method that defines optional arguments, this feature can be helpful.
好吧,事实证明,如果您有一个定义可选参数的方法,则此功能可能会有所帮助。

Assume DisplayFancyMessage() has been rewritten to now support optional arguments, as you have assigned fitting defaults.
假设 DisplayFancyMessage() 已被重写为现在支持可选参数,因为您已经分配了拟合默认值。

static void DisplayFancyMessage(ConsoleColor textColor = ConsoleColor.Blue,
    ConsoleColor backgroundColor = ConsoleColor.White,
string message = "Test Message")
{
    ...
}

Given that each argument has a default value, named arguments allow the caller to specify only the parameters for which they do not want to receive the defaults. Therefore, if the caller wants the value "Hello!" to appear in blue text surrounded by a white background, they can simply specify the following:
假设每个参数都有一个默认值,则命名参数允许调用方仅指定不希望接收默认值的参数。因此,如果调用者希望值“Hello!”以白色背景包围的蓝色文本显示,他们可以简单地指定以下内容:

DisplayFancyMessage(message: "Hello!");

Or, if the caller wants to see “Test Message” print out with a green background containing blue text, they can invoke the following:
或者,如果调用方希望看到“测试消息”打印出来,背景为包含蓝色文本,他们可以调用以下内容:

DisplayFancyMessage(backgroundColor: ConsoleColor.Green);

As you can see, optional arguments and named parameters tend to work hand in hand. To wrap up your examination of building C# methods, I need to address the topic of method overloading.
正如您所看到的,可选参数和命名参数往往是协同工作的。为了结束您对构建C#方法的研究,我需要讨论方法重载的主题。

Understanding Method Overloading

了解方法重载
Like other modern object-oriented languages, C# allows a method to be overloaded. Simply put, when you define a set of identically named methods that differ by the number (or type) of parameters, the method in question is said to be overloaded.
与其他现代面向对象语言一样,C# 允许重载方法。简单地说,当您定义一组名称相同的方法时,这些方法因参数的数量(或类型)而异,则称为重载。

To understand why overloading is so useful, consider life as an old-school Visual Basic 6.0 (VB6) developer. Assume you are using VB6 to build a set of methods that return the sum of various incoming data types (Integers, Doubles, etc.). Given that VB6 does not support method overloading, you would be required to define a unique set of methods that essentially do the same thing (return the sum of the arguments).
要理解为什么重载如此有用,请考虑作为老式 Visual Basic 6.0 (VB6) 开发人员的生活。假设您正在使用 VB6 构建一组方法,这些方法返回各种传入数据类型(整数s、双精度s 等)的总和。鉴于 VB6 不支持方法重载,您需要定义一组本质上执行相同操作的唯一方法(返回参数的总和)。

REM VB6 code examples.
Public Function AddInts(ByVal x As Integer, ByVal y As Integer) As Integer
    AddInts = x + y
End Function
Public Function AddDoubles(ByVal x As Double, ByVal y As Double) As Double
    AddDoubles = x + y
End Function
Public Function AddLongs(ByVal x As Long, ByVal y As Long) As Long
    AddLongs = x + y
End Function

Not only can code such as this become tough to maintain, but the caller must now be painfully aware of the name of each method. Using overloading, you can allow the caller to call a single method named Add(). Again, the key is to ensure that each version of the method has a distinct set of arguments (methods differing only by return type are not unique enough).
像这样的代码不仅会变得难以维护,而且调用者现在必须痛苦地意识到每个方法的名称。使用重载,可以允许调用方调用名为Add()的单个方法。同样,关键是要确保方法的每个版本都有一组不同的参数(仅返回类型不同的方法不够唯一)。

■Note as will be explained in Chapter 10, it is possible to build generic methods that take the concept of overloading to the next level. using generics, you can define type placeholders for a method implementation that are specified at the time you invoke the member in question.
请注意,正如将在第 10 章中解释的那样,可以构建将重载概念提升到下一个级别的泛型方法。使用泛型,可以为调用相关成员时指定的方法实现定义类型占位符。

To check this out firsthand, create a new Console Application project named FunWithMethodOverloading. Add a new class named AddOperations.cs, and update the code to the following:
若要直接检查这一点,请创建一个名为 FunWithMethodOverload 的新控制台应用程序项目。添加一个名为AddOperations.cs的新类,并将代码更新为以下内容:

namespace FunWithMethodOverloading;
// C# code.
// Overloaded Add() method.
public static class AddOperations
{
    // Overloaded Add() method.
    // 重载的Add()方法。
    public static int Add(int x, int y)
    {
        return x + y;
    }
    public static double Add(double x, double y)
    {
        return x + y;
    }
    public static long Add(long x, long y)
    {
        return x + y;
    }
}

Replace the code in the Program.cs file with the following:
将 Program.cs 文件中的代码替换为以下内容:

using static FunWithMethodOverloading.AddOperations;
Console.WriteLine("***** Fun with Method Overloading *****\n");
// Calls int version of Add()
// 调用int版本的Add()
Console.WriteLine(Add(10, 10));

// Calls long version of Add() (using the new digit separator)
// 调用long版本的Add()(使用新的数字分隔符)
Console.WriteLine(Add(900_000_000_000, 900_000_000_000));

// Calls double version of Add()
// 调用double版本的Add()
Console.WriteLine(Add(4.3, 4.4));
Console.ReadLine();

■ Note the using static statement will be covered in Chapter 5. For now, consider it a keyboard shortcut for using methods containing a static class named AddOperations in the FunWithMethodOverloading namespace.
请注意,使用 static 语句将在第 5 章中介绍。现在,将其视为键盘快捷键。方便调用FunWithMethodOverload命名空间中包含名为AddOperations 的静态类的方法。

The top-level statements called three different versions of the Add method, each using a different data type.
顶级语句调用 Add 方法的三个不同版本,每个版本使用不同的数据类型。

Both Visual Studio and Visual Studio Code help when calling overloaded methods to boot. When you type in the name of an overloaded method (such as your good friend Console.WriteLine()), IntelliSense will list each version of the method in question. Note that you can cycle through each version of an overloaded method using the up and down arrow keys, as indicated in Figure 4-1 (Visual Studio) and Figure 4-2 (Visual Studio Code).
Visual Studio 和 Visual Studio Code 在调用重载方法进行引导时都有帮助。当您键入重载方法的名称(例如 Console.WriteLine())时,IntelliSense 将列出相关方法的每个版本。请注意,您可以使用向上和向下箭头键循环浏览重载方法的每个版本,如图 4-1 (Visual Studio) 和图 4-2(Visual Studio 代码)所示。

Alt text

Figure 4-1. Visual Studio IntelliSense for overloaded methods
图 4-1。 Visual Studio 重载方法提示

Alt text
Figure 4-2. Visual Studio Code IntelliSense for overloaded methods
图 4-2。 Visual Studio Code 重载方法提示

If your overload has optional parameters, then the compiler will pick the method that is the best match for the calling code, based on named and/or positional arguments. Add the following method:
如果重载具有可选参数,则编译器将根据命名和(或)位置参数选择与调用代码最匹配的方法。添加以下方法:

static int Add(int x, int y, int z = 0)
{
    return x + (y * z);
}

If the optional argument is not passed in by the caller, the compiler will match the first signature (the one without the optional parameter). While there is a rule set for method location, it is generally not a good idea to create methods that differ only on the optional parameters.
如果调用方未传入可选参数,则编译器将匹配第一个签名(没有可选参数的签名)。虽然存在方法位置的规则集,但创建仅在可选参数上不同的方法通常不是一个好主意。

Finally, in, ref, and out are not considered as part of the signature for method overloading when more than one modifier is used. In other words, the following overloads will throw a compiler error:
最后,当使用多个修饰符时,in、ref 和 out 不被视为方法重载签名的一部分。换句话说,以下重载将引发编译器错误:

static int Add(ref int x) { /* */ }
static int Add(out int x) { /* */ }

However, if only one method uses in, ref, or out, the compiler can distinguish between the signatures.
但是,如果只有一个方法使用 in、ref 或 out,编译器可以区分签名。

So, this is allowed:
因此,这是允许的:

static int Add(ref int x) { /* */ }
static int Add(int x) { /* */ }

That wraps up the initial examination of building methods using the syntax of C#. Next, let’s check out how to build and manipulate enumerations and structures.
以上结束了使用 C# 语法对构建方法的初步检查。接下来,让我们看看如何生成和操作枚举和结构。

Checking Parameters for Null (Updated 10.0)

检查参数是否为 null(10.0 更新)

If a method parameter is nullable (e.g., a reference type–like string) and required by the method body, it is considered a good programming practice to check that the parameter is not null before using it. If it is null, the method should throw an ArgumentNullException. Consider the following update to the EnterLogData() method that does just that (change is in bold):
如果方法参数可以为null(例如,类似引用类型的字符串)并且是方法体所必需的,则在使用该参数之前检查该参数是否为null被认为是一种良好的编程习惯。如果该参数为null,则该方法应引发ArgumentNullException。考虑一下EnterLogData()方法的以下更新,该方法就是这样做的(更改以粗体显示):

static void EnterLogData(string message, string owner = "Programmer")
{
    if (message == null)
    {
        throw new ArgumentNullException(message);
    }
    Console.WriteLine("Error: {0}", message);
    Console.WriteLine("Owner of Error: {0}", owner);
}

Introduced in C# 10, the ArgumentNullException has an extension method to do this in one line of code:
在 C# 10 中引入的 ArgumentNullException 有一个扩展方法,可以在一行代码中执行此操作:

static void EnterLogData(string message, string owner = "Programmer")
{
    ArgumentNullException.ThrowIfNull(message);
    Console.WriteLine("Error: {0}", message);
    Console.WriteLine("Owner of Error: {0}", owner);
}

■ Note exceptions are covered in Chapter 7, and extension methods are covered in Chapter 11.
注意 程序异常在第7章中介绍,扩展方法在第11章中介绍。

Enabling nullable reference types (covered later in this chapter) helps to ensure required reference types are not null.
启用可为 null 的引用类型(本章后面将介绍)有助于确保所需的引用类型不为 null。

Understanding the enum Type

了解枚举类型

Recall from Chapter 1 that the .NET Core type system is composed of classes, structures, enumerations, interfaces, and delegates. To begin exploration of these types, let’s check out the role of the enumeration (or simply, enum) using a new Console Application project named FunWithEnums.
回想一下第 1 章,.NET Core 类型系统由类、结构、枚举、接口和委托组成。若要开始探索这些类型,让我们使用名为 FunWithEnums 的新控制台应用程序项目检查枚举(或简单地称为枚举)的作用。

■ Note Do not confuse the term enum with enumerator; they are completely different concepts. an enum is a custom data type of name-value pairs. an enumerator is a class or structure that implements a .net Core interface named IEnumerable. typically, this interface is implemented on collection classes, as well as the System.Array class. as you will see in Chapter 8, objects that support IEnumerable can work within the foreach loop.
注意 不要将术语枚举与枚举器混淆;它们是完全不同的概念。枚举是名称-值对的自定义数据类型。枚举器是实现名为 IEnumerable 的 .NET Core 接口的类或结构。通常,此接口在集合类以及 System.Array 类上实现。 正如您将在第 8 章中看到的,支持 IEnumerable 的对象可以在 foreach 循环中工作。

When building a system, it is often convenient to create a set of symbolic names that map to known numerical values. For example, if you are creating a payroll system, you might want to refer to the type of employees using constants such as vice president, manager, contractor, and grunt. C# supports the notion of custom enumerations for this very reason. For example, here is an enumeration named EmpTypeEnum (you can define this in the same file as your top-level statements, if it is placed at the end of the file):
构建系统时,创建一组映射到已知数值的符号名称通常很方便。例如,如果要创建工资单系统,则可能需要使用常量(如副总裁、经理、承包商和咕噜声)来引用员工类型。正是出于这个原因,C# 支持自定义枚举的概念。例如,下面是一个名为 EmpTypeEnum 的枚举(如果它放在文件末尾,则可以在与顶级语句相同的文件中定义它):

Console.WriteLine("** Fun with Enums ***\n"); Console.ReadLine();

//local functions go here:

// A custom enumeration. enum EmpTypeEnum
{
Manager, // = 0
Grunt, // = 1 Contractor, // = 2 VicePresident // = 3
}

■ Note By convention, enum types are usually suffixed with Enum. this is not necessary but makes for more readable code.
注意 按照惯例,枚举类型通常以 Enum 为后缀。这不是必需的,但会使代码更具可读性。

The EmpTypeEnum enumeration defines four named constants, corresponding to discrete numerical values. By default, the first element is set to the value zero (0), followed by an n+1 progression. You are free to change the initial value as you see fit. For example, if it made sense to number the members of EmpTypeEnum as 102 through 105, you could do so as follows:
枚举定义了四个命名常量,对应于离散数值。默认情况下,第一个元素设置为值零 (0),后跟 n+1 级数。您可以根据需要自由更改初始值。例如,如果将 EmpTypeEnum 的成员编号为 102 到 105 是有意义的,则可以按如下方式执行此操作:

// Begin with 102. enum EmpTypeEnum
{
Manager = 102,
Grunt, // = 103 Contractor, // = 104 VicePresident // = 105
}

Enumerations do not necessarily need to follow a sequential ordering and do not need to have unique values. If (for some reason or another) it makes sense to establish your EmpTypeEnum as shown here, the compiler continues to be happy:
枚举不一定需要遵循顺序排序,也不需要具有唯一值。如果(出于某种原因)建立 EmpTypeEnum 是有意义的,如下所示,编译器会继续很高兴:

// Elements of an enumeration need not be sequential!
// 枚举的元素不必是连续的!
enum EmpTypeEnum
{
Manager = 10,
Grunt = 1,
Contractor = 100,
VicePresident = 9
}

Controlling the Underlying Storage for an enum

控制枚举的基础存储

By default, the storage type used to hold the values of an enumeration is a System.Int32 (the C# int); however, you are free to change this to your liking. C# enumerations can be defined in a similar manner for any of the core system types (byte, short, int, or long). For example, if you want to set the underlying storage value of EmpTypeEnum to be a byte rather than an int, you can write the following:
默认情况下,用于保存枚举值的存储类型是 System.Int32(C# int);但是,您可以根据自己的喜好自由更改此设置。C# 枚举可以以类似的方式为任何核心系统类型(字节、短整型、整型或整型)定义。例如,如果要将 EmpTypeEnum 的基础存储值设置为字节而不是整数,则可以编写以下内容:

// This time, EmpTypeEnum maps to an underlying byte.
这一次,EmpTypeEnum 映射到底层字节。

enum EmpTypeEnum : byte
{
Manager = 10,
Grunt = 1,
Contractor = 100,
VicePresident = 9
}

Changing the underlying type of an enumeration can be helpful if you are building a .NET Core application that will be deployed to a low-memory device and need to conserve memory wherever possible. Of course, if you do establish your enumeration to use a byte as storage, each value must be within its range! For example, the following version of EmpTypeEnum will result in a compiler error, as the value 999 cannot fit within the range of a byte:
如果要生成将部署到低内存设备的 .NET Core 应用程序,并且需要尽可能节省内存,则更改枚举的基础类型会很有帮助。当然,如果您确实建立了使用字节作为存储的枚举,则每个值都必须在其范围内!例如,以下版本的 EmpTypeEnum 将导致编译器错误,因为值 999 无法容纳在字节范围内:

// Compile-time error! 999 is too big for a byte!
// 编译时错误!999对于一个字节来说太大了!
enum EmpTypeEnum : byte
{
Manager = 10,
Grunt = 1,
Contractor = 100,
VicePresident = 999
}

Declaring enum Variables

声明枚举变量

Once you have established the range and storage type of your enumeration, you can use it in place of so- called magic numbers. Because enumerations are nothing more than a user-defined data type, you can use them as function return values, method parameters, local variables, and so forth. Assume you have a
method named AskForBonus(), taking an EmpTypeEnum variable as the sole parameter. Based on the value of the incoming parameter, you will print out a fitting response to the pay bonus request.
确定枚举的范围和存储类型后,可以使用它代替所谓的幻数。由于枚举只不过是用户定义的数据类型,因此可以将它们用作函数返回值、方法参数、局部变量等。假设您有一个名为 AskForBonus() 的方法,将 EmpTypeEnum 变量作为唯一参数。根据传入参数的值,您将打印出对支付奖金请求的合适响应。

Console.WriteLine("** Fun with Enums ***");
// Make an EmpTypeEnum variable. EmpTypeEnum emp = EmpTypeEnum.Contractor; AskForBonus(emp);
Console.ReadLine();

// Enums as parameters.
static void AskForBonus(EmpTypeEnum e)
{
switch (e)
{
case EmpTypeEnum.Manager:

Console.WriteLine("How about stock options instead?"); break;
case EmpTypeEnum.Grunt:
Console.WriteLine("You have got to be kidding…"); break;
case EmpTypeEnum.Contractor:
Console.WriteLine("You already get enough cash…"); break;
case EmpTypeEnum.VicePresident: Console.WriteLine("VERY GOOD, Sir!"); break;
}
}

Notice that when you are assigning a value to an enum variable, you must scope the enum name (EmpTypeEnum) to the value (Grunt). Because enumerations are a fixed set of name-value pairs, it is illegal to set an enum variable to a value that is not defined directly by the enumerated type.
请注意,为枚举变量赋值时,必须将枚举名称 (EmpTypeEnum) 的作用域限定为值 (Grunt)。由于枚举是一组固定的名称-值对,因此将枚举变量设置为不是由枚举类型直接定义的值是非法的。

static void ThisMethodWillNotCompile()
{
// Error! SalesManager is not in the EmpTypeEnum enum! EmpTypeEnum emp = EmpTypeEnum.SalesManager;

// Error! Forgot to scope Grunt value to EmpTypeEnum enum! emp = Grunt;
}

Using the System.Enum Type

使用 System.Enum 类型

The interesting thing about .NET Core enumerations is that they gain functionality from the System. Enum class type. This class defines several methods that allow you to interrogate and transform a given enumeration. One helpful method is the static Enum.GetUnderlyingType(), which, as the name implies,
returns the data type used to store the values of the enumerated type (System.Byte in the case of the current
EmpTypeEnum declaration).
关于 .NET Core 枚举的有趣之处在于它们从系统获得功能。枚举类类型。此类定义了几个方法,允许您查询和转换给定的枚举。一个有用的方法是静态 Enum.GetUnderlyingType(),顾名思义,返回用于存储枚举类型值的数据类型(在当前的情况下为 System.Byte EmpTypeEnum 声明)。

Console.WriteLine("** Fun with Enums ***");

// Print storage for the enum. Console.WriteLine("EmpTypeEnum uses a {0} for storage",
Enum.GetUnderlyingType(emp.GetType())); Console.ReadLine();

The Enum.GetUnderlyingType() method requires you to pass in a System.Type as the first parameter.

As fully examined in Chapter 17, Type represents the metadata description of a given .NET Core entity.
如第 17 章所述,Type 表示给定 .NET Core 实体的元数据描述。

One possible way to obtain metadata (as shown previously) is to use the GetType() method, which is common to all types in the .NET Core base class libraries. Another approach is to use the C# typeof operator. One benefit of doing so is that you do not need to have a variable of the entity you want to obtain a metadata description of.
获取元数据的一种可能方法是使用 GetType() 方法,该方法对于 .NET Core 基类库中的所有类型都是通用的。另一种方法是使用 C# 类型算子。这样做的一个好处是,您不需要具有要获取其元数据描述的实体的变量。

// This time use typeof to extract a Type. Console.WriteLine("EmpTypeEnum uses a {0} for storage",
Enum.GetUnderlyingType(typeof(EmpTypeEnum)));

Dynamically Discovering an enum’s Name-Value Pairs

动态发现枚举的名称-值对

Beyond the Enum.GetUnderlyingType() method, all C# enumerations support a method named ToString(), which returns the string name of the current enumeration’s value. The following code is an example:
除了 Enum.GetUnderlyingType() 方法之外,所有 C# 枚举都支持名为 ToString() 的方法,该方法返回当前枚举值的字符串名称。以下代码是一个示例:

EmpTypeEnum emp = EmpTypeEnum.Contractor;

// Prints out "emp is a Contractor".
// 打印出“emp 是承包商”。
Console.WriteLine("emp is a {0}.", emp.ToString()); Console.ReadLine();

If you are interested in discovering the value of a given enumeration variable, rather than its name, you can simply cast the enum variable against the underlying storage type. The following is an example:
如果有兴趣发现给定枚举变量的值,而不是其名称,只需将枚举变量强制转换为基础存储类型即可。下面是一个示例:

Console.WriteLine("** Fun with Enums ***"); EmpTypeEnum emp = EmpTypeEnum.Contractor;

// Prints out "Contractor = 100".
Console.WriteLine("{0} = {1}", emp.ToString(), (byte)emp); Console.ReadLine();

■ Note the static Enum.Format() method provides a finer level of formatting options by specifying a desired format flag. Consult the documentation for a full list of formatting flags.
请注意,静态 Enum.Format() 方法通过指定所需的格式标志来提供更精细的格式选项。有关格式标志的完整列表,请参阅文档。

System.Enum also defines another static method named GetValues(). This method returns an instance of System.Array. Each item in the array corresponds to a member of the specified enumeration. Consider the following method, which will print out each name-value pair within any enumeration you pass in as a parameter:
System.Enum 还定义了另一个名为 GetValues() 的静态方法。此方法返回 System.Array 的实例。数组中的每个项对应于指定枚举的成员。请考虑以下方法,该方法将打印出作为参数传入的任何枚举中的每个名称-值对:

// This method will print out the details of any enum.
// 此方法将打印出任何枚举的详细信息。
static void EvaluateEnum(System.Enum e)
{
Console.WriteLine("=> Information about {0}", e.GetType().Name);

Console.WriteLine("Underlying storage type: {0}", Enum.GetUnderlyingType(e.GetType()));

// Get all name-value pairs for incoming parameter. Array enumData = Enum.GetValues(e.GetType());
Console.WriteLine("This enum has {0} members.", enumData.Length);

// Now show the string name and associated value, using the D format
// flag (see Chapter 3).
for(int i = 0; i < enumData.Length; i++)

{
Console.WriteLine("Name: {0}, Value: {0:D}", enumData.GetValue(i));
}
}

To test this new method, update your code to create variables of several enumeration types declared in the System namespace (as well as an EmpTypeEnum enumeration for good measure). The following code is an example:
若要测试此新方法,请更新代码以创建在 System 命名空间中声明的多个枚举类型的变量(以及用于良好度量的 EmpTypeEnum 枚举)。以下代码是一个示例:

Console.WriteLine("** Fun with Enums ***");

EmpTypeEnum e2 = EmpTypeEnum.Contractor;

// These types are enums in the System namespace. DayOfWeek day = DayOfWeek.Monday;
ConsoleColor cc = ConsoleColor.Gray;

EvaluateEnum(e2);
EvaluateEnum(day);
EvaluateEnum(cc);
Console.ReadLine();

Some partial output is shown here:
此处显示了一些部分输出:
=> Information about DayOfWeek

Underlying storage type: System.Int32 This enum has 7 members.

Name: Sunday, Value: 0 Name: Monday, Value: 1 Name: Tuesday, Value: 2 Name: Wednesday, Value: 3 Name: Thursday, Value: 4 Name: Friday, Value: 5 Name: Saturday, Value: 6

As you will see over the course of this text, enumerations are used extensively throughout the .NET Core base class libraries. When you make use of any enumeration, always remember that you can interact with the name-value pairs using the members of System.Enum.
正如您将在本文中看到的那样,枚举在整个 .NET Core 基类库中被广泛使用。使用任何枚举时,请始终记住,可以使用 System.Ene 的成员与名称-值对进行交互。

Using Enums, Flags, and Bitwise Operations

使用枚举、标志和按位运算

Bitwise operations provide a fast mechanism for operating on binary numbers at the bit level. Table 4-3 contains the C# bitwise operators, what they do, and an example of each.
按位运算提供了一种在位级别对二进制数进行操作的快速机制。表 4-3 包含 C# 按位运算符、它们的作用以及每个运算符的示例。

Table 4-3. Bitwise Operations
表 4-3. 按位运算

Operator Operation Example
& (AND) Copies a bit if it exists in both operands
复制位(如果两个操作数中都存在)
0110 & 0100 = 0100 (4)
| (OR) Copies a bit if it exists in both operands
复制位(如果两个操作数中都存在)
0110 | 0100 = 0110 (6)
^ (XOR) Copies a bit if it exists in one but not both operands
如果位存在于一个操作数中,则复制位,但不是两个操作数
0110 ^ 0100 = 0010 (2)
~ (ones’ compliment) Flips the bits
翻转位
~0110 = -7 (due to overflow)
<< (left shift) Shifts the bits left
向左移动位
0110 << 1 = 1100 (12)
>> (right shift) Shifts the bits right
向右移动位
0110 >> 1 = 0011 (3)

To show these in action, create a new Console Application project named FunWithBitwiseOperations.
若要在操作中显示这些内容,请创建一个名为 FunWithBitwiseOperations的新控制台应用程序项目。

Update the Program.cs file to the following code:
将程序.cs文件更新为以下代码:

using FunWithBitwiseOperations; Console.WriteLine("===== Fun wih Bitwise Operations");
Console.WriteLine("6 & 4 = {0} | {1}", 6 & 4, Convert.ToString((6 & 4),2));
Console.WriteLine("6 | 4 = {0} | {1}", 6 | 4, Convert.ToString((6 | 4),2));
Console.WriteLine("6 ^ 4 = {0} | {1}", 6 ^ 4, Convert.ToString((6 ^ 4),2));
Console.WriteLine("6 << 1 = {0} | {1}", 6 << 1, Convert.ToString((6 << 1),2));
Console.WriteLine("6 >> 1 = {0} | {1}", 6 >> 1, Convert.ToString((6 >> 1),2)); Console.WriteLine("~6 = {0} | {1}", ~6, Convert.ToString(~((short)6),2)); Console.WriteLine("Int.MaxValue {0}", Convert.ToString((int.MaxValue),2)); Console.readLine();

When you execute the code, you will see the following result:
执行代码时,您将看到以下结果:

===== Fun wih Bitwise Operations 6 & 4 = 4 | 100
6 | 4 = 6 | 110
6 ^ 4 = 2 | 10
6 << 1 = 12 | 1100
6 >> 1 = 3 | 11
~6 = -7 | 11111111111111111111111111111001
Int.MaxValue 1111111111111111111111111111111

Now that you know the basics of bitwise operations, it is time to apply them to enums. Add a new file named ContactPreferenceEnum.cs and update the code to the following:
现在您已经了解了按位运算的基础知识,是时候将它们应用于枚举了。添加一个名为 ContactPreferenceEnum.cs 的新文件,并将代码更新为以下内容:

namespace FunWithBitwiseOperations
{
[Flags]
public enum ContactPreferenceEnum
{
None = 1,
Email = 2,
Phone = 4,
Ponyexpress = 6
}
}

Notice the Flags attribute. This allows multiple values from an enum to be combined into a single variable. For example, Email and Phone can be combined like this:
请注意“标志”属性。这允许将枚举中的多个值组合到单个变量中。例如,电子邮件和电话可以像这样组合:

ContactPreferenceEnum emailAndPhone = ContactPreferenceEnum.Email | ContactPreferenceEnum.Phone;

This allows you to check if one of the values exists in the combined value. For example, if you want to check to see which ContactPreference value is in emailAndPhone variable, you can use the following code:
这允许您检查组合值中是否存在其中一个值。例如,如果要检查电子邮件和电话变量中的哪个联系人首选项值,则可以使用以下代码:

Console.WriteLine("None? {0}", (emailAndPhone | ContactPreferenceEnum.None) == emailAndPhone); Console.WriteLine("Email? {0}", (emailAndPhone | ContactPreferenceEnum.Email) == emailAndPhone); Console.WriteLine("Phone? {0}", (emailAndPhone | ContactPreferenceEnum.Phone) == emailAndPhone); Console.WriteLine("Text? {0}", (emailAndPhone | ContactPreferenceEnum.Text) == emailAndPhone);

When executed, the following is presented to the console window:
执行时,将显示以下内容到控制台窗口:

None? False Email? True Phone? True Text? False
没有?虚假电子邮件?真正的电话?真文?假

Understanding the Structure

了解结构

Now that you understand the role of enumeration types, let’s examine the use of .NET Core structures (or simply structs). Structure types are well suited for modeling mathematical, geometrical, and other “atomic” entities in your application. A structure (such as an enumeration) is a user-defined type; however, structures are not simply a collection of name-value pairs. Rather, structures are types that can contain any number of data fields and members that operate on these fields.
现在,你已了解枚举类型的作用,接下来让我们来看看 .NET Core 结构(或简单的结构)的用法。结构类型非常适合对应用程序中的数学、几何和其他“原子”实体进行建模。结构(如枚举)是用户定义的类型;但是,结构不仅仅是名称-值对的集合。相反,结构是可以包含任意数量的数据字段和对这些字段进行操作的成员的类型。

■ Note if you have a background in oop, you can think of a structure as a “lightweight class type,” given that structures provide a way to define a type that supports encapsulation but cannot be used to build a family of related types. they can’t inherit from other class or structure types and can’t be the base of a class. inheritance is covered in Chapter 5. structures can implement interfaces, which are covered in Chapter 8. When you need to build a family of related types through inheritance, you will need to make use of class types.
请注意,如果您有 oop 的背景,则可以将结构视为“轻量级类类型”,因为结构提供了一种定义支持封装但不能用于构建相关类型族的类型的方法。 它们不能从其他类或结构类型继承,也不能是类的基。 继承在第 5 章中介绍。 结构可以实现接口, 第8章对此进行了介绍。当您需要通过继承构建相关类型的族时,您将需要使用类类型。

On the surface, the process of defining and using structures is simple, but as they say, the devil is in the details. To begin understanding the basics of structure types, create a new project named FunWithStructures. In C#, structures are defined using the struct keyword. Define a new structure named Point, which defines two member variables of type int and a set of methods to interact with said data.
从表面上看,定义和使用结构的过程很简单,但正如他们所说,魔鬼在细节中。若要开始了解结构类型的基础知识,请创建一个名为FunWithStructures。在 C# 中,结构是使用 struct 关键字定义的。定义一个名为Point,它定义了两个 int 类型的成员变量和一组与所述数据交互的方法。
struct Point
{
// Fields of the structure. public int X;
public int Y;

// Add 1 to the (X, Y) position. public void Increment()

{
X++; Y++;
}

// Subtract 1 from the (X, Y) position. public void Decrement()
{
X–; Y–;
}

// Display the current position. public void Display()
{
Console.WriteLine("X = {0}, Y = {1}", X, Y);
}
}

Here, you have defined your two integer fields (X and Y) using the public keyword, which is an access control modifier (Chapter 5 continues this discussion). Declaring data with the public keyword ensures the caller has direct access to the data from a given Point variable (via the dot operator).
在这里,您使用 public 关键字定义了两个整数字段(X 和 Y),这是一个访问控制修饰符(第 5 章继续讨论)。使用 public 关键字声明数据可确保调用方可以直接访问来自给定 Point 变量的数据(通过点运算符)。

■ Note it is typically considered bad style to define public data within a class or structure. rather, you will want to define private data, which can be accessed and changed using public properties. these details will be examined in Chapter 5.
请注意,在类或结构中定义公共数据通常被认为是不好的样式。相反,您需要定义私有数据,可以使用公共属性访问和更改这些数据。 这些细节将在第5章中研究。

Here is code that takes the Point type out for a test-drive:
下面是将 Point 类型取出用于体验版的代码:

Console.WriteLine(" A First Look at Structures \n");

// Create an initial Point. Point myPoint;
myPoint.X = 349;
myPoint.Y = 76; myPoint.Display();

// Adjust the X and Y values. myPoint.Increment(); myPoint.Display(); Console.ReadLine();

The output is as you would expect.
输出如您所料。

A First Look at Structures X = 349, Y = 76
X = 350, Y = 77

There are some rules regarding structures. First, a structure can’t inherit from other class or structure types and can’t be the base of a class. Structures can implement interfaces.
有一些关于结构的规则。首先,结构不能从其他类或结构类型继承,也不能是类的基。结构可以实现接口。

Creating Structure Variables

创建结构变量
When you want to create a structure variable, you have a variety of options. Here, you simply create a Point variable and assign each piece of public field data before invoking its members. If you do not assign each piece of public field data (X and Y in this case) before using the structure, you will receive a compiler error.
当您想要创建结构变量时,有多种选择。在这里,您只需创建一个 Point 变量并在调用其成员之前分配每条公共字段数据。如果在使用结构之前未分配每条公共字段数据(在本例中为 X 和 Y),您将收到编译器错误。

// Error! Did not assign Y value. Point p1;
p1.X = 10;
p1.Display();

// OK! Both fields assigned before use. Point p2;
p2.X = 10;
p2.Y = 10;
p2.Display();

As an alternative, you can create structure variables using the C# new keyword, which will invoke the structure’s default constructor. By definition, a default constructor does not take any arguments. The benefit of invoking the default constructor of a structure is that each piece of field data is automatically set to its default value.
或者,可以使用 C# new 关键字创建结构变量,该关键字将调用结构的默认构造函数。根据定义,默认构造函数不接受任何参数。调用结构的默认构造函数的好处是,每段字段数据都自动设置为其默认值。

// Set all fields to default values
// 将所有字段设置为默认值
// using the default constructor.
// 使用默认构造函数。
Point p1 = new Point();

// Prints X=0,Y=0. p1.Display();

Structure Constructors (Updated 10.0)

结构构造函数(10.0 更新)

It is also possible to design a structure with a custom constructor. This allows you to specify the values of field data upon variable creation, rather than having to set each data member field by field. Chapter 5 will provide a detailed examination of constructors; however, to illustrate, update the Point structure with the following code:
也可以使用自定义构造函数设计结构。这允许您在创建变量时指定字段数据的值,而不必逐个字段设置每个数据成员。第5章将提供对构造函数的详细检查;但是,为了说明,请使用以下代码更新 Point 结构:

struct Point
{
// Fields of the structure. public int X;
public int Y;

// A custom constructor.
public Point(int xPos, int yPos)
{

}

}

X = xPos;
Y = yPos;

With this, you could now create Point variables, as follows:
有了这个,您现在可以创建点变量,如下所示:

// Call custom constructor. Point p2 = new Point(50, 60);

// Prints X=50,Y=60. p2.Display();

Prior to C# 10, you could not declare a parameterless (i.e., default) constructor on a structure, as it was provided in the implementation of structure types. Now you can create Point variables, as follows:
在 C# 10 之前,无法在结构上声明无参数(即默认)构造函数,因为它是在结构类型的实现中提供的。现在,您可以创建点变量,如下所示:

// Call custom constructor. Point p2 = new Point(50, 60);

// Prints X=50,Y=60. p2.Display();

Regardless of which constructor you choose to add, prior to C# 10, you could not declare a parameterless (i.e., default) constructor on a structure, as it was provided in the implementation of structure types. Now, this is possible, as long as all value types are assigned a value before the code in the constructor end. With this, you can now update the Point structure to the following:
无论选择添加哪个构造函数,在 C# 10 之前,都无法在结构上声明无参数(即默认)构造函数,因为它是在结构类型的实现中提供的。现在,这是可能的,只要在构造函数中的代码结束之前为所有值类型分配一个值。这样,您现在可以将点结构更新为以下内容:

struct Point
{
//omitted for brevity
//Parameterless constructor public Point()
{
X = 0;
Y= 0;
}
// A custom constructor.
public Point(int xPos, int yPos)
{
X = xPos;
Y = yPos;
}
}

■ Note C# 10 and .net 6 introduce the record struct, which will be covered in Chapter 5.
注意 C# 10 和 .net 6 介绍了记录结构,这将在第 5 章中介绍。

Using Field Initializers (New 10.0)

使用字段初始值设定项(新 10.0)

New in C# 10, structure fields can be initialized when declared. Update the code to the following, which initializes X with a value of 5, and Y with a value of 7:
C# 10 中的新增功能是,结构字段可以在声明时初始化。将代码更新为以下内容,该代码使用值 5 初始化 X,用值 7 初始化 Y:

struct Point
{
// Fields of the structure. public int X = 5;

public int Y = 7;
//omitted for brevity
}

With this update, the parameterless constructor no longer needs to initialize the X and Y fields:
通过此更新,无参数构造函数不再需要初始化 X 和 Y 字段:

struct Point
{
//omitted for brevity
//Parameterless constructor public Point() { }
//omitted for brevity
}

Using Read-Only Structs (New 7.2)

使用只读结构(新 7.2)

Structs can also be marked as read-only if there is a need for them to be immutable. Immutable objects must be set up at construction and because they cannot be changed, can be more performant. When declaring a struct as read-only, all the properties must also be read-only. But you might ask, how can a property be set (as all properties must be on a struct) if it is read-only? The answer is that the value must be set during the construction of the struct.
如果需要结构不可变,也可以将其标记为只读。不可变对象必须在构造时设置,并且由于它们无法更改,因此可以提高性能。将结构声明为只读时,所有属性也必须是只读的。但你可能会问,如果属性是只读的,如何设置属性(因为所有属性都必须在结构上)?答案是必须在结构构造期间设置该值。

Update the point class to the following example:
将点类更新为以下示例:

readonly struct ReadOnlyPoint
{
// Fields of the structure. public int X {get; }
public int Y { get; }

// Display the current position and name. public void Display()
{
Console.WriteLine($"X = {X}, Y = {Y}");
}

public ReadOnlyPoint(int xPos, int yPos)
{
X = xPos;
Y = yPos;
}
}

The Increment and Decrement methods have been removed since the variables are read-only. Notice also the two properties, X and Y. Instead of setting them up as fields, they are created as read-only automatic properties. Automatic properties are covered in Chapter 5.
增量和递减方法已被删除,因为这些变量是只读的。另请注意两个属性 X 和 Y。它们不是设置为字段,而是创建为只读自动属性。自动属性在第 5 章中介绍。

Using Read-Only Members (New 8.0)

使用只读成员(新版 8.0)

New in C# 8.0, you can declare individual fields of a struct as readonly. This is more granular than making the entire struct read-only. The readonly modifier can be applied to methods, properties, and property accessors. Add the following struct code to your file, outside of the Program.cs file:
作为 C# 8.0 中的新增功能,可以将结构的各个字段声明为只读。这比将整个结构设为只读更精细。只读修饰符可以应用于方法、属性和属性访问器。将以下结构代码添加到程序.cs文件之外的文件:

struct PointWithReadOnly
{
// Fields of the structure. public int X;
public readonly int Y; public readonly string Name;

// Display the current position and name. public readonly void Display()
{
Console.WriteLine($"X = {X}, Y = {Y}, Name = {Name}");
}

// A custom constructor.
public PointWithReadOnly(int xPos, int yPos, string name)
{
X = xPos;
Y = yPos;
Name = name;
}
}

To use this new struct, add the following to the top-level statements:
若要使用此新结构,请将以下内容添加到顶级语句中:

PointWithReadOnly p3 =
new PointWithReadOnly(50,60,"Point w/RO"); p3.Display();

Using ref Structs (New 7.2)

使用 ref 结构(新 7.2)

Also added in C# 7.2, the ref modifier can be used when defining a struct. This requires all instances of the struct to be stack allocated and cannot be assigned as a property of another class. The technical reason for this is that ref structs cannot referenced from the heap. The difference between the stack and the heap is covered in the next section.
同样在 C# 7.2 中添加了 ref 修饰符,可以在定义结构时使用。这要求对结构的所有实例进行堆栈分配,并且不能将其分配为另一个类的属性。这样做的技术原因是 ref 结构不能从堆中引用。堆栈和堆之间的区别将在下一节中介绍。

These are some additional limitations of ref structs:
这些是 ref 结构的一些附加限制:
• They cannot be assigned to a variable of type object or dynamic, and they cannot be an interface type.
它们不能分配给对象或动态类型的变量,也不能是接口类型。
• They cannot implement interfaces.
它们无法实现接口。
• They cannot be used as a property of a non-ref struct.
它们不能用作非引用结构的属性。
• They cannot be used in async methods, iterators, lambda expressions, or local functions.
它们不能在异步方法、迭代器、lambda 表达式或本地函数中使用。

The following code, which creates a simple struct and then attempts to create a property in that struct typed to a ref struct, will not compile:
下面的代码创建一个简单的结构,然后尝试在该结构中创建一个属性,键入为 ref 结构,但不会编译:

struct NormalPoint
{
//This does not compile
public PointWithRef PropPointer { get; set; }
}

Note that the readonly and ref modifiers can be combined to gain the benefits and restrictions of both.
请注意,只读和引用修饰符可以组合在一起以获得两者的优点和限制。

Using Disposable ref Structs (New 8.0)

使用一次性引用结构(新版 8.0)

As covered in the previous section, ref structs (and read-only ref structs) cannot implement an interface and therefore cannot implement IDisposable. New in C# 8.0, ref structs and read-only ref structs can be made disposable by adding a public void Dispose() method.
如上一节所述,ref 结构 (和只读 ref 结构)无法实现接口,因此无法实现 IDisposable。C# 8.0 中的新功能是 ,ref 结构和只读 ref 结构可以通过添加公共 void Dispose() 方法一次性使用。

Add the following struct definition to the Program.cs file:
将以下结构定义添加到程序.cs文件中:

ref struct DisposableRefStruct
{
public int X;
public readonly int Y;
public readonly void Display()
{
Console.WriteLine($"X = {X}, Y = {Y}");
}
// A custom constructor.
public DisposableRefStruct(int xPos, int yPos)
{
X = xPos;
Y = yPos;
Console.WriteLine("Created!");
}
public void Dispose()
{
//clean up any resources here Console.WriteLine("Disposed!");
}
}

Next, add the following to the end of the top-level statements to create and dispose of the new struct:
接下来,将以下内容添加到顶级语句的末尾,以创建和释放新结构:

var s = new DisposableRefStruct(50, 60); s.Display();
s.Dispose();

■ Note object lifetime and disposing of objects are covered in depth in Chapter 9.
注意 对象生存期和对象的处置在第 9 章中有深入介绍。

To deepen your understanding of stack and heap allocation, you need to explore the distinction between a .NET Core value type and a .NET Core reference type.
若要加深对堆栈和堆分配的理解,需要探索 .NET Core 值类型和 .NET Core 引用类型之间的区别。

Understanding Value Types and Reference Types

■ Note the following discussion of value types and reference types assumes that you have a background in object-oriented programming. if this is not the case, you might want to skip to the “understanding C# nullable types” section of this chapter and return to this section after you have read Chapters 5 and 6.
请注意,以下关于值类型和引用类型的讨论假定您具有面向对象编程的背景。 如果不是这种情况,您可能需要跳到本章的“了解 C# 可为 null 的类型”部分,并在阅读第 5 章和第 6 章后返回到本节。

Unlike arrays, strings, or enumerations, C# structures do not have an identically named representation in the .NET Core library (i.e., there is no System.Structure class) but are implicitly derived from System. ValueType. The role of System.ValueType is to ensure that the derived type (e.g., any structure) is allocated on the stack, rather than the garbage-collected heap. Simply put, data allocated on the stack can be created and destroyed quickly, as its lifetime is determined by the defining scope. Heap-allocated data, on the other hand, is monitored by the .NET Core garbage collector and has a lifetime that is determined by many factors, which will be examined in Chapter 9.
与数组、字符串或枚举不同,C# 结构在 .NET Core 库中没有同名的表示形式(即,没有 System.Structure 类),而是隐式派生自 System。值类型。System.ValueType 的作用是确保派生类型(例如,任何结构)在堆栈上分配,而不是在垃圾回收堆上分配。简而言之,堆栈上分配的数据可以快速创建和销毁,因为其生命周期由定义范围决定。另一方面,堆分配的数据由 .NET Core 垃圾回收器监视,其生存期由许多因素决定,这些因素将在第 9 章中介绍。

Functionally, the only purpose of System.ValueType is to override the virtual methods defined by System.Object to use value-based versus reference-based semantics. As you might know, overriding is the process of changing the implementation of a virtual (or possibly abstract) method defined within a base class. The base class of ValueType is System.Object. In fact, the instance methods defined by System.
从功能上讲,System.ValueType 的唯一用途是覆盖 System.Object 定义的虚拟方法,以使用基于值的语义与基于引用的语义。您可能知道,重写是更改基类中定义的虚拟(或可能是抽象)方法的实现的过程。ValueType 的基类是 System.Object。实际上,实例方法由系统定义。

ValueType are identical to those of System.Object.
ValueType 与 System.Object 相同。

// Structures and enumerations implicitly extend System.ValueType. public abstract class ValueType : object
{
public virtual bool Equals(object obj); public virtual int GetHashCode(); public Type GetType();
public virtual string ToString();
}

Given that value types are using value-based semantics, the lifetime of a structure (which includes all numerical data types [int, float], as well as any enum or structure) is predictable. When a structure variable falls out of the defining scope, it is removed from memory immediately.
鉴于值类型使用基于值的语义,结构(包括所有数值数据类型 [int、float] 以及任何枚举或结构)的生存期是可预测的。当结构变量超出定义范围时,会立即将其从内存中删除。

// Local structures are popped off
// 本地结构被弹出
// the stack when a method returns.
// 方法返回时的堆栈。
static void LocalValueTypes()
{
// Recall! "int" is really a System.Int32 structure. int i = 0;

// Recall! Point is a structure type. Point p = new Point();
} // "i" and "p" popped off the stack here!

Using Value Types, Reference Types, and the Assignment Operator

使用值类型、引用类型和赋值运算符

When you assign one value type to another, a member-by-member copy of the field data is achieved. In the case of a simple data type such as System.Int32, the only member to copy is the numerical value. However, in the case of your Point, the X and Y values are copied into the new structure variable. To illustrate,
create a new Console Application project named FunWithValueAndReferenceTypes and then copy your previous Point definition into your new namespace. Next, add the following local function to your top-level statements:
将一种值类型分配给另一种值类型时,将实现字段数据的成员副本。对于简单数据类型(如 System.Int32),唯一要复制的成员是数值。但是,对于点,X 和 Y 值将复制到新的结构变量中。为了说明,创建一个名为 FunWithValueAndReferenceType 的新控制台应用程序项目,然后将以前的 Point 定义复制到新命名空间中。接下来,将以下本地函数添加到顶级语句中:

// Assigning two intrinsic value types results in
// 分配两个内部值类型会导致
// two independent variables on the stack.
// 堆栈上的两个自变量。
static void ValueTypeAssignment()
{
Console.WriteLine("Assigning value types\n");

Point p1 = new Point(10, 10); Point p2 = p1;

// Print both points. p1.Display();
p2.Display();

// Change p1.X and print again. p2.X is not changed. p1.X = 100;
Console.WriteLine("\n=> Changed p1.X\n"); p1.Display();
p2.Display();
}

Here, you have created a variable of type Point (named p1) that is then assigned to another Point (p2).
在这里,您创建了一个类型为 Point(名为 p1)的变量,然后将其分配给另一个 Point (p2)。

Because Point is a value type, you have two copies of the Point type on the stack, each of which can be independently manipulated. Therefore, when you change the value of p1.X, the value of p2.X is unaffected.
由于 Point 是值类型,因此堆栈上有两个 Point 类型的副本,每个副本都可以独立操作。因此,当您更改 p1 的值时。X,p2的值。X 不受影响。

Assigning value types X = 10, Y = 10
X = 10, Y = 10
=> Changed p1.X X = 100, Y = 10
X = 10, Y = 10

In stark contrast to value types, when you apply the assignment operator to reference types (meaning all class instances), you are redirecting what the reference variable points to in memory. To illustrate, create a new class type named PointRef that has the same members as the Point structures, beyond renaming the constructor to match the class name.
与值类型形成鲜明对比的是,将赋值运算符应用于引用类型(即所有类实例)时,将引用变量指向内存中的内容重定向。为了进行说明,请创建一个名为 PointRef 的新类类型,该类类型与 Point 结构具有相同的成员,而不是重命名构造函数以匹配类名。

// Classes are always reference types.
// 类始终是引用类型。

class PointRef
{
// Same members as the Point structure…
// 与Point结构相同的成员..
// Be sure to change your constructor name to PointRef!
// 请务必将构造函数名称更改为 PointRef!

public PointRef(int xPos, int yPos)
{
X = xPos;
Y = yPos;
}
}

Now, use your PointRef type within the following new method. Note that beyond using the PointRef class, rather than the Point structure, the code is identical to the ValueTypeAssignment() method.
现在,在以下新方法中使用您的 PointRef 类型。请注意,除了使用 PointRef 之外 类,而不是 Point 结构,代码与 ValueTypeAssignment() 方法相同。

static void ReferenceTypeAssignment()
{
Console.WriteLine("Assigning reference types\n"); PointRef p1 = new PointRef(10, 10);
PointRef p2 = p1;

// Print both point refs.
// 打印两个点引用。

p1.Display();
p2.Display();

// Change p1.X and print again.
// 更改 p1。X 并再次打印。
p1.X = 100;
Console.WriteLine("\n=> Changed p1.X\n"); p1.Display();
p2.Display();
}

In this case, you have two references pointing to the same object on the managed heap. Therefore, when you change the value of X using the p1 reference, p2.X reports the same value. Assuming you have called this new method, your output should look like the following:
在这种情况下,您有两个引用指向托管堆上的同一对象。因此,当您使用 p1 引用更改 X 的值时,p2。X 报告相同的值。假设您已经调用了这个新方法,您的输出应如下所示:

Assigning reference types X = 10, Y = 10
X = 10, Y = 10
=> Changed p1.X X = 100, Y = 10
X = 100, Y = 10

Using Value Types Containing Reference Types

使用包含引用类型的值类型

Now that you have a better feeling for the basic differences between value types and reference types, let’s examine a more complex example. Assume you have the following reference (class) type that maintains an informational string that can be set using a custom constructor:
现在,您对值类型和引用类型之间的基本差异有了更好的了解,让我们看一个更复杂的示例。假设您具有以下引用(类)类型,该类型维护可以使用自定义构造函数设置的信息字符串:

class ShapeInfo
{
public string InfoString; public ShapeInfo(string info)
{

InfoString = info;
}
}

Now assume that you want to contain a variable of this class type within a value type named Rectangle.
现在假定您希望在名为 Rectangle 的值类型中包含此类类型的变量。
To allow the caller to set the value of the inner ShapeInfo member variable, you also provide a custom constructor. Here is the complete definition of the Rectangle type:
若要允许调用方设置内部 ShapeInfo 成员变量的值,还需要提供自定义构造函数。下面是矩形类型的完整定义:

struct Rectangle
{
// The Rectangle structure contains a reference type member. public ShapeInfo RectInfo;

public int RectTop, RectLeft, RectBottom, RectRight;

public Rectangle(string info, int top, int left, int bottom, int right)
{
RectInfo = new ShapeInfo(info); RectTop = top; RectBottom = bottom; RectLeft = left; RectRight = right;
}

public void Display()
{
Console.WriteLine("String = {0}, Top = {1}, Bottom = {2}, " + "Left = {3}, Right = {4}",
RectInfo.InfoString, RectTop, RectBottom, RectLeft, RectRight);
}
}

At this point, you have contained a reference type within a value type. The million-dollar question now becomes “What happens if you assign one Rectangle variable to another?” Given what you already know about value types, you would be correct in assuming that the integer data (which is indeed a structure, System.Int32) should be an independent entity for each Rectangle variable. But what about the internal reference type? Will the object’s state be fully copied, or will the reference to that object be copied? To answer this question, define the following method and invoke it:
此时,您已在值类型中包含引用类型。百万美元的问题现在变成了“如果将一个矩形变量分配给另一个矩形变量会发生什么?鉴于您已经了解的值类型,假设整数数据(实际上是一个结构,System.Int32)应该是每个矩形变量的独立实体,这是正确的。但是内部参考类型呢?是完全复制对象的状态,还是复制对该对象的引用?若要回答此问题,请定义以下方法并调用它:

static void ValueTypeContainingRefType()
{
// Create the first Rectangle.
// 创建第一个矩形。
Console.WriteLine("-> Creating r1");
Rectangle r1 = new Rectangle("First Rect", 10, 10, 50, 50);

// Now assign a new Rectangle to r1.
// 现在为 r1 分配一个新的矩形。
Console.WriteLine("-> Assigning r2 to r1"); Rectangle r2 = r1;

// Change some values of r2.
// 更改 r2 的某些值。
Console.WriteLine("-> Changing values of r2"); r2.RectInfo.InfoString = "This is new info!"; r2.RectBottom = 4444;

// Print values of both rectangles. r1.Display();
r2.Display();
}

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

-> Creating r1

-> Assigning r2 to r1
-> Changing values of r2
String = This is new info!, Top = 10, Bottom = 50, Left = 10, Right = 50 String = This is new info!, Top = 10, Bottom = 4444, Left = 10, Right = 50

As you can see, when you change the value of the informational string using the r2 reference, the r1 reference displays the same value. By default, when a value type contains other reference types, assignment results in a copy of the references. In this way, you have two independent structures, each of which contains a reference pointing to the same object in memory (i.e., a shallow copy). When you want to perform a deep copy, where the state of internal references is fully copied into a new object, one approach is to implement the ICloneable interface (as you will do in Chapter 8).
如您所见,当您使用 r2 引用更改信息字符串的值时,r1 引用将显示相同的值。默认情况下,当值类型包含其他引用类型时,赋值将生成引用的副本。这样,您就有两个独立的结构,每个结构都包含一个指向内存中同一对象的引用(即浅拷贝)。当您想要执行深层复制时,将内部引用的状态完全复制到新对象中,一种方法是实现 ICloneable 接口(如第 8 章中所述)。

Passing Reference Types by Value

按值传递引用类型

As covered earlier in the chapter, reference types or value types can be passed as parameters to methods. However, passing a reference type (e.g., a class) by reference is quite different from passing it by value. To understand the distinction, assume you have a simple Person class defined in a new Console Application project named FunWithRefTypeValTypeParams, defined as follows:
如本章前面所述,引用类型或值类型可以作为参数传递给方法。但是,通过引用传递引用类型(例如,类)与按值传递引用类型完全不同。若要理解这种区别,假设您在名为 FunWithRefTypeValTypeParams 的新控制台应用程序项目中定义了一个简单的 Person 类,定义如下:

class Person
{
public string personName; public int personAge;

// Constructors.
public Person(string name, int age)
{
personName = name; personAge = age;
}
public Person(){}

public void Display()
{
Console.WriteLine("Name: {0}, Age: {1}", personName, personAge);
}
}

Now, what if you create a method that allows the caller to send in the Person object by value (note the lack of parameter modifiers, such as out or ref)?
现在,如果您创建一个允许调用方按值发送 Person 对象的方法(请注意缺少参数修饰符,例如 out 或 ref)怎么办?

static void SendAPersonByValue(Person p)
{
// Change the age of "p"?
// 更改“p”的年龄?
p.personAge = 99;

// Will the caller see this reassignment?
// 呼叫者会看到此重新分配吗?
p = new Person("Nikki", 99);
}

Notice how the SendAPersonByValue() method attempts to reassign the incoming Person reference to a new Person object, as well as change some state data. Now let’s test this method using the following code:
请注意 SendAPersonByValue() 方法如何尝试将传入的 Person 引用重新分配给新的 Person 对象,以及更改一些状态数据。现在,让我们使用以下代码测试此方法:

// Passing ref-types by value.
// 按值传递引用类型。
Console.WriteLine(" Passing Person object by value ");
Person fred = new Person("Fred", 12);
Console.WriteLine("\nBefore by value call, Person is:");
fred.Display();

SendAPersonByValue(fred);
Console.WriteLine("\nAfter by value call, Person is:"); fred.Display();
Console.ReadLine();

The following is the output of this call:
以下是此调用的输出:

Passing Person object by value
Before by value call, Person is:
Name: Fred, Age: 12
After by value call, Person is:
Name: Fred, Age: 99

As you can see, the value of personAge has been modified. This behavior, discussed earlier, should make more sense now that you understand the way reference types work. Given that you were able to change the state of the incoming Person, what was copied? The answer: a copy of the reference to the caller’s object.
如您所见,personAge 的值已被修改。前面讨论的此行为应该更有意义,因为您了解了引用类型的工作方式。既然您能够更改传入人员的状态,则复制了什么?答案:对调用方对象的引用的副本。
Therefore, as the SendAPersonByValue() method is pointing to the same object as the caller, it is possible to alter the object’s state data. What is not possible is to reassign what the reference is pointing to.
因此,由于 SendAPersonByValue() 方法指向与调用方相同的对象,因此可以更改对象的状态数据。不可能的是重新分配引用所指向的内容。

Passing Reference Types by Reference

按引用传递引用类型
Now assume you have a SendAPersonByReference() method, which passes a reference type by reference (note the ref parameter modifier).
现在假设你有一个 SendAPersonByReference() 方法,它通过引用传递引用类型(请注意 ref 参数修饰符)。

static void SendAPersonByReference(ref Person p)
{
// Change some data of "p". p.personAge = 555;

// "p" is now pointing to a new object on the heap! p = new Person("Nikki", 999);
}

As you might expect, this allows complete flexibility of how the callee is able to manipulate the incoming parameter. Not only can the callee change the state of the object, but if it so chooses, it may also reassign the reference to a new Person object. Now ponder the following updated code:
如您所料,这允许被调用方如何操作传入参数的完全灵活性。被调用方不仅可以更改对象的状态,而且如果它选择这样做,它还可以将引用重新分配给新的 Person 对象。现在考虑以下更新的代码:

// Passing ref-types by ref.
// 通过引用传递引用类型。
Console.WriteLine(" Passing Person object by reference ");

Person mel = new Person("Mel", 23); Console.WriteLine("Before by ref call, Person is:"); mel.Display();

SendAPersonByReference(ref mel); Console.WriteLine("After by ref call, Person is:"); mel.Display();
Console.ReadLine();

Notice the following output:
请注意以下输出:
Passing Person object by reference Before by ref call, Person is:
Name: Mel, Age: 23
After by ref call, Person is:
Name: Nikki, Age: 999

As you can see, an object named Mel returns after the call as an object named Nikki, as the method was able to change what the incoming reference pointed to in memory. The golden rule to keep in mind when passing reference types is the following:
如您所见,名为 Mel 的对象在调用后作为名为 Nikki 的对象返回,因为该方法能够更改传入引用在内存中指向的内容。传递引用类型时要记住的黄金法则如下:
• If a reference type is passed by reference, the callee may change the values of the object’s state data, as well as the object it is referencing.
如果引用类型是通过引用传递的,则被调用方可以更改对象的状态数据的值以及它所引用的对象。
• If a reference type is passed by value, the callee may change the values of the object’s state data but not the object it is referencing.
如果引用类型是按值传递的,则被调用方可以更改对象的状态数据的值,但不能更改它所引用的对象的值。

Final Details Regarding Value Types and Reference Types

有关值类型和引用类型的最终详细信息

To wrap up this topic, consider the information in Table 4-4, which summarizes the core distinctions between value types and reference types.
为了总结本主题,请考虑表 4-4 中的信息,其中总结了值类型和引用类型之间的核心区别。

Table 4-4. Value Types and Reference Types Comparison
有关值类型和引用类型的最终详细信息

Intriguing Question
耐人寻味的问题
Value Type
值类型
Reference Type
引用类型
Where are objects allocated?
对象分配在哪里?
Allocated on the stack.
在堆栈上分配。
Allocated on the managed heap.
在托管堆上分配。
How is a variable represented?
变量如何表示?
Value type variables are local copies.
值类型变量是本地副本。
Reference type variables are pointing to the memory occupied by the allocated instance.
引用类型变量指向分配的实例占用的内存。
What is the base type?
基本类型是什么?
Implicitly extends System. ValueType.
隐式扩展系统。值类型。
Can derive from any other type (except System.ValueType), if that type is not “sealed” (more details on this in Chapter 6).
可以从任何其他类型(System.ValueType 除外)派生,如果该类型不是“密封的”(有关此内容的更多详细信息,请参见第 6 章)。
Can this type function as a base to other types?
此类型可以作为其他类型的基础吗?
No. Value types are always sealed and cannot be inherited from.
不。值类型始终是密封的,不能从中继承。
Yes. If the type is not sealed, it may function as a base to other types.
是的。如果该类型未密封,则可以用作其他类型的基础。
What is the default parameter-passing behavior?
默认参数传递行为是什么?
Variables are passed by value (i.e., a copy of the variable is passed into the called function).
默认参数传递行为是什么?
For reference types, the reference is copied by value.
对于引用类型,引用按值复制。
Can this type override System. Object.Finalize()?
此类型可以覆盖System. Object.Finalize()?
No.
不。
Yes, indirectly (more details on this in Chapter 9).
是的,间接的(有关此内容的更多详细信息请参阅第9章)。
Can I define constructors for this type?
我可以为此类型定义构造函数吗?
Yes, but the default constructor is reserved (i.e., your custom constructors must all have arguments).
是的,但默认构造函数是保留的(即,您的自定义构造函数必须全部具有参数)。
But of course!
但是当然!
When do variables of this type die?
这种类型的变量什么时候死亡?
When they fall out of the defining scope.
当它们超出定义范围时。
When the object is garbage collected (see Chapter 9).
当对象被垃圾回收时(请参阅第 9 章)。

Despite their differences, value types and reference types both can implement interfaces and may support any number of fields, methods, overloaded operators, constants, properties, and events.
尽管存在差异,但值类型和引用类型都可以实现接口,并且可以支持任意数量的字段、方法、重载运算符、常量、属性和事件。

Understanding C# Nullable Types

了解 C# 可为空的类型
Let’s examine the role of the nullable data type using a Console Application project named FunWithNullableValueTypes. As you know, C# data types have a fixed range and are represented as a type in the System namespace. For example, the System.Boolean data type can be assigned a value from the set
{true, false}. Now, recall that all the numerical data types (as well as the Boolean data type) are value types. Value types can never be assigned the value of null, as that is used to establish an empty object reference.
让我们使用名为 FunWithNullableValueType 的控制台应用程序项目检查可为空数据类型的角色。如您所知,C# 数据类型具有固定范围,并在 System 命名空间中表示为一种类型。例如,可以从集合中为 System.Boolean 数据类型分配一个值{真,假}。现在,回想一下,所有数值数据类型(以及布尔数据类型)都是值类型。永远不能为值类型分配 null 值,因为它用于建立空对象参考。

// Compiler errors!
// Value types cannot be set to null! bool myBool = null;
int myInt = null;

C# supports the concept of nullable data types. Simply put, a nullable type can represent all the values of its underlying type, plus the value null. Thus, if you declare a nullable bool, it could be assigned a value from the set {true, false, null}. This can be extremely helpful when working with relational databases, given that it is quite common to encounter undefined columns in database tables. Without the concept of a nullable data type, there is no convenient manner in C# to represent a numerical data point with no value.
C# 支持可为 null 的数据类型的概念。简单地说,可为 null 的类型可以表示其基础类型的所有值以及值 null。因此,如果你声明一个可为空的布尔值,则可以从集合 {true, false, null} 中为其赋值。这在使用关系数据库时非常有用,因为在数据库表中遇到未定义的列是很常见的。如果没有可为 null 的数据类型的概念,C# 中就没有方便的方式来表示没有值的数值数据点。

To define a nullable variable type, the question mark symbol (?) is suffixed to the underlying data type. Like a non-nullable variable, local nullable variables must be assigned an initial value before you can use them.
C# 支持可为 null 的数据类型的概念。简单地说,可为 null 的类型可以表示其基础类型的所有值以及值 null。因此,如果你声明一个可为空的布尔值,则可以从集合 {true, false, null} 中为其赋值。这在使用关系数据库时非常有用,因为在数据库表中遇到未定义的列是很常见的。如果没有可为 null 的数据类型的概念,C# 中就没有方便的方式来表示没有值的数值数据点。

static void LocalNullableVariables()
{
// Define some local nullable variables. int? nullableInt = 10;
double? nullableDouble = 3.14; bool? nullableBool = null; char? nullableChar = ‘a’;
int?[] arrayOfNullableInts = new int?[10];
}

Using Nullable Value Types

使用可为空的值类型

In C#, the ? suffix notation is a shorthand for creating an instance of the generic System.Nullable structure type. It is also used for creating nullable reference types (covered in the next section), although the behavior is a bit different. While you will not examine generics until Chapter 10, it is important to understand that the System.Nullable type provides a set of members that all nullable types can make use of.
在 C# 中,? 后缀表示法是创建通用 System.Nullable 结构类型实例的简写。它还用于创建可为 null 的引用类型(将在下一节中介绍),尽管行为略有不同。虽然在第 10 章之前不会检查泛型,但重要的是要了解 System.Nullable 类型提供了一组所有可为 null 类型都可以使用的成员。

For example, you can programmatically discover whether the nullable variable indeed has been assigned a null value using the HasValue property or the != operator. The assigned value of a nullable type may be obtained directly or via the Value property. In fact, given that the ? suffix is just a shorthand for using Nullable, you could implement your LocalNullableVariables() method as follows:
例如,可以使用 HasValue 属性或 != 运算符以编程方式发现是否确实为可为 null 变量分配了 null 值。可为 null 类型的赋值可以直接获取,也可以通过 Value 属性获取。事实上,鉴于 ? 后缀只是使用 Nullable 的简写,您可以按如下方式实现 LocalNullableVariables() 方法:

static void LocalNullableVariablesUsingNullable()
{
// Define some local nullable types using Nullable. Nullable nullableInt = 10;
Nullable nullableDouble = 3.14; Nullable nullableBool = null; Nullable nullableChar = ‘a’;
Nullable[] arrayOfNullableInts = new Nullable[10];
}

As stated, nullable data types can be particularly useful when you are interacting with databases, given that columns in a data table may be intentionally empty (e.g., undefined). To illustrate, assume the following class, which simulates the process of accessing a database that has a table containing two columns that
may be null. Note that the GetIntFromDatabase() method is not assigning a value to the nullable integer member variable, while GetBoolFromDatabase() is assigning a valid value to the bool? member.
如前所述,在与数据库交互时,可为空的数据类型可能特别有用,因为数据表中的列可能有意为空(例如,未定义)。为了说明这一点,假设以下类,该类模拟访问数据库的过程,该数据库的表包含两列可能为空。请注意,GetIntFromDatabase() 方法没有为可为空的整数成员变量赋值,而 GetBoolFromDatabase() 为布尔值赋值? 成员。

class DatabaseReader
{
// Nullable data field.
public int? numericValue = null; public bool? boolValue = true;

// Note the nullable return type. public int? GetIntFromDatabase()
{ return numericValue; }

// Note the nullable return type. public bool? GetBoolFromDatabase()
{ return boolValue; }
}

Now, examine the following code, which invokes each member of the DatabaseReader class and discovers the assigned values using the HasValue and Value members, as well as using the C# equality operator (not equal, to be exact):
现在,检查以下代码,该代码调用 DatabaseReader 类的每个成员,并使用 HasValue 和 Value 成员以及 C# 相等运算符(确切地说,不相等)发现分配的值:

Console.WriteLine(" Fun with Nullable Value Types \n"); DatabaseReader dr = new DatabaseReader();

// Get int from "database".
int? i = dr.GetIntFromDatabase(); if (i.HasValue)
{
Console.WriteLine("Value of ‘i’ is: {0}", i.Value);
}
else
{
Console.WriteLine("Value of ‘i’ is undefined.");
}
// Get bool from "database".
bool? b = dr.GetBoolFromDatabase(); if (b != null)
{
Console.WriteLine("Value of ‘b’ is: {0}", b.Value);
}
else
{
Console.WriteLine("Value of ‘b’ is undefined.");
}
Console.ReadLine();

Using Nullable Reference Types (New 8.0, Updated 10.0)

使用可为空的引用类型(新 8.0,更新的 10.0)

A significant feature added with C# 8 is support for nullable reference types. In fact, the change is so significant that the .NET Framework could not be updated to support this new feature, which is one of the reasons for only supporting C# 8 in .NET Core 3.0 and above.
C# 8 添加的一项重要功能是支持可为 null 的引用类型。事实上,此更改是如此之大,以至于无法更新 .NET Framework 以支持此新功能,这是在 .NET Core 8.3 及更高版本中仅支持 C# 0 的原因之一。
When you create a new project in .NET Core 3.0/3.1 or .NET 5, reference types work the same way that they did with C# 7. This is to prevent breaking billions of lines of code that exist in the pre–C# 8 ecosystem. Developers must opt in to enable nullable reference types in their applications.
在 .NET Core 3.0/3.1 或 .NET 5 中创建新项目时,引用类型的工作方式与使用 C# 7 的工作方式相同。这是为了防止破坏 C# 8 之前的生态系统中存在的数十亿行代码。开发人员必须选择在其应用程序中启用可为空的引用类型。

C# 10 and .NET 6 (and above) change the default and enable nullable reference types in all of the project templates.
C# 10 和 .NET 6(及更高版本)更改默认值,并在所有项目模板中启用可为 null 的引用类型。

Nullable reference types follow many of the same rules as nullable value types. Non-nullable reference types must be assigned a non-null value at initialization and cannot later be changed to a null value.
可为 null 的引用类型遵循许多与可为 null 的值类型相同的规则。不可为空的引用类型必须在初始化时分配非空值,以后不能更改为空值。可为 null 的引用类型可以为 null,但在首次使用之前仍必须为其分配某些内容(某些内容的实际实例或 null 的值)。

Nullable reference types can be null, but still must be assigned something before first use (either an actual instance of something or the value of null).
Nullable reference types use the same symbol (?) to indicate that they are nullable. However, this is not a shorthand for using System.Nullable, as only value types can be used in place of T. As a reminder, generics and constraints are covered in Chapter 10.
可为空的引用类型使用相同的符号 (?) 来指示它们可为空。但是,这不是使用 System.Nullable 的简写,因为只能使用值类型代替 T。提醒一下,第10章介绍了泛型和约束。

Opting in for Nullable Reference Types (Updated 10.0)

选择加入可为空的引用类型(10.0 更新)
Support for nullable reference types is controlled by setting a nullable context. This can be as big as an entire project (by updating the project file) or as small as a few lines (by using compiler directives). There are also two contexts that can be set:
对可为空的引用类型的支持是通过设置可为空的上下文来控制的。这可以大到整个项目(通过更新项目文件),也可以小到几行(通过使用编译器指令)。还可以设置两个上下文:

• Nullable annotation context: This enables/disables the nullable annotation (?) for nullable reference types.
可为空的注释上下文:这将启用/禁用可为空的引用类型的可为空的注释 (?)。
• Nullable warning context: This enables/disables the compiler warnings for nullable reference types.
可为空的警告上下文:这将启用/禁用可为空的引用类型的编译器警告。

To see these in action, create a new console application named FunWithNullableReferenceTypes. Open the project file (if you are using Visual Studio, double-click the project name in Solution Explorer or right- click the project name and select Edit Project File). As mentioned previously, in C# 10, every new project defaults to enabling nullable reference types. Notice the node in the project file listing (all the available options are shown in Table 4-5):
若要查看这些操作,请创建一个名为 FunWithNullableReferenceType 的新控制台应用程序。打开项目文件(如果使用的是 Visual Studio,请在“解决方案资源管理器”中双击项目名称,或右键单击项目名称并选择“编辑项目文件”)。如前所述,在 C# 10 中,每个新项目默认启用可为 null 的引用类型。请注意项目文件列表中的<可为空>节点(表 4-5 中显示了所有可用选项):

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    </PropertyGroup>
</Project>

Table 4-5. Values for Nullable in Project Files
表 4-5. 项目文件中可为空的值

Value Meaning in Life
enable Nullable annotations are enabled, and nullable warnings are enabled.
启用可为空的批注,并启用可为空的警告。
warnings Nullable annotations are disabled, and nullable warnings are enabled.
禁用可为空的批注,并启用可为空的警告。
annotations Nullable annotations are enabled, and nullable warnings are disabled.
启用可为空的批注,禁用可为空的警告。
disable Nullable annotations are disabled, and nullable warnings are disabled.
禁用可为空的批注,并禁用可为空的警告。

As one would expect, the element in the project file affects the entire project. To control smaller parts of the project, use the compiler directives shown in Table 4-6.
正如人们所期望的那样,项目文件中的<可为空>元素会影响整个项目。若要控制项目的较小部分,请使用表 4-6 中所示的编译器指令。

Table 4-6. Values for #nullable Compiler Directive
表 4-6. #nullable 编译器指令的值

Value Meaning in Life
enable Annotations are enabled, and warnings are enabled.
启用批注,并启用警告。
disable Annotations are disabled, and warnings are disabled.
禁用批注,并禁用警告。
restore Restores all settings to the project settings.
将所有设置还原为项目设置。
disable warnings Warnings are disabled, and annotations are unaffected.
警告被禁用,批注不受影响。
enable warnings Warnings are enabled, and annotations are unaffected.
警告已启用,批注不受影响。
restore warnings Warnings reset to project settings; annotations are unaffected.
警告重置为项目设置;批注不受影响。
disable annotations Annotations are disabled, and warnings are unaffected.
注释被禁用,警告不受影响。
enable annotations Annotations are enabled, and warnings are unaffected.
批注已启用,警告不受影响。
restore annotations Annotations are reset to project settings; warnings are unaffected.
注释将重置为项目设置;警告不受影响。

■ Note as mentioned, with the introduction of C# 10/.net 6, nullable reference types (nrts) are enabled by default. For the rest of this book, the code samples all have nrts disabled unless specifically called out in the example. this is not to say you shouldn’t use nrts—that is a decision you need to make based on your project’s requirements. i have disabled them to keep the examples in this book focused on the specific teaching goal.
注意 如前所述,随着 C# 10/.net 6 的引入,默认情况下启用可为空的引用类型 (nrt)。对于本书的其余部分,除非示例中特别指出,否则代码示例都禁用了 nrt。这并不是说您不应该使用 NRT,这是您需要根据项目要求做出的决定。我禁用了它们,以使本书中的示例专注于特定的教学目标。

Nullable Reference Types in Action

操作中的可为空的引用类型

Largely because of the significance of the change, nullable types only throw errors when used improperly. Add the following class to the Program.cs file:
很大程度上由于更改的重要性,可为 null 的类型仅在使用不当时引发错误。将以下类添加到程序.cs文件中:

public class TestClass
{
public string Name { get; set; } public int Age { get; set; }
}

As you can see, this is just a normal class. The nullability comes in when you use this class in your code.
如您所见,这只是一个普通的课程。在代码中使用此类时,可空性就会出现。

Take the following declarations:
采取以下声明:

string? nullableString = null; TestClass? myNullableClass = null;

The project file setting makes the entire project a nullable context. The nullable context allows the declarations of the string and TestClass types to use the nullable annotation (?). The following line of code generates a warning (CS8600) due to the assignment of a null to a non-nullable type in a nullable context:
项目文件设置使整个项目成为可为空的上下文。可为空的上下文允许字符串和 TestClass 类型的声明使用可为空的注释 (?)。以下代码行生成警告 (CS8600),原因是在可为 null 的上下文中将 null 分配给不可为空的类型:

//Warning CS8600 Converting null literal or possible null value to non-nullable type TestClass myNonNullableClass = myNullableClass;

For finer control of where the nullable contexts are in your project, you can use compiler directives (as discussed earlier) to enable or disable the context. The following code turns off the nullable context (set at the project level) and then reenables it by restoring the project settings:
为了更好地控制可为 null 的上下文在项目中的位置,可以使用编译器指令(如前所述)来启用或禁用上下文。以下代码关闭可为 null 的上下文(在项目级别设置),然后通过还原项目设置重新启用它:

nullable disable

TestClass anotherNullableClass = null;
//Warning CS8632 The annotation for nullable reference types
//should only be used in code within a ‘#nullable’ annotations

TestClass? badDefinition = null;
//Warning CS8632 The annotation for nullable reference types
//should only be used in code within a ‘#nullable’ annotations string? anotherNullableString = null;

nullable restore

Add the previous example method EnterLogData() into the top-level statements of our current project:
将前面的示例方法 EnterLogData() 添加到我们当前项目的顶级语句中:

static void EnterLogData(string message, string owner = "Programmer")
{
ArgumentNullException.ThrowIfNull(message); Console.WriteLine("Error: {0}", message); Console.WriteLine("Owner of Error: {0}", owner);
}

Since this project has nullable reference types enabled, the message and owner parameters are not nullable. The owner parameter has a default value set, so it will never be null. However, the message parameter does not have a default value set, so calling it like this will raise a compiler warning:
由于此项目启用了可为空的引用类型,因此消息和所有者参数不可为空。owner 参数设置了默认值,因此它永远不会为 null。但是,消息参数没有设置默认值,因此像这样调用它将引发编译器警告:

EnterLogData(null);
//Warning CS8625 Cannot convert null literal to non-nullable reference type.

Chances are you won’t explicitly pass in a null value, but more likely you might pass in a variable that happens to be null:
您可能不会显式传入 null 值,但更有可能传入恰好为 null 的变量:

string? msg = null;
EnterLogData(msg);
//Warning CS8604 Possible null reference argument for parameter ‘message’ in
// ‘void EnterLogData(string message, string owner = "Programmer")’.

This doesn’t solve the problem of null values passed into a method, but it does provide some level of compile-time checking of your code.
这并不能解决将 null 值传递到方法中的问题,但它确实提供了一定程度的代码编译时检查。
As a final note, the nullable reference types do not have the HasValue and Value properties, as those are supplied by System.Nullable.
最后,可为空的引用类型没有 HasValue 和 Value 属性,因为它们由 System.Nullable 提供。

Migration Considerations

迁移注意事项

When migrating your code from C# 7 to C# 8, C# 9, or C# 10 and you want to make use of nullable reference types, you can use a combination of the project setting and compiler directives to work through your code.
将代码从 C# 7 迁移到 C# 8、C# 9 或 C# 10 时,如果想要使用可为 null 的引用类型,则可以结合使用项目设置和编译器指令来完成代码。

A common practice is to start by enabling warnings and disabling nullable annotations for the entire project. Then, as you clean up areas of code, use the compiler directives to gradually enable the annotations. Remember that with C# 10, nullable reference types are enabled by default in project templates.
一种常见的做法是从启用警告并禁用整个项目的可为空的批注开始。然后,在清理代码区域时,使用编译器指令逐步启用批注。请记住,在 C# 10 中,默认情况下在项目模板中启用可为 null 的引用类型。

Change Nullable Warnings to Errors

将可为空的警告更改为错误

When you are ready to commit to nullable reference types, you can configure the nullable warnings as errors. The easiest way to do this for a project is to add the following into the project file:
准备好提交可为空的引用类型时,可以将可为空的警告配置为错误。为项目执行此操作的最简单方法是将以下内容添加到项目文件中:

<PropertyGroup>
    <WarningsAsErrors>CS8604,CS8625</WarningsAsErrors>
</PropertyGroup>

In fact, if you want to treat all warnings related to nullable reference types to errors, you can use the following syntax:
实际上,如果要将与可为 null 的引用类型相关的所有警告处理为错误,则可以使用以下语法:

<PropertyGroup>
    <WarningsAsErrors>Nullable</WarningsAsErrors>
</PropertyGroup>

■ Note You can change the severity of any warning to an error, not just those regarding nullable reference types. You can also change all warnings to errors by using true</ TreatWarningsAsErrors> instead of the WarningsAsErrors node in your project file.
注意 可以将任何警告的严重性更改为错误,而不仅仅是有关可为空的引用类型的警告。还可以通过使用项目文件中的 true</ TreatWarningsAsErrors>而不是 WarningsAsErrors 节点将所有警告更改为错误。

Operating on Nullable Types

对可为空类型进行操作

C# provides several operators for working with nullable types. The next sections code the null-coalescing operator, the null-coalescing assignment operator, and the null conditional operator. For these examples, go back to the FunWithNullableValueTypes project.
C# 提供了多个运算符来处理可为 null 的类型。接下来的部分对 null 合并运算符、null 合并赋值运算符和 null 条件运算符进行编码。对于这些示例,请返回到 FunWithNullableValueTypes 项目。

The Null-Coalescing Operator

零合并运算符

The next aspect to be aware of is any variable that might have a null value can make use of the C# ?? operator, which is formally termed the null-coalescing operator. This operator allows you to assign a value to a nullable type if the retrieved value is in fact null. For this example, assume you want to assign a local nullable integer to 100 if the value returned from GetIntFromDatabase() is null (of course, this method is programmed to always return null, but I am sure you get the general idea). Move back to the FunWithNullableValueTypes project (and set it as the startup project), and enter the following code:
下一个需要注意的方面是任何可能具有空值的变量都可以使用 C#? 运算符,正式名称为零合并运算符。如果检索到的值实际上是 null,则此运算符允许您将值分配给可为 null 的类型。对于此示例,假设如果从 GetIntFromDatabase() 返回的值为 null,则要将本地可为 null 的整数分配给 100(当然,此方法被编程为始终返回 null,但我相信您了解大致想法)。移回 FunWithNullableValueTypes 项目(并将其设置为启动项目),然后输入以下代码:

Console.WriteLine(" Fun with Nullable Value Types \n");
DatabaseReader dr = new DatabaseReader();

// If the value from GetIntFromDatabase() is null,
// assign local variable to 100.
int myData = dr.GetIntFromDatabase() ?? 100; Console.WriteLine("Value of myData: {0}", myData); Console.ReadLine();

The benefit of using the ?? operator is that it provides a more compact version of a traditional if/else condition. However, if you want, you could have authored the following functionally equivalent code to ensure that if a value comes back as null, it will indeed be set to the value 100:
使用 ?? 运算符是它提供了传统 IF/ELSE 条件的更紧凑版本。但是,如果需要,可以编写以下功能等效的代码,以确保如果值返回为 null,它确实会设置为值 100:

// Longhand notation not using ?? syntax. int? moreData = dr.GetIntFromDatabase(); if (!moreData.HasValue)
{
moreData = 100;
}
Console.WriteLine("Value of moreData: {0}", moreData);

The Null-Coalescing Assignment Operator (New 8.0)

空合并赋值运算符(新版 8.0)

Building on the null-coalescing operator, C# 8 introduced the null-coalescing assignment operator (??=). This operator assigns the left-hand side to the right-hand side only if the left-hand side is null. For example, enter the following code:
在零合并运算符的基础上,C# 8 引入了零合并赋值运算符 (??=).仅当左侧为 null 时,此运算符才会将左侧分配给右侧。例如,输入以下代码:

//Null-coalescing assignment operator int? nullableInt = null;
nullableInt ??= 12;
nullableInt ??= 14; Console.WriteLine(nullableInt);

The nullableInt variable is initialized to null. The next line assigns the value of 12 to the variable since the left-hand side is indeed null. The next line does not assign 14 to the variable since it is not null.
nullableInt 变量初始化为 null。下一行将值 12 分配给变量,因为左侧确实为 null。下一行不会为变量赋值 14,因为它不为 null。

The Null Conditional Operator

空条件运算符

When you are writing software, it is common to check incoming parameters, which are values returned from type members (methods, properties, indexers), against the value null. For example, let’s assume you have a method that takes a string array as a single parameter. To be safe, you might want to test for null before proceeding. In that way, you will not get a runtime error if the array is empty. The following would be a traditional way to perform such a check:
编写软件时,通常会根据值 null 检查传入参数,这些参数是从类型成员(方法、属性、索引器)返回的值。例如,假设你有将字符串数组作为单个参数的方法。为了安全起见,您可能需要在之前测试空值进行中。这样,如果数组为空,则不会收到运行时错误。以下是执行此类检查的传统方法:

static void TesterMethod(string[] args)
{
// We should check for null before accessing the array data!
// 我们应该在访问数组数据之前检查 null!
if (args != null)
{
Console.WriteLine($"You sent me {args.Length} arguments.");
}
}

Here, you use a conditional scope to ensure that the Length property of the string array will not be accessed if the array is null. If the caller failed to make an array of data and called your method like so, you are still safe and will not trigger a runtime error:
在这里,您使用条件作用域来确保如果数组为 null,则不会访问字符串数组的 Length 属性。如果调用方未能创建数据数组并像这样调用您的方法,您仍然是安全的,并且不会触发运行时错误:

TesterMethod(null);

C# includes the null conditional operator token (a question mark placed after a variable type but before an access operator) to simplify the previous error checking. Rather than explicitly building a conditional statement to check for null, you can now write the following:
C# 包含 null 条件运算符标记(位于变量类型之后但访问运算符之前的问号),以简化前面的错误检查。您现在可以编写以下内容,而不是显式构建条件语句来检查 null:

static void TesterMethod(string[] args)
{
// We should check for null before accessing the array data! Console.WriteLine($"You sent me {args?.Length} arguments.");
}

In this case, you are not using a conditional statement. Rather, you are suffixing the ? operator directly after the string array variable. If the variable is null, its call to the Length property will not throw a runtime error. If you want to print an actual value, you could leverage the null-coalescing operator to assign a default value as so:
在这种情况下,您没有使用条件语句。相反,您正在为? 运算符直接在字符串数组变量之后。如果变量为 null,则它对 Length 属性的调用不会引发运行时错误。如果要打印实际值,可以利用 null 合并运算符分配默认值,如下所示:

Console.WriteLine($"You sent me {args?.Length ?? 0} arguments.");

There are some additional areas of coding where the C# 6.0 null conditional operator will be quite handy, especially when working with delegates and events. Those topics are addressed later in the book (see Chapter 12), and you will see many more examples.
还有一些其他编码区域,其中 C# 6.0 null 条件运算符将非常方便,尤其是在处理委托和事件时。这些主题将在本书后面讨论(见第12章),你将看到更多的例子。

Understanding Tuples (New/Updated 7.0)

了解元组(新增/更新 7.0)

To wrap up this chapter, let’s examine the role of tuples using a Console Application project named FunWithTuples. As mentioned earlier in this chapter, one way to use out parameters is to retrieve more than one value from a method call. Another way is to use a light construct called a tuple.
为了结束本章,让我们使用名为 FunWithTuples 的控制台应用程序项目来检查元组的作用。如本章前面所述,使用参数的一种方法是从方法调用中检索多个值。另一种方法是使用称为元组的轻构造。

Tuples are lightweight data structures that contain multiple fields. They were added to the language in C# 6, but in an extremely limited way. There was also a potentially significant problem with the C# 6 implementation: each field is implemented as a reference type, potentially creating memory and/or performance problems (from boxing/unboxing).
元组是包含多个字段的轻量级数据结构。它们被添加到 C# 6 的语言中,但方式极其有限。C 还有一个潜在的重大问题#6 实现:每个字段都作为引用类型实现,可能会产生内存和/或性能问题(来自装箱/取消装箱)。

■ Note Boxing occurs when a value type is stored as a reference variable (stored on the heap), and unboxing is when the value type is returned to a value type variable (stored on the stack). Boxing and unboxing and their performance implications are covered in depth in Chapter 10.
注意 当值类型存储为引用变量(存储在堆上)时,会发生装箱,而取消装箱是指将值类型返回到值类型变量(存储在堆栈上)。第10章深入介绍了装箱和拆箱及其性能影响。

In C# 7, tuples use the new ValueTuple data type instead of reference types, potentially saving significant memory. The ValueTuple data type creates different structs based on the number of properties for a tuple. An additional feature added in C# 7 is that each property in a tuple can be assigned a specific name (just like variables), greatly enhancing the usability.
在 C# 7 中,元组使用新的 ValueTuple 数据类型而不是引用类型,这可能会节省大量内存。ValueTuple 数据类型根据元组的属性数创建不同的结构。C# 7 中添加的另一个功能是,元组中的每个属性都可以分配一个特定名称(就像变量一样),从而大大提高了可用性。

These are two important considerations for tuples:
以下是元组的两个重要注意事项:

• The fields are not validated.
字段未验证。

• You cannot define your own methods.
• 您无法定义自己的方法。

They are really designed to just be a lightweight data transport mechanism.
它们实际上被设计为一种轻量级的数据传输机制。

Getting Started with Tuples

元组入门

Enough theory. Let’s write some code! To create a tuple, simply enclose the values to be assigned to the tuple in parentheses, as follows:
足够的理论。让我们编写一些代码!要创建元组,只需将要分配给元组的值括在括号中,如下所示:
("a", 5, "c")

Notice that they do not all have to be the same data type. The parenthetical construct is also used to assign the tuple to a variable (or you can use the var keyword and the compiler will assign the data types for you). To assign the previous example to a variable, the following two lines achieve the same thing. The values variable will be a tuple with two string properties and an int property sandwiched in between.
请注意,它们不必都是相同的数据类型。括号结构也用于将元组分配给变量(或者您可以使用 var 关键字,编译器将为您分配数据类型)。为了将前面的示例分配给变量,以下两行实现相同的目标。values 变量将是一个元组,其中包含两个字符串属性和一个夹在两者之间的 int 属性。

(string, int, string) values = ("a", 5, "c");
var values = ("a", 5, "c");

By default, the compiler assigns each property the name ItemX, where X represents the one-based position in the tuple. For the previous example, the property names are Item1, Item2, and Item3. Accessing them is done as follows:
默认情况下,编译器为每个属性分配名称 ItemX,其中 X 表示元组中从 1 开始的位置。对于前面的示例,属性名称为 Item2、Item3 和 Item<>。访问它们的方式如下:

Console.WriteLine($"First item: {values.Item1}"); Console.WriteLine($"Second item: {values.Item2}"); Console.WriteLine($"Third item: {values.Item3}");

Specific names can also be added to each property in the tuple on either the right side or the left side of the statement. While it is not a compiler error to assign names on both sides of the statement, if you do, the right side will be ignored, and only the left-side names are used. The following two lines of code show setting the names on the left and the right to achieve the same end:
还可以将特定名称添加到语句右侧或左侧元组中的每个属性。虽然在语句的两端分配名称不是编译器错误,但如果这样做,右侧将被忽略,仅使用左侧名称。以下两行代码显示设置左侧和右侧的名称以实现相同的目的:

(string FirstLetter, int TheNumber, string SecondLetter) valuesWithNames = ("a", 5, "c"); var valuesWithNames2 = (FirstLetter: "a", TheNumber: 5, SecondLetter: "c");

Now the properties on the tuple can be accessed using the field names as well as the ItemX notation, as shown in the following code:
现在,可以使用字段名称和 ItemX 表示法访问元组上的属性,如以下代码所示:

Console.WriteLine($"First item: {valuesWithNames.FirstLetter}"); Console.WriteLine($"Second item: {valuesWithNames.TheNumber}"); Console.WriteLine($"Third item: {valuesWithNames.SecondLetter}");
//Using the item notation still works! Console.WriteLine($"First item: {valuesWithNames.Item1}"); Console.WriteLine($"Second item: {valuesWithNames.Item2}"); Console.WriteLine($"Third item: {valuesWithNames.Item3}");

Note that when setting the names on the right, you must use the keyword var to declare the variable. Setting the data types specifically (even without custom names) triggers the compiler to use the left side, assign the properties using the ItemX notation, and ignore any of the custom names set on the right. The following two examples ignore the Custom1 and Custom2 names:
请注意,在右侧设置名称时,必须使用关键字 var 来声明变量。专门设置数据类型(即使没有自定义名称)会触发编译器使用左侧,使用 ItemX 表示法分配属性,并忽略右侧设置的任何自定义名称。以下两个示例忽略自定义 1 和自定义 2 名称:

(int, int) example = (Custom1:5, Custom2:7);
(int Field1, int Field2) example = (Custom1:5, Custom2:7);

It is also important to call out that the custom field names exist only at compile time and are not available when inspecting the tuple at runtime using reflection (reflection is covered in Chapter 17).
同样重要的是要指出,自定义字段名称仅在编译时存在,并且在运行时使用反射检查元组时不可用(反射在第 17 章中介绍)。

Tuples can also be nested as tuples inside of tuples. Since each property in a tuple is a data type and a tuple is a data type, the following code is perfectly legitimate:
元组也可以作为元组嵌套在元组内。由于元组中的每个属性都是数据类型,元组是数据类型,因此以下代码是完全合法的:

Console.WriteLine("=> Nested Tuples"); var nt = (5, 4, ("a", "b"));

Using Inferred Variable Names (Updated 7.1)

使用推断的变量名称(7.1 更新)

An update to tuples in C# 7.1 is the ability for C# to infer the variable names of tuples, as shown here:
C# 7.1 中对元组的更新是 C# 能够推断元组的变量名称,如下所示:

Console.WriteLine("=> Inferred Tuple Names");
var foo = new {Prop1 = "first", Prop2 = "second"}; var bar = (foo.Prop1, foo.Prop2); Console.WriteLine($"{bar.Prop1};{bar.Prop2}");

Understanding Tuple Equality/Inequality (New 7.3)

了解元组相等/不等式(新 7.3)

An added feature in C# 7.3 is the tuple equality (==) and inequality (!=). When testing for inequality, the comparison operators will perform implicit conversions on data types within the tuples, including comparing nullable and non-nullable tuples and/or properties. That means the following tests work perfectly, despite the difference between int/long:
C# 7.3 中新增的功能是元组相等 (==) 和不相等 (!=)。测试不等式时,比较运算符将对元组中的数据类型执行隐式转换,包括比较可为空和不可为空的元组和/或属性。这意味着以下测试可以完美运行,尽管 int/long 之间存在差异:

Console.WriteLine("=> Tuples Equality/Inequality");
// lifted conversions
var left = (a: 5, b: 10);
(int? a, int? b) nullableMembers = (5, 10); Console.WriteLine(left == nullableMembers); // Also true
// converted type of left is (long, long) (long a, long b) longTuple = (5, 10);
Console.WriteLine(left == longTuple); // Also true
// comparisons performed on (long, long) tuples (long a, int b) longFirst = (5, 10);
(int a, long b) longSecond = (5, 10); Console.WriteLine(longFirst == longSecond); // Also true

Tuples that contain tuples can also be compared, but only if they have the same shape. You cannot compare one tuple of three int properties with another tuple of two ints and a tuple.
也可以比较包含元组的元组,但前提是它们的形状相同。不能将一个包含三个 int 属性的元组与另一个包含两个 int和一个元组的元组进行比较。

Understanding Tuples as Method Return Values

将元组理解为方法返回值

Earlier in this chapter, out parameters were used to return more than one value from a method call. There are additional ways to do this, such as creating a class or structure specifically to return the values. But if this class or struct is to be used as a data transport for only one method, that is extra work and extra code that does not need to be developed. Tuples are perfectly suited for this task, are lightweight, and are easy to declare and use.
在本章前面,out 参数用于从方法调用返回多个值。还有其他方法可以执行此操作,例如创建专门用于返回值的类或结构。但是,如果仅将此类或结构用作一种方法的数据传输,那就是不需要开发的额外工作和额外代码。元组非常适合此任务,是轻量级的,并且易于声明和使用。

This is one of the examples from the out parameter section. It returns three values but requires three parameters passed in as transport mechanisms for the calling code.
这是 out 参数部分中的示例之一。它返回三个值,但需要三个参数作为调用代码的传输机制传入。

static void FillTheseValues(out int a, out string b, out bool c)
{
a = 9;
b = "Enjoy your string."; c = true;
}

By using a tuple, you can remove the parameters and still get the three values back.
通过使用元组,可以删除参数并仍返回三个值。

static (int a,string b,bool c) FillTheseValues()
{
return (9,"Enjoy your string.",true);
}

Calling this method is as simple as calling any other method.
调用此方法与调用任何其他方法一样简单。

var samples = FillTheseValues(); Console.WriteLine($"Int is: {samples.a}");

Console.WriteLine($"String is: {samples.b}"); Console.WriteLine($"Boolean is: {samples.c}");

Perhaps a better example is deconstructing a full name into its individual parts (first, middle, last). The following code takes in a full name and returns a tuple with the different parts:
也许一个更好的例子是将全名解构为其各个部分(第一个、中间、最后一个)。以下代码采用全名并返回包含不同部分的元组:

static (string first, string middle, string last) SplitNames(string fullName)
{
//do what is needed to split the name apart return ("Philip", "F", "Japikse");
}

Understanding Discards with Tuples

了解使用元组的丢弃
Following up on the SplitNames() example, suppose you know that you need only the first and last names and do not care about the middle. By providing variable names for the values you want returned and filling in the unneeded values with an underscore () placeholder, you can refine the return value like this:
继续 SplitNames() 示例,假设您知道您只需要名字和姓氏,而不关心中间。通过为要返回的值提供变量名称,并使用下划线 (
) 占位符填充不需要的值,可以像这样优化返回值:

var (first, _, last) = SplitNames("Philip F Japikse"); Console.WriteLine($"{first}:{last}");

The middle name value of the tuple is discarded.
元组的中间名值将被丢弃。

Understanding Tuple Pattern Matching switch Expressions (New 8.0)

了解元组模式匹配开关表达式(新 8.0)

Now that you have a thorough understanding of tuples, it is time to revisit the switch expression with tuples from Chapter 3. Here is the example again:
现在您已经对元组有了透彻的了解,是时候重新审视第 3 章中带有元组的 switch 表达式了。这里又是一个例子:

//Switch expression with Tuples
static string RockPaperScissors(string first, string second)
{
return (first, second) switch
{
("rock", "paper") => "Paper wins.",
("rock", "scissors") => "Rock wins.",
("paper", "rock") => "Paper wins.", ("paper", "scissors") => "Scissors wins.", ("scissors", "rock") => "Rock wins.", ("scissors", "paper") => "Scissors wins.", (, ) => "Tie.",
};
}

In this example, the two parameters are converted into a tuple as they are passed into the switch expression. The relevant values are represented in the switch expression, and any other cases are handled by the final tuple, which is composed of two discards.
The RockPaperScissors() method signature could also be written to take in a tuple, like this:
在此示例中,这两个参数在传递到 switch 表达式时将转换为元组。相关值在 switch 表达式中表示,任何其他情况由最终元组处理,该元组由两个丢弃组成。RockPaperScissors() 方法签名也可以写成接受元组,如下所示:

static string RockPaperScissors( (string first, string second) value)

{
return value switch
{
//omitted for brevity
};
}

Deconstructing Tuples (Updated 10.0)

解构元组(10.0 更新)

Deconstructing is the term given when separating out the properties of a tuple to be used individually. The SplitNames() example did just that. The first and last variables were accessed independently from any tuple construct. The variables can be initialized while deconstructing the tuple, or they can be pre-initialized. The following examples show both patterns:
解构是在分离要单独使用的元组的属性时给出的术语。SplitNames() 示例就是这样做的。第一个和最后一个变量独立于任何元组构造进行访问。变量可以在解构元组时初始化,也可以预先初始化。以下示例显示了这两种模式:

(int X, int Y) myTuple = (4,5); int x = 0;
int y = 0;
(x,y) = myTuple; Console.WriteLine($"X is: {x}"); Console.WriteLine($"Y is: {y}"); (int x1, int y1) = myTuple; Console.WriteLine($"x1 is: {x}"); Console.WriteLine($"y1 is: {y}");

New in C# 10, the assignment and declaration can be mixed, as the following shows:
C# 10 中的新增功能是,赋值和声明可以混合使用,如下所示:

int x2 = 0;
(x2, int y2) = myTuple; Console.WriteLine($"x2 is: {x}"); Console.WriteLine($"y2 is: {y}");

There is another use for this pattern that can be helpful, and that is deconstructing custom types. Take a shorter version of the Point structure used earlier in this chapter. A new method named Deconstruct() has been added to return the individual properties of the Point instance as a tuple with properties named XPos and YPos.
此模式还有另一个有用的用途,那就是解构自定义类型。以本章前面使用的 Point 结构的较短版本为例。添加了一个名为 Deconstruct() 的新方法,用于将 Point 实例的各个属性作为元组返回,其中包含名为 XPos 和 YPos 的属性。

struct Point
{
// Fields of the structure. public int X;
public int Y;

// A custom constructor.
public Point(int XPos, int YPos)
{
X = XPos;
Y = YPos;
}

public (int XPos, int YPos) Deconstruct() => (X, Y);
}

Notice the new Deconstruct() method, shown in bold in the previous code listing. This method can be named anything, but by convention it is typically named Deconstruct(). This allows a single method call to get the individual values of the structure by returning a tuple.
请注意新的 Deconstruct() 方法,在前面的代码清单中以粗体显示。此方法可以命名为任何名称,但按照惯例,它通常被命名为 Deconstruct()。这允许单个方法调用通过返回元组来获取结构的各个值。

Point p = new Point(7,5);
var pointValues = p.Deconstruct(); Console.WriteLine($"X is: {pointValues.XPos}"); Console.WriteLine($"Y is: {pointValues.YPos}");

Deconstructing Tuples with Positional Pattern Matching (New 8.0)

使用位置模式匹配解构元组(新 8.0)

When tuples have an accessible Deconstruct() method, the deconstruction can happen implicitly without having to call the Deconstruct() method. The following code shows this implicit deconstruction:
当元组具有可访问的 Deconstruct() 方法时,解构可以隐式发生,而无需调用 Deconstruct() 方法。以下代码显示了这种隐式解构:

Point p2 = new Point(8,3); int xp2 = 0;
int yp2 = 0; (xp2,yp2) = p2;
Console.WriteLine($"XP2 is: {xp2}"); Console.WriteLine($"YP2 is: {yp2}");

Additionally, the deconstruction can be used in a tuple-based switch expression. Using the Point example, the following code uses the generated tuple and uses those values for the when clause of each expression:
此外,解构可用于基于元组的开关表达式。使用 Point 示例,以下代码使用生成的元组,并将这些值用于每个表达式的 when 子句:

static string GetQuadrant1(Point p)
{
return p.Deconstruct() switch
{
(0, 0) => "Origin",
var (x, y) when x > 0 && y > 0 => "One", var (x, y) when x < 0 && y > 0 => "Two", var (x, y) when x < 0 && y < 0 => "Three", var (x, y) when x > 0 && y < 0 => "Four", var (, ) => "Border",
};
}

If the Deconstruct() method is defined with two out parameters, then the switch expression will automatically deconstruct the point. Add another Deconstruct method to the Point as follows:
如果 Deconstruct() 方法使用两个 out 参数定义,则 switch 表达式将自动解构该点。将另一个解构方法添加到 Point,如下所示:

public void Deconstruct(out int XPos, out int YPos)
=> (XPos,YPos)=(X, Y);

Now you can update (or add a new) GetQuadrant() method to this:

static string GetQuadrant2(Point p)
{
return p switch
{
(0, 0) => "Origin",

var (x, y) when x > 0 && y > 0 => "One", var (x, y) when x < 0 && y > 0 => "Two", var (x, y) when x < 0 && y < 0 => "Three", var (x, y) when x > 0 && y < 0 => "Four", var (, ) => "Border",
};
}

The change is very subtle (and is highlighted in bold). Instead of calling p.Deconstruct(), just the Point variable is used in the switch expression.
更改非常微妙(并以粗体突出显示)。而不是调用 p.Deconstruct(),只需点变量用于开关表达式。

Summary
总结
This chapter began with an examination of arrays. Then, we discussed the C# keywords that allow you to build custom methods. Recall that by default parameters are passed by value; however, you may pass a parameter by reference if you mark it with ref or out. You also learned about the role of optional or named parameters and how to define and invoke methods taking parameter arrays.
本章从对数组的检查开始。然后,我们讨论了允许您生成自定义方法的 C# 关键字。回想一下,默认情况下参数是按值传递的;但是,如果使用 ref 或 out 标记参数,则可以通过引用传递参数。您还了解了可选或命名参数的角色,以及如何定义和调用采用参数数组的方法。

After you investigated the topic of method overloading, the bulk of this chapter examined several details regarding how enumerations and structures are defined in C# and represented within the .NET Core base class libraries. Along the way, you examined several details regarding value types and reference types, including how they respond when passing them as parameters to methods and how to interact with nullable data types and variables that might be null (e.g., reference type variables and nullable value type variables) using the ?, ??, and ??= operators.
在研究了方法重载的主题之后,本章的大部分内容检查了有关如何在 C# 中定义枚举和结构以及如何在 .NET Core 基类库中表示枚举和结构的几个详细信息。在此过程中,您检查了有关值类型和引用类型的几个详细信息,包括它们作为参数传递给方法时的响应方式,以及如何使用 ?、??和 ??= 运算符。

The final section of the chapter investigated a long-anticipated feature in C#, tuples. After getting an understanding of what they are and how they work, you used them to return multiple values from methods as well as to deconstruct custom types.
本章的最后一部分探讨了 C# 中期待已久的功能,即元组。在了解它们是什么以及它们如何工作之后,您使用它们从方法返回多个值以及解构自定义类型。

In Chapter 5, you will begin to dig into the details of object-oriented development.
在第5章中,您将开始深入研究面向对象开发的细节。

发表评论