Tag Archives: Pro-csharp10-with-net6

Pro C#10 CHAPTER 13 LINQ to Objects

CHAPTER 13

LINQ to Objects

Regardless of the type of application you are creating using the .NET platform, your program will certainly need to access some form of data as it executes. To be sure, data can be found in numerous locations including XML files, relational databases, in-memory collections, and primitive arrays. Historically speaking, based on the location of said data, programmers needed to use different and unrelated APIs. The Language Integrated Query (LINQ) technology set, introduced initially in .NET 3.5, provides a concise, symmetrical, and strongly typed manner to access a wide variety of data stores. In this chapter, you will begin your investigation of LINQ by focusing on LINQ to Objects.
Before you dive into LINQ to Objects proper, the first part of this chapter quickly reviews the key C# programming constructs that enable LINQ. As you work through this chapter, you will find that implicitly typed local variables, object initialization syntax, lambda expressions, extension methods, and anonymous types will be quite useful (if not occasionally mandatory).
After this supporting infrastructure is reviewed, the remainder of the chapter will introduce you to the LINQ programming model and its role in the .NET platform. Here, you will come to learn the role of query operators and query expressions, which allow you to define statements that will interrogate a data source to yield the requested result set. Along the way, you will build numerous LINQ examples that interact with data contained within arrays as well as various collection types (both generic and nongeneric) and understand the assemblies, namespaces, and types that represent the LINQ to Objects API.

■ Note The information in this chapter is the foundation for future sections and chapters of this book, including Parallel LINQ (Chapter 15) and Entity Framework Core (Chapters 21 through 23).

LINQ-Specific Programming Constructs
From a high level, LINQ can be understood as a strongly typed query language, embedded directly into the grammar of C#. Using LINQ, you can build any number of expressions that have a look and feel like that of a database SQL query. However, a LINQ query can be applied to any number of data stores, including stores that have nothing to do with a literal relational database.

■ Note Although LINQ queries can look similar to SQL queries, the syntax is not identical. In fact, many LINQ queries seem to be the exact opposite format of a similar database query! If you attempt to map LINQ directly to SQL, you will surely become frustrated. To keep your sanity, I recommend you try your best to regard LINQ queries as unique statements, which just “happen to look” like SQL.

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_13

509

When LINQ was first introduced to the .NET platform in version 3.5, the C# and VB languages were each expanded with many new programming constructs used to support the LINQ technology set. Specifically, the C# language uses the following core LINQ-centric features:
•Implicitly typed local variables
•Object/collection initialization syntax
•Lambda expressions
•Extension methods
•Anonymous types
These features have already been explored in detail within various chapters of the text. However, to get the ball rolling, let’s quickly review each feature in turn, just to make sure we are all in the proper mindset.

■ Note Because the following sections are reviews of material covered elsewhere in the book, I have not included a C# code project for this content.

Implicit Typing of Local Variables
In Chapter 3, you learned about the var keyword of C#. This keyword allows you to define a local variable without explicitly specifying the underlying data type. The variable, however, is strongly typed, as the compiler will determine the correct data type based on the initial assignment. Recall this code example from Chapter 3:

static void DeclareImplicitVars()
{
// Implicitly typed local variables. var myInt = 0;
var myBool = true;
var myString = "Time, marches on...";

// Print out the underlying type.
Console.WriteLine("myInt is a: {0}", myInt.GetType().Name); Console.WriteLine("myBool is a: {0}",
myBool.GetType().Name); Console.WriteLine("myString is a: {0}",
myString.GetType().Name);
}

This language feature is helpful, and often mandatory, when using LINQ. As you will see during this chapter, many LINQ queries will return a sequence of data types, which are not known until compile time. Given that the underlying data type is not known until the application is compiled, you obviously can’t declare a variable explicitly!

Object and Collection Initialization Syntax
Chapter 5 explored the role of object initialization syntax, which allows you to create a class or structure variable and to set any number of its public properties in one fell swoop. The result is a compact (yet still easy on the eyes) syntax that can be used to get your objects ready for use. Also recall from Chapter 10, the C# language allows you to use a similar syntax to initialize collections of objects. Consider the following code snippet, which uses collection initialization syntax to fill a List of Rectangle objects, each of which maintains two Point objects to represent an (x,y) position:

List myListOfRects = new List
{
new Rectangle {TopLeft = new Point { X = 10, Y = 10 }, BottomRight = new Point { X = 200, Y = 200}},
new Rectangle {TopLeft = new Point { X = 2, Y = 2 }, BottomRight = new Point { X = 100, Y = 100}},
new Rectangle {TopLeft = new Point { X = 5, Y = 5 }, BottomRight = new Point { X = 90, Y = 75}}
};

While you are never required to use collection/object initialization syntax, doing so results in a more compact code base. Furthermore, this syntax, when combined with implicit typing of local variables, allows you to declare an anonymous type, which is useful when creating a LINQ projection. You’ll learn about LINQ projections later in this chapter.

Lambda Expressions
The C# lambda operator (=>) was fully explored in Chapter 12. Recall that this operator allows you to build a lambda expression, which can be used any time you invoke a method that requires a strongly typed delegate as an argument. Lambdas greatly simplify how you work with delegates in that they reduce the amount of code you must author by hand. Recall that a lambda expression can be broken down into the following usage:

( ArgumentsToProcess ) => { StatementsToProcessThem }

In Chapter 12, I walked you through how to interact with the FindAll() method of the generic List class using three different approaches. After working with the raw Predicate delegate and a C# anonymous method, you eventually arrived at the following (extremely concise) iteration with this lambda expression:

static void LambdaExpressionSyntax()
{
// Make a list of integers. List list = new List();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });

// C# lambda expression.
List evenNumbers = list.FindAll(i => (i % 2) == 0);

Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers)
{

Console.Write("{0}\t", evenNumber);
}
Console.WriteLine();
}

Lambdas will be useful when working with the underlying object model of LINQ. As you will soon find out, the C# LINQ query operators are simply a shorthand notation for calling true-blue methods on a class named System.Linq.Enumerable. These methods typically require delegates (the Func<> delegate in
particular) as parameters, which are used to process your data to yield the correct result set. Using lambdas, you can streamline your code and allow the compiler to infer the underlying delegate.

Extension Methods
C# extension methods allow you to tack on new functionality to existing classes without the need to subclass.
As well, extension methods allow you to add new functionality to sealed classes and structures, which could never be subclassed in the first place. Recall from Chapter 11, when you author an extension method, the first parameter is qualified with the this keyword and marks the type being extended. Also recall that extension methods must always be defined within a static class and must, therefore, also be declared using the static keyword. Here’s an example:

namespace MyExtensions; static class ObjectExtensions
{
// Define an extension method to System.Object.
public static void DisplayDefiningAssembly(this object obj)
{
Console.WriteLine("{0} lives here:\n\t->{1}\n", obj.GetType().Name, Assembly.GetAssembly(obj.GetType()));
}
}

To use this extension, an application must import the namespace defining the extension (and possibly add a reference to the external assembly). At this point, simply import the defining namespace and
code away.

// Since everything extends System.Object, all classes and structures
// can use this extension. int myInt = 12345678;
myInt.DisplayDefiningAssembly();

System.Data.DataSet d = new System.Data.DataSet(); d.DisplayDefiningAssembly();

When you are working with LINQ, you will seldom, if ever, be required to manually build your own extension methods. However, as you create LINQ query expressions, you will be making use of numerous extension methods already defined by Microsoft. In fact, each C# LINQ query operator is a shorthand notation for making a manual call on an underlying extension method, typically defined by the System. Linq.Enumerable utility class.

Anonymous Types
The final C# language feature I’d like to quickly review is that of anonymous types, which were explored in Chapter 11. This feature can be used to quickly model the “shape” of data by allowing the compiler to generate a new class definition at compile time, based on a supplied set of name-value pairs. Recall that
this type will be composed using value-based semantics, and each virtual method of System.Object will be overridden accordingly. To define an anonymous type, declare an implicitly typed variable and specify the data’s shape using object initialization syntax.

// Make an anonymous type that is composed of another. var purchaseItem = new {
TimeBought = DateTime.Now, ItemBought =
new {Color = "Red", Make = "Saab", CurrentSpeed = 55}, Price = 34.000};

LINQ makes frequent use of anonymous types when you want to project new forms of data on the fly. For example, assume you have a collection of Person objects and want to use LINQ to obtain information on the age and Social Security number of each. Using a LINQ projection, you can allow the compiler to generate a new anonymous type that contains your information.

Understanding the Role of LINQ
That wraps up the quick review of the C# language features that allow LINQ to work its magic. However, why have LINQ in the first place? Well, as software developers, it is hard to deny that a significant amount of programming time is spent obtaining and manipulating data. When speaking of “data,” it is easy to
immediately envision information contained within relational databases. However, another popular location for data is within XML documents or simple text files.
Data can be found in numerous places beyond these two common homes for information. For instance, say you have an array or generic List type containing 300 integers and you want to obtain a subset that meets a given criterion (e.g., only the odd or even members in the container, only prime numbers, only nonrepeating numbers greater than 50). Or perhaps you are making use of the reflection APIs and need
to obtain only metadata descriptions for each class deriving from a parent class within an array of Types. Indeed, data is everywhere.
Prior to .NET 3.5, interacting with a flavor of data required programmers to use very diverse APIs.
Consider, for example, Table 13-1, which illustrates several common APIs used to access various types of data (I’m sure you can think of many other examples).

Table 13-1. Ways to Manipulate Various Types of Data

The Data You Want How to Obtain It
Relational data System.Data.dll, System.Data.SqlClient.dll, etc.
XML document data System.Xml.dll
Metadata tables The System.Reflection namespace
Collections of objects System.Array and the System.Collections/System.Collections.Generic
namespaces

Of course, nothing is wrong with these approaches to data manipulation. In fact, you can (and will) certainly make direct use of ADO.NET, the XML namespaces, reflection services, and the various collection types. However, the basic problem is that each of these APIs is an island unto itself, which offers little
in the way of integration. True, it is possible (for example) to save an ADO.NET DataSet as XML and then manipulate it via the System.Xml namespaces, but nonetheless, data manipulation remains rather asymmetrical.
The LINQ API is an attempt to provide a consistent, symmetrical way programmers can obtain and manipulate “data” in the broad sense of the term. Using LINQ, you can create directly within the C# programming language constructs called query expressions. These query expressions are based on numerous query operators that have been intentionally designed to look and feel similar (but not quite identical) to a SQL expression.
The twist, however, is that a query expression can be used to interact with numerous types of data— even data that has nothing to do with a relational database. Strictly speaking, “LINQ” is the term used to describe this overall approach to data access. However, based on where you are applying your LINQ queries, you will encounter various terms, such as the following:
•LINQ to Objects: This term refers to the act of applying LINQ queries to arrays and collections.
•LINQ to XML: This term refers to the act of using LINQ to manipulate and query XML documents.
•LINQ to Entities: This aspect of LINQ allows you to make use of LINQ queries within the ADO.NET Entity Framework (EF) Core API.
•Parallel LINQ (aka PLINQ): This allows for parallel processing of data returned from a LINQ query.
Today, LINQ is an integral part of the .NET base class libraries, managed languages, and Visual Studio itself.

LINQ Expressions Are Strongly Typed
It is also important to point out that a LINQ query expression (unlike a traditional SQL statement) is strongly typed. Therefore, the C# compiler will keep you honest and make sure that these expressions are syntactically well formed. Tools such as Visual Studio and Visual Studio Code can use metadata for useful features such as IntelliSense, autocompletion, and so forth.

The Core LINQ Assemblies
The LINQ assemblies are contained in the System.Linq namespace, which is provided by the .NET Framework as an implicit global using.

Applying LINQ Queries to Primitive Arrays
To begin examining LINQ to Objects, let’s build an application that will apply LINQ queries to various array objects. Create a Console Application project named LinqOverArray and define a static helper method within the Program.cs file named QueryOverStrings(). In this method, create a string array containing six or so items of your liking (here, I listed a batch of video games in my library). Make sure to have at least two entries that contain numerical values and a few that have embedded spaces.

static void QueryOverStrings()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
}

Now, update Program.cs to invoke QueryOverStrings().

Console.WriteLine(" Fun with LINQ to Objects \n"); QueryOverStrings();
Console.ReadLine();

When you have any array of data, it is common to extract a subset of items based on a given requirement. Maybe you want to obtain only the subitems that contain a number (e.g., System Shock 2, Uncharted 2, and Fallout 3), have some number of characters, or don’t contain embedded spaces (e.g., Morrowind or Daxter). While you could certainly perform such tasks using members of the System.Array type and a bit of elbow grease, LINQ query expressions can greatly simplify the process.
Going on the assumption that you want to obtain from the array only items that contain an embedded blank space and you want these items listed in alphabetical order, you could build the following LINQ query expression:

static void QueryOverStrings()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Build a query expression to find the items in the array
// that have an embedded space. IEnumerable subset =
from g in currentVideoGames where g.Contains(" ") orderby g
select g;

// Print out the results. foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}
}

Notice that the query expression created here makes use of the from, in, where, orderby, and select
LINQ query operators. You will dig into the formalities of query expression syntax later in this chapter. However, even now you should be able to read this statement roughly as “Give me the items inside of currentVideoGames that contain a space, ordered alphabetically.”
Here, each item that matches the search criteria has been given the name g (as in “game”); however, any
valid C# variable name would do.

IEnumerable subset = from game in currentVideoGames where game.Contains(" ") orderby game
select game;

Notice that the returned sequence is held in a variable named subset, typed as a type that implements the generic version of IEnumerable, where T is of type System.String (after all, you are querying an array of strings). After you obtain the result set, you then simply print out each item using a standard foreach construct. If you run your application, you will find the following output:

Fun with LINQ to Objects Item: Fallout 3
Item: System Shock 2 Item: Uncharted 2

Once Again, Using Extension Methods
The LINQ syntax used earlier (and the rest of this chapter) is referred to as LINQ query expressions, which is a format that is like SQL but slightly different. There is another syntax that uses extension methods that will be the syntax used in most of the examples in this book.
Create a new method named QueryOverStringsWithExtensionMethods() and enter the following code:

static void QueryOverStringsWithExtensionMethods()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Build a query expression to find the items in the array
// that have an embedded space.
IEnumerable subset =
currentVideoGames.Where(g => g.Contains(" ")).OrderBy(g => g).Select(g => g);

// Print out the results. foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}
}

Everything is the same as the previous method, except for the line in bold. This is using the extension method syntax. This syntax uses lambda expressions within each method to define the operation. For example, the lambda in the Where() method defines the condition (where a value contains a space). Just as in the query expression syntax, the letter used to indicate the value being evaluated in the lambda is arbitrary; I could have used v for video games.
While the results are the same (running this method produces the same output as the previous method using the query expression), you will see soon that the type of the result set is slightly different. For most
(if not practically all) scenarios, this difference doesn’t cause any issues, and the formats can be used interchangeably.

Once Again, Without LINQ
To be sure, LINQ is never mandatory. If you so choose, you could have found the same result set by forgoing LINQ altogether and making use of programming primitives such as if statements and for loops. Here is a method that yields the same result as the QueryOverStrings() method but in a much more verbose manner:
static void QueryOverStringsLongHand()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
string[] gamesWithSpaces = new string[5];
for (int i = 0; i < currentVideoGames.Length; i++)
{
if (currentVideoGames[i].Contains(" "))
{
gamesWithSpaces[i] = currentVideoGames[i];
}
}
// Now sort them. Array.Sort(gamesWithSpaces);
// Print out the results.
foreach (string s in gamesWithSpaces)
{
if( s != null)
{
Console.WriteLine("Item: {0}", s);
}
}
Console.WriteLine();
}
While I am sure you can think of ways to tweak the previous method, the fact remains that LINQ queries
can be used to radically simplify the process of extracting new subsets of data from a source. Rather than building nested loops, complex if/else logic, temporary data types, and so on, the C# compiler will perform the dirty work on your behalf, once you create a fitting LINQ query.

Reflecting Over a LINQ Result Set
Now, assume the Program.cs file defines an additional helper function named ReflectOverQueryResults() that will print out various details of the LINQ result set (note the parameter is a System.Object to account for multiple types of result sets).

static void ReflectOverQueryResults(object resultSet, string queryType = "Query Expressions")
{
Console.WriteLine($" Info about your query using {queryType} "); Console.WriteLine("resultSet is of type: {0}", resultSet.GetType().Name); Console.WriteLine("resultSet location: {0}", resultSet.GetType().Assembly.GetName().Name);
}

Update the core of the QueryOverStrings() method to the following:

// Build a query expression to find the items in the array
// that have an embedded space. IEnumerable subset =
from g in currentVideoGames where g.Contains(" ") orderby g
select g;

ReflectOverQueryResults(subset);

// Print out the results. foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}

When you run the application, you will see the subset variable is really an instance of the generic OrderedEnumerable<TElement, TKey> type (represented as OrderedEnumerable`2), which is an internal abstract type residing in the System.Linq.dll assembly.

Info about your query using Query Expressions resultSet is of type: OrderedEnumerable`2
resultSet location: System.Linq

Make the same change to the QueryOverStringsWithExtensionMethods() method, except for adding
"Extension Methods" for the second parameter.

// Build a query expression to find the items in the array
// that have an embedded space. IEnumerable subset =
currentVideoGames
.Where(g => g.Contains(" "))
.OrderBy(g => g)
.Select(g => g);

ReflectOverQueryResults(subset,"Extension Methods");

// Print out the results. foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}

When you run the application, you will see the subset variable is an instance of type SelectIPartitionIterator. If you remove Select(g=>g) from the query, you will be back to having an instance of type OrderedEnumerable<TElement, TKey>. What does this all mean? For most developers, not much (if anything). They both derive from IEnumerable, both can be iterated over in the same manner, and both can create a list or an array from their values.

Info about your query using Extension Methods resultSet is of type: SelectIPartitionIterator`2 resultSet location: System.Linq

LINQ and Implicitly Typed Local Variables
While the current sample program makes it relatively easy to determine that the result set can be captured as an enumeration of the string object (e.g., IEnumerable), I would guess that it is not clear that subset is really of type OrderedEnumerable<TElement, TKey>.
Given that LINQ result sets can be represented using a good number of types in various LINQ-centric namespaces, it would be tedious to define the proper type to hold a result set, because in many cases the underlying type may not be obvious or even directly accessible from your code base (and as you will see, in some cases the type is generated at compile time).
To further accentuate this point, consider the following additional helper method defined within the
Program.cs file:

static void QueryOverInts()
{
int[] numbers = {10, 20, 30, 40, 1, 2, 3, 8};

// Print only items less than 10.
IEnumerable subset = from i in numbers where i < 10 select i;

foreach (int i in subset)
{
Console.WriteLine("Item: {0}", i);
}
ReflectOverQueryResults(subset);
}

In this case, the subset variable is a completely different underlying type. This time, the type implementing the IEnumerable interface is a low-level class named WhereArrayIterator.

Item: 1
Item: 2
Item: 3
Item: 8

Info about your query resultSet is of type: WhereArrayIterator`1 resultSet location: System.Linq

Given that the exact underlying type of a LINQ query is certainly not obvious, these first examples have represented the query results as an IEnumerable variable, where T is the type of data in the returned sequence (string, int, etc.). However, this is still rather cumbersome. To add insult to injury, given that IEnumerable extends the nongeneric IEnumerable interface, it would also be permissible to capture the result of a LINQ query as follows:

System.Collections.IEnumerable subset = from i in numbers
where i < 10 select i;

Thankfully, implicit typing cleans things up considerably when working with LINQ queries.

static void QueryOverInts()
{
int[] numbers = {10, 20, 30, 40, 1, 2, 3, 8};

// Use implicit typing here...
var subset = from i in numbers where i < 10 select i;

// ...and here.
foreach (var i in subset)
{
Console.WriteLine("Item: {0} ", i);
}
ReflectOverQueryResults(subset);
}

As a rule of thumb, you will always want to make use of implicit typing when capturing the results of a LINQ query. Just remember, however, that (in most cases) the real return value is a type implementing the generic IEnumerable interface.
Exactly what this type is under the covers (OrderedEnumerable<TElement, TKey>, WhereArrayIterator, etc.) is irrelevant and not necessary to discover. As shown in the previous code example, you can simply use the var keyword within a foreach construct to iterate over the fetched data.

LINQ and Extension Methods
Although the current example does not have you author any extension methods directly, you are in fact using them seamlessly in the background. LINQ query expressions can be used to iterate over data
containers that implement the generic IEnumerable interface. However, the System.Array class type (used to represent the array of strings and array of integers) does not implement this contract.

// The System.Array type does not seem to implement the
// correct infrastructure for query expressions! public abstract class Array : ICloneable, IList, IStructuralComparable, IStructuralEquatable
{
...
}

While System.Array does not directly implement the IEnumerable interface, it indirectly gains the required functionality of this type (as well as many other LINQ-centric members) via the static System.
Linq.Enumerable class type.
This utility class defines a good number of generic extension methods (such as Aggregate(), First(), Max(), etc.), which System.Array (and other types) acquires in the background. Thus, if you apply the dot operator on the currentVideoGames local variable, you will find a good number of members not found within the formal definition of System.Array.

The Role of Deferred Execution
Another important point regarding LINQ query expressions is that when they return a sequence, they are not actually evaluated until you iterate over the resulting sequence. Formally speaking, this is termed deferred execution. The benefit of this approach is that you can apply the same LINQ query multiple times to the same container and rest assured you are obtaining the latest and greatest results. Consider the following update to the QueryOverInts() method:

static void QueryOverInts()
{
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };

// Get numbers less than ten.
var subset = from i in numbers where i < 10 select i;

// LINQ statement evaluated here!
foreach (var i in subset)
{
Console.WriteLine("{0} < 10", i);
}
Console.WriteLine();
// Change some data in the array. numbers[0] = 4;

// Evaluated again!
foreach (var j in subset)
{
Console.WriteLine("{0} < 10", j);
}

Console.WriteLine(); ReflectOverQueryResults(subset);
}

■ Note When a LINQ statement is selecting a single element (using First/FirstOrDefault, Single/SingleOrDefault, or any of the aggregation methods), the query is executed immediately. First, FirstOrDefault, Single, and SingleOrDefault are covered in the next section. The aggregation methods are covered later in this chapter.

If you were to execute the program yet again, you would find the following output. Notice that the second time you iterate over the requested sequence, you find an additional member, as you set the first item in the array to be a value less than ten.

1 < 10
2 < 10
3 < 10
8 < 10

4 < 10
1 < 10
2 < 10
3 < 10
8 < 10

One useful aspect of Visual Studio is that if you set a breakpoint before the evaluation of a LINQ query, you can view the contents during a debugging session. Simply locate your mouse cursor over the LINQ result set variable (subset in Figure 13-1). When you do, you will be given the option of evaluating the query at that time by expanding the Results View option.

Figure 13-1. Debugging LINQ expressions

DefaultIfEmpty (New 10.0)
New to C# 10, the DefaultIfEmpty() method returns the elements of the sequence or a default value if the sequence is empty. The query execution is deferred until the list is iterated. The following example shows DefaultIfEmpty() in action:

static void DefaultWhenEmpty()
{
Console.WriteLine("Default When Empty");
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };

//Returns all of the numbers
foreach (var i in numbers.DefaultIfEmpty(-1))
{
Console.Write($"{i},");
}
Console.WriteLine();
//Returns -1 since the sequence is empty
foreach (var i in (from i in numbers where i > 99 select i).DefaultIfEmpty(-1))
{

Console.Write($"{i},");
}
Console.WriteLine();
}

The Role of Immediate Execution
When you need to evaluate a LINQ expression resulting in a sequence from outside the confines of foreach logic, you can call any number of extension methods defined by the Enumerable type such as ToArray(), ToDictionary<TSource,TKey>(), and ToList(). These methods will cause a LINQ query to execute at the exact moment you call one of the extension methods to obtain a snapshot of the data. After you have done so, the snapshot of data may be independently manipulated.
Additionally, if you are seeking only a single element, the query is executed immediately.
First() returns the first member of the sequence (and should always be used with an OrderBy() or OrderByDescending()). FirstOrDefault() returns the default value for the type of item in the list if there aren’t any to return, such as when the original sequence is empty or the Where() clause filters out all elements. Single() also returns the first member of the sequence (based on the Orderby()/ OrderByDescending(), or element order if there isn’t an ordering clause). Like its similarly named counterpart, SingleOrDefault() returns the default value for the element type if there aren’t any items in the sequence (or all records are filtered out by a where clause). If no records are returned, First() and Single() throw an exception that no records were returned, while FirstOrDefault() and SingleOrDefault() simply return null. The difference between First()/FirstOrDefault() and Single()/SingleOr() is that Single()/SingleOrDefault() will throw an exception if more than one element is returned from the query.

static void ImmediateExecution()
{
Console.WriteLine(); Console.WriteLine("Immediate Execution");
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };

//get the first element in sequence order
int number = (from i in numbers select i).First(); Console.WriteLine("First is {0}", number);

//get the first in query order
number = (from i in numbers orderby i select i).First(); Console.WriteLine("First is {0}", number);

//get the one element that matches the query
number = (from i in numbers where i > 30 select i).Single(); Console.WriteLine("Single is {0}", number);

//Return null if nothing is returned
number = (from i in numbers where i > 99 select i).FirstOrDefault(); number = (from i in numbers where i > 99 select i).SingleOrDefault(); try
{
//Throw an exception if no records returned
number = (from i in numbers where i > 99 select i).First();
}

catch (Exception ex)
{

}

try
{

}

Console.WriteLine("An exception occurred: {0}", ex.Message);

//Throw an exception if no records returned
number = (from i in numbers where i > 99 select i).Single();

catch (Exception ex)
{

}

try
{

}

Console.WriteLine("An exception occurred: {0}", ex.Message);

//Throw an exception if more than one element passes the query number = (from i in numbers where i > 10 select i).Single();

catch (Exception ex)
{
Console.WriteLine("An exception occurred: {0}", ex.Message);
}
// Get data RIGHT NOW as int[].
int[] subsetAsIntArray =
(from i in numbers where i < 10 select i).ToArray();

// Get data RIGHT NOW as List.
List subsetAsListOfInts =
(from i in numbers where i < 10 select i).ToList();
}

Notice that the entire LINQ expression is wrapped within parentheses to cast it into the correct underlying type (whatever that might be) to call the extension methods of Enumerable.
Also recall from Chapter 10 that when the C# compiler can unambiguously determine the type parameter of a generic, you are not required to specify the type parameter. Thus, you could also call ToArray() (or ToList() for that matter) as follows:

int[] subsetAsIntArray =
(from i in numbers where i < 10 select i).ToArray();

The usefulness of immediate execution is obvious when you need to return the results of a LINQ query to an external caller. And, as luck would have it, this happens to be the next topic of this chapter.

Set Default for [First/Last/Single]OrDefault Methods (New 10)
The FirstOrDefault(), SingleOrDefault(), and LastOrDefault() methods have been updated in .NET 6/C# 10 to allow specification of the default value when the query doesn’t return any elements. The base version of these methods automatically sets the default value (0 for a number, null for class, etc.). Now you can programmatically set the default when no record is returned.

Take the following trivial example. The sample LINQ query doesn’t return any records. Instead of returning the default value of zero, each of the methods returns a different negative number.

static void SettingDefaults()
{
int[] numbers = Array.Empty();
var query = from i in numbers where i>100 select i; var number = query.FirstOrDefault(-1); Console.WriteLine(number);
number = query.SingleOrDefault(-2); Console.WriteLine(number);
number = query.LastOrDefault(-3); Console.WriteLine(number);
}

Returning the Result of a LINQ Query
It is possible to define a field within a class (or structure) whose value is the result of a LINQ query. To do so, however, you cannot use implicit typing (as the var keyword cannot be used for fields), and the target of the LINQ query cannot be instance-level data; therefore, it must be static. Given these limitations, you will seldom need to author code like the following:

class LINQBasedFieldsAreClunky
{
private static string[] currentVideoGames =
{"Morrowind", "Uncharted 2",
"Fallout 3", "Daxter", "System Shock 2"};

// Can't use implicit typing here! Must know type of subset!
private IEnumerable subset = from g in currentVideoGames
where g.Contains(" ") orderby g
select g;

public void PrintGames()
{
foreach (var item in subset)
{
Console.WriteLine(item);
}
}
}

Often, LINQ queries are defined within the scope of a method or property. Moreover, to simplify your programming, the variable used to hold the result set will be stored in an implicitly typed local variable using the var keyword. Now, recall from Chapter 3 that implicitly typed variables cannot be used to define parameters, return values, or fields of a class or structure.
Given this point, you might wonder exactly how you could return a query result to an external caller.
The answer is: it depends. If you have a result set consisting of strongly typed data, such as an array of

strings or a List, you could abandon the use of the var keyword and use a proper IEnumerable or IEnumerable type (again, as IEnumerable extends IEnumerable). Consider the following example for a new console application named LinqRetValues:

Console.WriteLine(" LINQ Return Values \n"); IEnumerable subset = GetStringSubset();

foreach (string item in subset)
{
Console.WriteLine(item);
}

Console.ReadLine();

static IEnumerable GetStringSubset()
{
string[] colors = {"Light Red", "Green", "Yellow", "Dark Red", "Red", "Purple"};
// Note subset is an IEnumerable-compatible object.
IEnumerable theRedColors = from c in colors where c.Contains("Red") select c; return theRedColors;
}

The results are as expected.

Light Red Dark Red Red

Returning LINQ Results via Immediate Execution
This example works as expected, only because the return value of GetStringSubset() and the LINQ query within this method has been strongly typed. If you used the var keyword to define the subset variable, it would be permissible to return the value only if the method is still prototyped to return IEnumerable (and if the implicitly typed local variable is in fact compatible with the specified return type).
Because it is a bit inconvenient to operate on IEnumerable, you could use immediate execution. For example, rather than returning IEnumerable, you could simply return a string[], if you transform the sequence to a strongly typed array. Consider this new method of the Program.cs file, which does this very thing:

static string[] GetStringSubsetAsArray()
{
string[] colors = {"Light Red", "Green", "Yellow", "Dark Red", "Red", "Purple"}; var theRedColors = from c in colors where c.Contains("Red") select c;
// Map results into an array. return theRedColors.ToArray();
}

With this, the caller can be blissfully unaware that their result came from a LINQ query and simply work with the array of strings as expected. Here’s an example:

foreach (string item in GetStringSubsetAsArray())
{
Console.WriteLine(item);
}

Immediate execution is also critical when attempting to return to the caller the results of a LINQ projection. You’ll examine this topic a bit later in the chapter. Next up, let’s look at how to apply LINQ queries to generic and nongeneric collection objects.

Applying LINQ Queries to Collection Objects
Beyond pulling results from a simple array of data, LINQ query expressions can also manipulate data within members of the System.Collections.Generic namespace, such as the List type. Create a new Console Application project named LinqOverCollections, and define a basic Car class that maintains a current speed, color, make, and pet name, as shown in the following code:

namespace LinqOverCollections; class Car
{
public string PetName {get; set;} = ""; public string Color {get; set;} = ""; public int Speed {get; set;}
public string Make {get; set;} = "";
}

Now, within your top-level statements, define a local List variable of type Car, and make use of object initialization syntax to fill the list with a handful of new Car objects.

using System.Collections; using LinqOverCollections;

Console.WriteLine(" LINQ over Generic Collections \n");

// Make a List<> of Car objects. List myCars = new List() {
new Car{ PetName = "Henry", Color = "Silver", Speed = 100, Make = "BMW"}, new Car{ PetName = "Daisy", Color = "Tan", Speed = 90, Make = "BMW"},
new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW"}, new Car{ PetName = "Clunker", Color = "Rust", Speed = 5, Make = "Yugo"}, new Car{ PetName = "Melvin", Color = "White", Speed = 43, Make = "Ford"}
};

Console.ReadLine();

Accessing Contained Subobjects
Applying a LINQ query to a generic container is no different from doing so with a simple array, as LINQ to Objects can be used on any type implementing IEnumerable. This time, your goal is to build a query expression to select only the Car objects within the myCars list, where the speed is greater than 55.
After you get the subset, you will print out the name of each Car object by calling the PetName property. Assume you have the following helper method (taking a List parameter), which is called from the top- level statements:

static void GetFastCars(List myCars)
{
// Find all Car objects in the List<>, where the Speed is
// greater than 55.
var fastCars = from c in myCars where c.Speed > 55 select c;

foreach (var car in fastCars)
{
Console.WriteLine("{0} is going too fast!", car.PetName);
}
}

Notice that your query expression is grabbing only those items from the List where the Speed property is greater than 55. If you run the application, you will find that Henry and Daisy are the only two items that match the search criteria.
If you want to build a more complex query, you might want to find only the BMWs that have a Speed
value greater than 90. To do so, simply build a compound Boolean statement using the C# && operator.

static void GetFastBMWs(List myCars)
{
// Find the fast BMWs!
var fastCars = from c in myCars where c.Speed > 90 && c.Make == "BMW" select c; foreach (var car in fastCars)
{
Console.WriteLine("{0} is going too fast!", car.PetName);
}
}

In this case, the only pet name printed out is Henry.

Applying LINQ Queries to Nongeneric Collections
Recall that the query operators of LINQ are designed to work with any type implementing IEnumerable (either directly or via extension methods). Given that System.Array has been provided with such necessary infrastructure, it might surprise you that the legacy (nongeneric) containers within System.Collections have not. Thankfully, it is still possible to iterate over data contained within nongeneric collections using the generic Enumerable.OfType() extension method.
When calling OfType() from a nongeneric collection object (such as the ArrayList), simply specify the type of item within the container to extract a compatible IEnumerable object. In code, you can store this data point using an implicitly typed variable.
Consider the following new method, which fills an ArrayList with a set of Car objects (be sure to import the System.Collections namespace into your Program.cs file):

static void LINQOverArrayList()
{
Console.WriteLine(" LINQ over ArrayList ");
// Here is a nongeneric collection of cars. ArrayList myCars = new ArrayList() {
new Car{ PetName = "Henry", Color = "Silver", Speed = 100, Make = "BMW"}, new Car{ PetName = "Daisy", Color = "Tan", Speed = 90, Make = "BMW"},
new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW"}, new Car{ PetName = "Clunker", Color = "Rust", Speed = 5, Make = "Yugo"}, new Car{ PetName = "Melvin", Color = "White", Speed = 43, Make = "Ford"}
};
// Transform ArrayList into an IEnumerable-compatible type. var myCarsEnum = myCars.OfType();
// Create a query expression targeting the compatible type.
var fastCars = from c in myCarsEnum where c.Speed > 55 select c; foreach (var car in fastCars)
{
Console.WriteLine("{0} is going too fast!", car.PetName);
}
}
Like the previous examples, this method, when called from the top-level statements, will display only
the names Henry and Daisy, based on the format of the LINQ query.

Filtering Data Using OfType()
As you know, nongeneric types can contain any combination of items, as the members of these containers (again, such as the ArrayList) are prototyped to receive System.Objects. For example, assume an ArrayList contains a variety of items, only a subset of which are numerical. If you want to obtain a subset that contains only numerical data, you can do so using OfType() since it filters out each element whose type is different from the given type during the iterations.

static void OfTypeAsFilter()
{
// Extract the ints from the ArrayList. ArrayList myStuff = new ArrayList();
myStuff.AddRange(new object[] { 10, 400, 8, false, new Car(), "string data" }); var myInts = myStuff.OfType();

// Prints out 10, 400, and 8. foreach (int i in myInts)
{
Console.WriteLine("Int value: {0}", i);
}
}
At this point, you have had a chance to apply LINQ queries to arrays, generic collections, and
nongeneric collections. These containers held both C# primitive types (integers, string data) as well as custom classes. The next task is to learn about many additional LINQ operators that can be used to build more complex and useful queries.

Investigating the C# LINQ Query Operators
C# defines a good number of query operators out of the box. Table 13-2 documents some of the more commonly used query operators. In addition to the partial list of operators shown in Table 13-2, the System.Linq.Enumerable class provides a set of methods that do not have a direct C# query operator shorthand notation but are instead exposed as extension methods. These generic methods can be called to transform a result set in various manners (Reverse<>(), ToArray<>(), ToList<>(), etc.). Some are used to extract singletons from a result set, others perform various set operations (Distinct<>(), Union<>(), Intersect<>(), etc.), and still others aggregate results (Count<>(), Sum<>(), Min<>(), Max<>(), etc.).

Table 13-2. Common LINQ Query Operators

Query Operators Meaning in Life
from, in Used to define the backbone for any LINQ expression, which allows you to extract a subset of data from a fitting container.
where Used to define a restriction for which items to extract from a container.
select Used to select a sequence from the container.
join, on, equals, into Performs joins based on specified key. Remember, these “joins” do not need to have anything to do with data in a relational database.
orderby, ascending, descending Allows the resulting subset to be ordered in ascending or descending order.
groupby Yields a subset with data grouped by a specified value.

To begin digging into more intricate LINQ queries, create a new Console Application project named FunWithLinqExpressions. Next, you need to define an array or collection of some sample data. For this project, you will make an array of ProductInfo objects, defined in the following code:

namespace FunWithLinqExpressions; class ProductInfo
{
public string Name {get; set;} = "";
public string Description {get; set;} = ""; public int NumberInStock {get; set;} = 0;

public override string ToString()
=> $"Name={Name}, Description={Description}, Number in Stock={NumberInStock}";
}

Now populate an array with a batch of ProductInfo objects within your calling code.

Console.WriteLine(" Fun with Query Expressions \n");

// This array will be the basis of our testing... ProductInfo[] itemsInStock = new[] {
new ProductInfo{ Name = "Mac's Coffee", Description = "Coffee with TEETH", NumberInStock = 24},

new ProductInfo{ Name = "Milk Maid Milk", Description = "Milk cow's love", NumberInStock = 100},
new ProductInfo{ Name = "Pure Silk Tofu", Description = "Bland as Possible", NumberInStock = 120},
new ProductInfo{ Name = "Crunchy Pops", Description = "Cheezy, peppery goodness", NumberInStock = 2},
new ProductInfo{ Name = "RipOff Water", Description = "From the tap to your wallet", NumberInStock = 100},
new ProductInfo{ Name = "Classic Valpo Pizza", Description = "Everyone loves pizza!", NumberInStock = 73}
};

// We will call various methods here! Console.ReadLine();

Basic Selection Syntax
Because the syntactical correctness of a LINQ query expression is validated at compile time, you need to remember that the ordering of these operators is critical. In the simplest terms, every LINQ query expression is built using the from, in, and select operators. Here is the general template to follow:

var result =
from matchingItem in container select matchingItem;

The item after the from operator represents an item that matches the LINQ query criteria, which can be named anything you choose. The item after the in operator represents the data container to search (an array, collection, XML document, etc.).
Here is a simple query, doing nothing more than selecting every item in the container (similar in behavior to a database Select * SQL statement). Consider the following:

static void SelectEverything(ProductInfo[] products)
{
// Get everything!
Console.WriteLine("All product details:");
var allProducts = from p in products select p;

foreach (var prod in allProducts)
{
Console.WriteLine(prod.ToString());
}
}

To be honest, this query expression is not entirely useful, given that your subset is identical to that of the data in the incoming parameter. If you want, you could extract only the Name values of each car using the following selection syntax:

static void ListProductNames(ProductInfo[] products)
{
// Now get only the names of the products. Console.WriteLine("Only product names:");

var names = from p in products select p.Name;

foreach (var n in names)
{
Console.WriteLine("Name: {0}", n);
}
}

Obtaining Subsets of Data
To obtain a specific subset from a container, you can use the where operator. When doing so, the general template now becomes the following code:

var result = from item
in container
where BooleanExpression select item;

Notice that the where operator expects an expression that resolves to a Boolean. For example, to extract from the ProductInfo[] argument only the items that have more than 25 items on hand, you could author the following code:

static void GetOverstock(ProductInfo[] products)
{
Console.WriteLine("The overstock items!");

// Get only the items where we have more than
// 25 in stock. var overstock =
from p
in products
where p.NumberInStock > 25 select p;

foreach (ProductInfo c in overstock)
{
Console.WriteLine(c.ToString());
}
}

As shown earlier in this chapter, when you are building a where clause, it is permissible to make use of any valid C# operators to build complex expressions. For example, recall this query that extracts only the BMWs going at least 100 MPH:

// Get BMWs going at least 100 MPH. var onlyFastBMWs =
from c
in myCars
where c.Make == "BMW" && c.Speed >= 100 select c;

Paging Data
If you need to get a certain number of records, you can use the Take()/TakeWhile()/TakeLast() and Skip()/SkipWhile()/SkipLast() methods. The first set of methods returns the specified number of records (Take()), all of the records where the condition is true (TakeWhile()), or the last specified number of records (TakeLast()). The second set of methods passes over the specified number of records (Skip()), skips all
of the records where the condition is true (SkipWhile()), or skips the last specified number of records (SkipLast()).
These methods are exposed through the IEnumerable interface, so while you can’t use the paging operators on the LINQ statements directly, you can use them on the result of the LINQ statement.
Begin by creating a new method named PagingWithLinq(). The first example will take the first three records from the list and then pass the result to a local function for display. The paging methods (like the where clause) participate in deferred execution, so the return type is also an IEnumerable. Here is the method with the first example and local helper function:

static void PagingWithLINQ(ProductInfo[] products)
{
Console.WriteLine("Paging Operations");

IEnumerable list = (from p in products select p).Take(3); OutputResults("The first 3",list);

static void OutputResults(string message, IEnumerable products)
{
Console.WriteLine(message);
foreach (ProductInfo c in products)
{
Console.WriteLine(c.ToString());
}
}
}

The TakeWhile() method takes records as long as a condition is true. The condition is passed into the method as a lambda expression, like this:

list = (from p in products select p).TakeWhile(x=>x.NumberInStock>20); OutputResults("All while number in stock > 20",list);

The preceding code returns the first three in the list, as the fourth record (“Crunchy Pops”) has only two items in stock. It’s important to note that the method stops taking records when the condition fails, even though there are two more items that would have passed the condition. If you need to take all the items into consideration, then add an orderby clause to the list before calling TakeWhile().
The TakeLast() method takes the last specified number of records.

list = (from p in products select p).TakeLast(2); OutputResults("The last 2",list);

The Skip() and SkipWhile() methods work in the same manner, only skipping records instead of taking records. The next example skips the first three records and then returns the rest:

list = (from p in products select p).Skip(3); OutputResults("Skipping the first 3",list);

To skip the records where the number in stock is greater than 20, use the following code. Note that the same issue of sorting exists with SkipWhile() as TakeWhile(). Both methods are best used when the records are sorted accordingly.

list = (from p in products select p).SkipWhile(x=>x.NumberInStock>20); OutputResults("Skip while number in stock > 20",list);

The SkipLast() method takes all but the last specified number of records:

list = (from p in products select p).SkipLast(2); OutputResults("All but the last 2",list);

These methods can be combined to truly “page” the data. To skip three records and take two, execute the following code:

list = (from p in products select p).Skip(3).Take(2); OutputResults("Skip 3 take 2",list);

Paging Data with Ranges (New 10.0)
Support for using ranges in the Take() method has been added in .NET 6/C# 10, enabling paging without needing the Take() and Skip() methods used together. Note that the TakeWhile() and SkipWhile() methods have not been updated to accept ranges.
Here is a new method named PagingWithRanges() that repeats the calls to Take() and Skip() from the previous example, using ranges to retrieve the same data:

static void PagingWithRanges(ProductInfo[] products)
{
Console.WriteLine("Paging Operations");

IEnumerable list = (from p in products select p).Take(..3); OutputResults("The first 3",list);

list = (from p in products select p).Take(3..); OutputResults("Skipping the first 3",list);

list = (from p in products select p).Take(^2..); OutputResults("The last 2",list);

list = (from p in products select p).Take(3..5); OutputResults("Skip 3 take 2",list);

list = (from p in products select p).Take(..^2); OutputResults("Skip the last 2",list);

static void OutputResults(string message, IEnumerable products)
{
Console.WriteLine(message);
foreach (ProductInfo c in products)
{

Console.WriteLine(c.ToString());
}
}
}

Paging Data with Chunks (New 10.0)
Chunk() is another new method for paging has been added in .NET 6/C# 10. This method takes one parameter (size) and then splits the source into an Enumerable of Enumerables. For example, if we take the ProductInfo list and apply Chunk(2), the return value is a list of three lists, each inner list containing two records. If the list can’t be split evenly, the last list will hold fewer records.
Here is a new method named PagingWithChunks() that demonstrates the Chunk() method:

static void PagingWithChunks(ProductInfo[] products)
{
Console.WriteLine("Chunking Operations");

IEnumerable<ProductInfo[]> chunks = products.Chunk(size:2); var counter = 0;
foreach (var chunk in chunks)
{
OutputResults($"Chunk #{++counter}",chunk);
}
static void OutputResults(string message, IEnumerable products)
{
Console.WriteLine(message);
foreach (ProductInfo c in products)
{
Console.WriteLine(c.ToString());
}
}
}

Projecting New Data Types
It is also possible to project new forms of data from an existing data source. Let’s assume you want to take the incoming ProductInfo[] parameter and obtain a result set that accounts only for the name and description of each item. To do so, you can define a select statement that dynamically yields a new anonymous type.

static void GetNamesAndDescriptions(ProductInfo[] products)
{
Console.WriteLine("Names and Descriptions:"); var nameDesc =
from p
in products
select new { p.Name, p.Description };

foreach (var item in nameDesc)
{

// Could also use Name and Description properties
// directly. Console.WriteLine(item.ToString());
}
}

Always remember that when you have a LINQ query that makes use of a projection, you have no way of knowing the underlying data type, as this is determined at compile time. In these cases, the var keyword is mandatory. As well, recall that you cannot create methods with implicitly typed return values. Therefore, the following method would not compile:

static var GetProjectedSubset(ProductInfo[] products)
{
var nameDesc =
from p in products select new { p.Name, p.Description }; return nameDesc; // Nope!
}

When you need to return projected data to a caller, one approach is to transform the query result into a System.Array object using the ToArray() extension method. Thus, if you were to update your query expression as follows:

// Return value is now an Array.
static Array GetProjectedSubset(ProductInfo[] products)
{
var nameDesc =
from p in products select new { p.Name, p.Description };

// Map set of anonymous objects to an Array object. return nameDesc.ToArray();
}

you could invoke and process the data as follows:

Array objs = GetProjectedSubset(itemsInStock); foreach (object o in objs)
{
Console.WriteLine(o); // Calls ToString() on each anonymous object.
}

Note that you must use a literal System.Array object and cannot use the C# array declaration syntax, given that you don’t know the underlying type because you are operating on a compiler-generated anonymous class! Also note that you are not specifying the type parameter to the generic ToArray() method, as you once again don’t know the underlying data type until compile time, which is too late for your purposes.
The obvious problem is that you lose any strong typing, as each item in the Array object is assumed to be of type Object. Nevertheless, when you need to return a LINQ result set that is the result of a projection operation to an anonymous type, transforming the data into an Array type (or another suitable container via other members of the Enumerable type) is mandatory.

Projecting to Different Data Types
In addition to projecting into anonymous types, you can project the results of your LINQ query into another concrete type. This allows for static typing and using IEnumerable as the result set. To start, create a smaller version of the ProductInfo class.

namespace FunWithLinqExpressions; class ProductInfoSmall
{
public string Name {get; set;} = "";
public string Description {get; set;} = ""; public override string ToString()
=> $"Name={Name}, Description={Description}";
}

The next change is to project the query results into a collection of ProductInfoSmall objects, instead of anonymous types. Add the following method to your class:

static void GetNamesAndDescriptionsTyped( ProductInfo[] products)
{
Console.WriteLine("Names and Descriptions:"); IEnumerable nameDesc =
from p
in products
select new ProductInfoSmall
{ Name=p.Name, Description=p.Description };

foreach (ProductInfoSmall item in nameDesc)
{
Console.WriteLine(item.ToString());
}
}

With LINQ projections, you have choices for which method you use (anonymous or strong-typed objects). Which decision you make depends entirely on your business need.

Obtaining Counts Using Enumerable
When you are projecting new batches of data, you may need to discover exactly how many items have been returned into the sequence. Any time you need to determine the number of items returned from a LINQ query expression, simply use the Count() extension method of the Enumerable class. For example, the following method will find all string objects in a local array that have a length greater than six characters:

static void GetCountFromQuery()
{
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Get count from the query.
int numb = (from g in currentVideoGames where g.Length > 6 select g).Count();

// Print out the number of items.
Console.WriteLine("{0} items honor the LINQ query.", numb);
}

Obtaining Nonenumerated Counts (New 10.0)
Introduced in .NET 6/C# 10, the TryGetNonEnumeratedCount() method attempts to get the total count of an IEnumerable without actually enumerating the list. If the list must be enumerated over to get the count, the method fails.
The following code demonstrates an “easy” count that doesn’t cause a performance issue by enumerating the entire list:

static void GetUnenumeratedCount(ProductInfo[] products)
{
Console.WriteLine("Get Unenumeratord Count"); IEnumerable query = from p in products select p; var result = query.TryGetNonEnumeratedCount(out int count); if (result)
{
Console.WriteLine($"Total count:{count}");
}
else
{
Console.WriteLine("Try Get Count Failed");
}
}

The following updated code fails when calling the GetProducts() local function, since the yield return must be enumerated:

static void GetUnenumeratedCount(ProductInfo[] products)
{
Console.WriteLine("Get Unenumeratord Count");
//omitted for brevity
var newResult = GetProduct(products).TryGetNonEnumeratedCount(out int newCount); if (newResult)
{
Console.WriteLine($"Total count:{newCount}");
}
else
{
Console.WriteLine("Try Get Count Failed");
}
static IEnumerable GetProduct(ProductInfo[] products)
{
for (int i = 0;i < products.Count();i++)
{
yield return products[i];
}
}
}

Reversing Result Sets
You can reverse the items within a result set quite simply using the Reverse() extension method of the Enumerable class. For example, the following method selects all items from the incoming ProductInfo[] parameter, in reverse:

static void ReverseEverything(ProductInfo[] products)
{
Console.WriteLine("Product in reverse:");
var allProducts = from p in products select p; foreach (var prod in allProducts.Reverse())
{
Console.WriteLine(prod.ToString());
}
}

Sorting Expressions
As you have seen in this chapter’s initial examples, a query expression can take an orderby operator to sort items in the subset by a specific value. By default, the order will be ascending; thus, ordering by a string would be alphabetical, ordering by numerical data would be lowest to highest, and so forth. If you need to view the results in descending order, simply include the descending operator. Ponder the following method:

static void AlphabetizeProductNames(ProductInfo[] products)
{
// Get names of products, alphabetized.
var subset = from p in products orderby p.Name select p;

Console.WriteLine("Ordered by Name:"); foreach (var p in subset)
{
Console.WriteLine(p.ToString());
}
}

Although ascending order is the default, you can make your intentions clear by using the ascending
operator.

var subset = from p in products orderby p.Name ascending select p;

If you want to get the items in descending order, you can do so via the descending operator.

var subset = from p in products orderby p.Name descending select p;

LINQ As a Better Venn Diagramming Tool
The Enumerable class supports a set of extension methods that allows you to use two (or more) LINQ queries as the basis to find unions, differences, concatenations, and intersections of data. First, consider the Except() extension method, which will return a LINQ result set that contains the difference between two containers, which, in this case, is the value Yugo.

static void DisplayDiff()
{
List myCars =
new List {"Yugo", "Aztec", "BMW"}; List yourCars =
new List{"BMW", "Saab", "Aztec" };

var carDiff =
(from c in myCars select c)
.Except(from c2 in yourCars select c2);

Console.WriteLine("Here is what you don't have, but I do:"); foreach (string s in carDiff)
{
Console.WriteLine(s); // Prints Yugo.
}
}

The Intersect() method will return a result set that contains the common data items in a set of containers. For example, the following method returns the sequence Aztec and BMW:

static void DisplayIntersection()
{
List myCars = new List { "Yugo", "Aztec", "BMW" }; List yourCars = new List { "BMW", "Saab", "Aztec" };

// Get the common members. var carIntersect =
(from c in myCars select c)
.Intersect(from c2 in yourCars select c2);

Console.WriteLine("Here is what we have in common:"); foreach (string s in carIntersect)
{
Console.WriteLine(s); // Prints Aztec and BMW.
}
}

The Union() method, as you would guess, returns a result set that includes all members of a batch of LINQ queries. Like any proper union, you will not find repeating values if a common member appears more than once. Therefore, the following method will print out the values Yugo, Aztec, BMW, and Saab:

static void DisplayUnion()
{
List myCars =
new List { "Yugo", "Aztec", "BMW" }; List yourCars =
new List { "BMW", "Saab", "Aztec" };

// Get the union of these containers. var carUnion =

(from c in myCars select c)
.Union(from c2 in yourCars select c2);

Console.WriteLine("Here is everything:"); foreach (string s in carUnion)
{
Console.WriteLine(s); // Prints all common members.
}
}

Finally, the Concat() extension method returns a result set that is a direct concatenation of LINQ result sets. For example, the following method prints out the results Yugo, Aztec, BMW, BMW, Saab, and Aztec:

static void DisplayConcat()
{
List myCars =
new List { "Yugo", "Aztec", "BMW" }; List yourCars =
new List { "BMW", "Saab", "Aztec" };

var carConcat =
(from c in myCars select c)
.Concat(from c2 in yourCars select c2);

// Prints:
// Yugo Aztec BMW BMW Saab Aztec. foreach (string s in carConcat)
{
Console.WriteLine(s);
}
}

Venn Diagramming with Selectors (New 10.0)
A new set of methods was introduced in .NET 6/C# 10 that adds the ability to use a selector when finding unions, differences, and intersections of data. The selectors use a specific property of the objects in the lists to determine the action to perform.
The ExceptBy() extension method uses the selector to remove the records from the first set where the value of the selector exists in the second set. The following method creates two lists of tuples and uses the Age property for the selector. Both Claire and Pat have the same age as Lindsey, but Age values for
Francis and Ashley are not in the second list. So the result of the ExceptBy() extension method is Francis
and Ashley:

static void DisplayDiffBySelector()
{
var first = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
var second = new (string Name, int Age)[] { ("Claire", 30), ("Pat", 30), ("Drew", 33) }; var result = first.ExceptBy(second.Select(x=>x.Age), product => product.Age); // } Console.WriteLine("Except for by selector:");
foreach (var item in result)

{
Console.WriteLine(item); // { ("Francis", 20), ("Ashley", 40) }
}
}

The IntersectBy() method will return a result set that contains the common data items in a set of containers based on the selector. The following method returns the tuple Lindsey. Note that even though Claire and Pat also have the same Age value as Lindsey, they are not returned since the IntersectBy() method returns only one result per selector value.

static void DisplayIntersectionBySelector()
{
var first = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
var second = new (string Name, int Age)[] { ("Claire", 30), ("Pat", 30), ("Drew", 33) }; var result = first.IntersectBy(second.Select(x=>x.Age), person => person.Age); Console.WriteLine("Intersection by selector:");
foreach (var item in result)
{
Console.WriteLine(item); // { ("Lindsey", 30) }
}
}

Reversing the call between the first and second lists still produces one result, which is the first tuple in the second list with the selector value of 30.

var result = second.IntersectBy(first.Select(x=>x.Age), person => person.Age);
// returns (“Claire”,30)

The UnionBy() method returns a result set that includes all the values for the selector, and the first member of the combined lists that have a value that matches the list. In the following method, notice that while each of the Age values is represented in the result, Claire and Pat are not, since their Age is already represented by Lindsey:

static void DisplayUnionBySelector()
{
var first = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
var second = new (string Name, int Age)[] { ("Claire", 30), ("Pat", 30), ("Drew", 33) }; var result = first.UnionBy(second, person => person.Age);
Console.WriteLine("Union by selector:"); foreach (var item in result)
{
Console.WriteLine(item); // { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40),
("Drew", 33) };
}
}

These new methods, ExceptBy(), IntersectBy(), and UnionBy(), add a lot of power to your list arsenal, but as you saw, they might not always do what you expect them to do.

Removing Duplicates
When you call the Concat() extension method, you could very well end up with redundant entries in the fetched result, which could be exactly what you want in some cases. However, in other cases, you might want to remove duplicate entries in your data. To do so, simply call the Distinct() extension method, as shown here:

static void DisplayConcatNoDups()
{
List myCars =
new List { "Yugo", "Aztec", "BMW" }; List yourCars =
new List { "BMW", "Saab", "Aztec" };

var carConcat =
(from c in myCars select c)
.Concat(from c2 in yourCars select c2);

// Prints:
// Yugo Aztec BMW Saab.
foreach (string s in carConcat.Distinct())
{
Console.WriteLine(s);
}
}

Removing Duplicates with Selectors (New 10.0)
Another new method in .NET 6/C# 10 for working with lists is the DistinctBy() method. This method, like its cousins UnionBy(), IntersectBy(), and ExceptBy(), uses a selector to perform its function. The
following example gets the distinct records by Age. Notice that the final list only includes Lindsey of the three tuples that have 30 for their Age value. The accepted tuple is the one that appears first in the list.

static void DisplayConcatNoDupsBySelector()
{
var first = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
var second = new (string Name, int Age)[] { ("Claire", 30), ("Pat", 30), ("Drew", 33) }; var result = first.Concat(second).DistinctBy(x=>x.Age);
Console.WriteLine("Distinct by selector:"); foreach (var item in result)
{
Console.WriteLine(item); // { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40),
("Drew", 33) };
}
}

LINQ Aggregation Operations
LINQ queries can also be designed to perform various aggregation operations on the result set. The Count() extension method is one such aggregation example. Other possibilities include obtaining an average, maximum, minimum, or sum of values using the Max(), Min(), Average(), or Sum() members of the Enumerable class. Here is a simple example:

static void AggregateOps()
{
double[] winterTemps = { 2.0, -21.3, 8, -4, 0, 8.2 };

// Various aggregation examples. Console.WriteLine("Max temp: {0}",
(from t in winterTemps select t).Max());

Console.WriteLine("Min temp: {0}",
(from t in winterTemps select t).Min());

Console.WriteLine("Average temp: {0}",
(from t in winterTemps select t).Average());

Console.WriteLine("Sum of all temps: {0}", (from t in winterTemps select t).Sum());
}

Aggregation with Selectors (New 10.0)
Two new aggregate functions introduced in .NET 6/C# 10 are MaxBy() and MinBy(). Using the ProductInfo
list from earlier, here is a method that gets the max and min, using the NumberInStock property:

static void AggregateOpsBySelector(ProductInfo[] products)
{
Console.WriteLine("Max by In Stock: {0}", products.MaxBy(x=>x.NumberInStock)); Console.WriteLine("Min temp: {0}", products.MinBy(x=>x.NumberInStock));
}

The Internal Representation of LINQ Query Statements
While this is not a complete LINQ reference, the previous examples in this chapter should give you enough knowledge to feel comfortable with the process of building LINQ query expressions. You will see further examples later in this text, especially in the Entity Framework Core chapters. To wrap up your first look at LINQ, the remainder of this chapter will dive into the details between the C# LINQ query operators and the underlying object model.
At this point, you have been introduced to the process of building query expressions using various C# query operators (such as from, in, where, orderby, and select). Also, you discovered that some functionality of the LINQ to Objects API can be accessed only when calling extension methods of the Enumerable class.
The truth of the matter, however, is that when LINQ queries are compiled, the C# compiler translates all C# LINQ operators into calls on methods of the Enumerable class.
A great many of the methods of Enumerable have been prototyped to take delegates as arguments. Many methods require a generic delegate named Func<>, which was introduced to you during your examination of

generic delegates in Chapter 10. Consider the Where() method of Enumerable, which is called on your behalf when you use the C# where LINQ query operator.

// Overloaded versions of the Enumerable.Where() method.
// Note the second parameter is of type System.Func<>.
public static IEnumerable Where( this IEnumerable source, System.Func<TSource,int,bool> predicate)

public static IEnumerable Where( this IEnumerable source, System.Func<TSource,bool> predicate)

The Func<> delegate (as the name implies) represents a pattern for a given function with a set of up to 16 arguments and a return value. If you were to examine this type using the Visual Studio Object Browser, you would notice various forms of the Func<> delegate. Here’s an example:

// The various formats of the Func<> delegate.
public delegate TResult Func<T1,T2,T3,T4,TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4) public delegate TResult Func<T1,T2,T3,TResult>(T1 arg1, T2 arg2, T3 arg3)
public delegate TResult Func<T1,T2,TResult>(T1 arg1, T2 arg2) public delegate TResult Func<T1,TResult>(T1 arg1)
public delegate TResult Func()

Given that many members of System.Linq.Enumerable demand a delegate as input, when invoking them, you can either manually create a new delegate type and author the necessary target methods, use a C# anonymous method, or define a proper lambda expression. Regardless of which approach you take, the result is identical.
While it is true that using C# LINQ query operators is far and away the simplest way to build a LINQ query expression, let’s walk through each of these possible approaches, just so you can see the connection between the C# query operators and the underlying Enumerable type.

Building Query Expressions with Query Operators (Revisited)
To begin, create a new Console Application project named LinqUsingEnumerable. The Program.cs file will define a series of static helper methods (each of which is called within the top-level statements) to illustrate the various manners in which you can build LINQ query expressions.
The first method, QueryStringsWithOperators(), offers the most straightforward way to build a query expression and is identical to the code shown in the LinqOverArray example earlier in this chapter.

static void QueryStringWithOperators()
{
Console.WriteLine(" Using Query Operators ");

string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

var subset = from game in currentVideoGames
where game.Contains(" ") orderby game select game;

foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}
}

The obvious benefit of using C# query operators to build query expressions is that the Func<> delegates and calls on the Enumerable type are abstracted away from your code, as it is the job of the C# compiler to perform this translation. To be sure, building LINQ expressions using various query operators (from, in, where, or orderby) is the most common and straightforward approach.

Building Query Expressions Using the Enumerable Type and Lambda Expressions
Keep in mind that the LINQ query operators used here are simply shorthand versions for calling various extension methods defined by the Enumerable type. Consider the following
QueryStringsWithEnumerableAndLambdas() method, which is processing the local string array now making direct use of the Enumerable extension methods:

static void QueryStringsWithEnumerableAndLambdas()
{
Console.WriteLine(" Using Enumerable / Lambda Expressions ");

string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Build a query expression using extension methods
// granted to the Array via the Enumerable type. var subset = currentVideoGames
.Where(game => game.Contains(" "))
.OrderBy(game => game).Select(game => game);

// Print out the results. foreach (var game in subset)
{
Console.WriteLine("Item: {0}", game);
}
Console.WriteLine();
}

Here, you begin by calling the Where() extension method on the currentVideoGames string array.
Recall that the Array class receives this via an extension method granted by Enumerable. The Enumerable. Where() method requires a System.Func<T1, TResult> delegate parameter. The first type parameter of this delegate represents the IEnumerable-compatible data to process (an array of strings in this case), while the second type parameter represents the method result data, which is obtained from a single statement fed into the lambda expression.

The return value of the Where() method is hidden from view in this code example, but under the covers you are operating on an Enumerable type. From this object, you call the generic OrderBy() method, which also requires a Func<> delegate parameter. This time, you are simply passing each item in turn via a fitting lambda expression. The result of calling OrderBy() is a new ordered sequence of the initial data.
Finally, you call the Select() method off the sequence returned from OrderBy(), which results in the final set of data that is stored in an implicitly typed variable named subset.
To be sure, this “longhand” LINQ query is a bit more complex to tease apart than the previous C# LINQ query operator example. Part of the complexity is, no doubt, due to the chaining together of calls using the dot operator. Here is the same query, with each step broken into discrete chunks (as you might guess, you could break down the overall query in various manners):

static void QueryStringsWithEnumerableAndLambdas2()
{
Console.WriteLine(" Using Enumerable / Lambda Expressions ");

string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Break it down!
var gamesWithSpaces = currentVideoGames.Where(game => game.Contains(" ")); var orderedGames = gamesWithSpaces.OrderBy(game => game);
var subset = orderedGames.Select(game => game);

foreach (var game in subset)
{
Console.WriteLine("Item: {0}", game);
}
Console.WriteLine();
}

As you might agree, building a LINQ query expression using the methods of the Enumerable class directly is much more verbose than making use of the C# query operators. As well, given that the methods of Enumerable require delegates as parameters, you will typically need to author lambda expressions to allow the input data to be processed by the underlying delegate target.

Building Query Expressions Using the Enumerable Type and Anonymous Methods
Given that C# lambda expressions are simply shorthand notations for working with anonymous methods, consider the third query expression created within the QueryStringsWithAnonymousMethods() helper function, shown here:

static void QueryStringsWithAnonymousMethods()
{
Console.WriteLine(" Using Anonymous Methods ");

string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Build the necessary Func<> delegates using anonymous methods.

Func<string, bool> searchFilter = delegate(string game) { return game.Contains(" "); }; Func<string, string> itemToProcess = delegate(string s) { return s; };

// Pass the delegates into the methods of Enumerable.
var subset = currentVideoGames.Where(searchFilter).OrderBy(itemToProcess). Select(itemToProcess);

// Print out the results. foreach (var game in subset)
{
Console.WriteLine("Item: {0}", game);
}
Console.WriteLine();
}

This iteration of the query expression is even more verbose, because you are manually creating the Func<> delegates used by the Where(), OrderBy(), and Select() methods of the Enumerable class. On the plus side, the anonymous method syntax does keep all the delegate processing contained within a single method definition. Nevertheless, this method is functionally equivalent to the
QueryStringsWithEnumerableAndLambdas() and QueryStringsWithOperators() methods created in the previous sections.

Building Query Expressions Using the Enumerable Type and Raw Delegates
Finally, if you want to build a query expression using the verbose approach, you could avoid the use of lambdas/anonymous method syntax and directly create delegate targets for each Func<> type. Here is the final iteration of your query expression, modeled within a new class type named VeryComplexQueryExpression:

class VeryComplexQueryExpression
{
public static void QueryStringsWithRawDelegates()
{
Console.WriteLine(" Using Raw Delegates ");
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Build the necessary Func<> delegates. Func<string, bool> searchFilter =
new Func<string, bool>(Filter); Func<string, string> itemToProcess =
new Func<string,string>(ProcessItem);

// Pass the delegates into the methods of Enumerable. var subset =
currentVideoGames
.Where(searchFilter)
.OrderBy(itemToProcess)
.Select(itemToProcess);

// Print out the results. foreach (var game in subset)
{
Console.WriteLine("Item: {0}", game);
}
Console.WriteLine();
}

// Delegate targets.
public static bool Filter(string game)
{
return game.Contains(" ");
}
public static string ProcessItem(string game)
{
return game;
}
}

You can test this iteration of your string-processing logic by calling this method within the top-level statements of the Program.cs file, as follows:

VeryComplexQueryExpression.QueryStringsWithRawDelegates();

If you were to now run the application to test each possible approach, it should not be too surprising that the output is identical, regardless of the path taken. Keep the following points in mind regarding how LINQ query expressions are represented under the covers:
• Query expressions are created using various C# query operators.
• Query operators are simply shorthand notations for invoking extension methods defined by the System.Linq.Enumerable type.
• Many methods of Enumerable require delegates (Func<> in particular) as parameters.
• Any method requiring a delegate parameter can instead be passed a lambda expression.
• Lambda expressions are simply anonymous methods in disguise (which greatly improve readability).
• Anonymous methods are shorthand notations for allocating a raw delegate and manually building a delegate target method.
Whew! That might have been a bit deeper under the hood than you wanted to go, but I hope this discussion has helped you understand what the user-friendly C# query operators are doing behind the scenes.

Summary
LINQ is a set of related technologies attempting to provide a single, symmetrical manner to interact with diverse forms of data. As explained over the course of this chapter, LINQ can interact with any type implementing the IEnumerable interface, including simple arrays as well as generic and nongeneric collections of data.

As you have seen, working with LINQ technologies is accomplished using several C# language features. For example, given that LINQ query expressions can return any number of result sets, it is common to make use of the var keyword to represent the underlying data type. Lambda expressions, object initialization syntax, and anonymous types can all be used to build functional and compact LINQ queries.
More importantly, you have seen how the C# LINQ query operators are simply shorthand notations for making calls on static members of the System.Linq.Enumerable type. As shown, most members of Enumerable operate on Func delegate types, which can take literal method addresses, anonymous methods, or lambda expressions as input to evaluate the query.

Pro C#10 CHAPTER 11 Advanced C# Language Features

CHAPTER 11

Advanced C# Language Features

In this chapter, you’ll deepen your understanding of the C# programming language by examining several more advanced topics. To begin, you’ll learn how to implement and use an indexer method. This C# mechanism enables you to build custom types that provide access to internal subitems using an array-like syntax. After you learn how to build an indexer method, you’ll see how to overload various operators (+, -,
<, >, etc.) and how to create custom explicit and implicit conversion routines for your types (and you’ll learn
why you might want to do this).
Next, you’ll examine topics that are particularly useful when working with LINQ-centric APIs (though you can use them outside of the context of LINQ)—specifically extension methods and anonymous types.
To wrap things up, you’ll learn how to create an “unsafe” code context to directly manipulate unmanaged pointers. While it is certainly true that using pointers in C# applications is an infrequent activity, understanding how to do so can be helpful in some circumstances that involve complex interoperability scenarios.

Understanding Indexer Methods
As a programmer, you are certainly familiar with the process of accessing individual items contained within a simple array using the index operator ([]). Here’s an example:

// Loop over incoming command-line arguments
// using index operator.
for(int i = 0; i < args.Length; i++)
{
Console.WriteLine("Args: {0}", args[i]);
}

// Declare an array of local integers. int[] myInts = { 10, 9, 100, 432, 9874};

// Use the index operator to access each element. for(int j = 0; j < myInts.Length; j++)
{
Console.WriteLine("Index {0} = {1} ", j, myInts[j]);
}
Console.ReadLine();

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_11

427

This code is by no means a major news flash. However, the C# language provides the capability to design custom classes and structures that may be indexed just like a standard array, by defining an indexer method. This feature is most useful when you are creating custom collection classes (generic or nongeneric).
Before examining how to implement a custom indexer, let’s begin by seeing one in action. Assume you have added support for an indexer method to the custom PersonCollection type developed in
Chapter 10 (specifically, the IssuesWithNonGenericCollections project). While you have not yet added the indexer, observe the following usage within a new Console Application project named SimpleIndexer:

using System.Data; using SimpleIndexer;

// Indexers allow you to access items in an array-like fashion. Console.WriteLine(" Fun with Indexers \n");

PersonCollection myPeople = new PersonCollection();

// Add objects with indexer syntax.
myPeople[0] = new Person("Homer", "Simpson", 40); myPeople[1] = new Person("Marge", "Simpson", 38); myPeople[2] = new Person("Lisa", "Simpson", 9); myPeople[3] = new Person("Bart", "Simpson", 7); myPeople[4] = new Person("Maggie", "Simpson", 2);

// Now obtain and display each item using indexer.
for (int i = 0; i < myPeople.Count; i++)
{
Console.WriteLine("Person number: {0}", i); Console.WriteLine("Name: {0} {1}",
myPeople[i].FirstName, myPeople[i].LastName); Console.WriteLine("Age: {0}", myPeople[i].Age); Console.WriteLine();
}

As you can see, indexers allow you to manipulate the internal collection of subobjects just like a standard array. Now for the big question: how do you configure the PersonCollection class (or any custom class or structure) to support this functionality? An indexer is represented as a slightly modified C# property definition. In its simplest form, an indexer is created using the this[] syntax. Here is the required update for the PersonCollection class:
using System.Collections; namespace SimpleIndexer;
// Add the indexer to the existing class definition. public class PersonCollection : IEnumerable
{
private ArrayList arPeople = new ArrayList();
...
// Custom indexer for this class. public Person this[int index]

{
get => (Person)arPeople[index];
set => arPeople.Insert(index, value);
}
}

Apart from using the this keyword with the brackets, the indexer looks just like any other C# property declaration. For example, the role of the get scope is to return the correct object to the caller. Here, you are doing so by delegating the request to the indexer of the ArrayList object, as this class also supports an
indexer. The set scope oversees adding new Person objects; this is achieved by calling the Insert() method of the ArrayList.
Indexers are yet another form of syntactic sugar, given that this functionality can also be achieved using “normal” public methods such as AddPerson() or GetPerson(). Nevertheless, when you support indexer methods on your custom collection types, they integrate well into the fabric of the .NET base class libraries.
While creating indexer methods is quite commonplace when you are building custom collections, do remember that generic types give you this functionality out of the box. Consider the following method, which uses a generic List of Person objects. Note that you can simply use the indexer of List directly. Here’s an example:

static void UseGenericListOfPeople()
{
List myPeople = new List(); myPeople.Add(new Person("Lisa", "Simpson", 9)); myPeople.Add(new Person("Bart", "Simpson", 7));

// Change first person with indexer.
myPeople[0] = new Person("Maggie", "Simpson", 2);

// Now obtain and display each item using indexer. for (int i = 0; i < myPeople.Count; i++)
{
Console.WriteLine("Person number: {0}", i);
Console.WriteLine("Name: {0} {1}", myPeople[i].FirstName, myPeople[i].LastName);
Console.WriteLine("Age: {0}", myPeople[i].Age); Console.WriteLine();
}
}

Indexing Data Using String Values
The current PersonCollection class defined an indexer that allowed the caller to identify subitems using a numerical value. Understand, however, that this is not a requirement of an indexer method. Suppose you’d prefer to contain the Person objects using a System.Collections.Generic.Dictionary<TKey, TValue> rather than an ArrayList. Given that Dictionary types allow access to the contained types using a key (such as a person’s first name), you could define an indexer as follows:

using System.Collections; namespace SimpleIndexer;
public class PersonCollectionStringIndexer : IEnumerable
{
private Dictionary<string, Person> listPeople = new Dictionary<string, Person>();

// This indexer returns a person based on a string index. public Person this[string name]
{
get => (Person)listPeople[name]; set => listPeople[name] = value;
}
public void ClearPeople()
{
listPeople.Clear();
}

public int Count => listPeople.Count;

IEnumerator IEnumerable.GetEnumerator() => listPeople.GetEnumerator();
}

The caller would now be able to interact with the contained Person objects, as shown here:
Console.WriteLine(" Fun with Indexers \n"); PersonCollectionStringIndexer myPeopleStrings =
new PersonCollectionStringIndexer();

myPeopleStrings["Homer"] =
new Person("Homer", "Simpson", 40); myPeopleStrings["Marge"] =
new Person("Marge", "Simpson", 38);

// Get "Homer" and print data.
Person homer = myPeopleStrings["Homer"]; Console.ReadLine();

Again, if you were to use the generic Dictionary<TKey, TValue> type directly, you’d gain the indexer method functionality out of the box, without building a custom, nongeneric class supporting a string indexer. Nevertheless, do understand that the data type of any indexer will be based on how the supporting collection type allows the caller to retrieve subitems.

Overloading Indexer Methods
Indexer methods may be overloaded on a single class or structure. Thus, if it makes sense to allow the caller to access subitems using a numerical index or a string value, you might define multiple indexers for a single type. By way of example, in ADO.NET (.NET’s native database-access API), the DataSet class supports a property named Tables, which returns to you a strongly typed DataTableCollection type. As it turns out, DataTableCollection defines three indexers to get and set DataTable objects—one by ordinal position and the others by a friendly string moniker and optional containing namespace, as shown here:

public sealed class DataTableCollection : InternalDataCollectionBase
{
...
// Overloaded indexers!
public DataTable this[int index] { get; }

public DataTable this[string name] { get; }
public DataTable this[string name, string tableNamespace] { get; }
}

It is common for types in the base class libraries to support indexer methods. So be aware, even if your current project does not require you to build custom indexers for your classes and structures, that many types already support this syntax.

Indexers with Multiple Dimensions
You can also create an indexer method that takes multiple parameters. Assume you have a custom collection that stores subitems in a 2D array. If this is the case, you may define an indexer method as follows:

public class SomeContainer
{
private int[,] my2DintArray = new int[10, 10];

public int this[int row, int column]
{ / get or set value from 2D array / }
}

Again, unless you are building a highly stylized custom collection class, you won’t have much need to build a multidimensional indexer. Still, once again ADO.NET showcases how useful this construct can be. The ADO.NET DataTable is essentially a collection of rows and columns, much like a piece of graph paper or the general structure of a Microsoft Excel spreadsheet.
While DataTable objects are typically populated on your behalf using a related “data adapter,” the following code illustrates how to manually create an in-memory DataTable containing three columns (for the first name, last name, and age of each record). Notice how once you have added a single row to the DataTable, you use a multidimensional indexer to drill into each column of the first (and only) row. (If you are following along, you’ll need to import the System.Data namespace into your code file.)

static void MultiIndexerWithDataTable()
{
// Make a simple DataTable with 3 columns. DataTable myTable = new DataTable(); myTable.Columns.Add(new DataColumn("FirstName")); myTable.Columns.Add(new DataColumn("LastName")); myTable.Columns.Add(new DataColumn("Age"));

// Now add a row to the table. myTable.Rows.Add("Mel", "Appleby", 60);

// Use multidimension indexer to get details of first row. Console.WriteLine("First Name: {0}", myTable.Rows[0][0]); Console.WriteLine("Last Name: {0}", myTable.Rows[0][1]); Console.WriteLine("Age : {0}", myTable.Rows[0][2]);
}

Do be aware that you’ll take a rather deep dive into ADO.NET beginning with Chapter 20, so if some of the previous code seems unfamiliar, fear not. The main point of this example is that indexer methods can support multiple dimensions and, if used correctly, can simplify the way you interact with contained subobjects in custom collections.

Indexer Definitions on Interface Types
Indexers can be defined on a given .NET interface type to allow supporting types to provide a custom implementation. Here is a simple example of an interface that defines a protocol for obtaining string objects using a numerical indexer:

public interface IStringContainer
{
string this[int index] { get; set; }
}

With this interface definition, any class or structure that implements this interface must now support a read-write indexer that manipulates subitems using a numerical value. Here is a partial implementation of such a class:

class SomeClass : IStringContainer
{
private List myStrings = new List();

public string this[int index]
{
get => myStrings[index];
set => myStrings.Insert(index, value);
}
}

That wraps up the first major topic of this chapter. Now let’s examine a language feature that lets you build custom classes or structures that respond uniquely to the intrinsic operators of C#. Next, allow me to introduce the concept of operator overloading.

Understanding Operator Overloading
C#, like any programming language, has a canned set of tokens that are used to perform basic operations on intrinsic types. For example, you know that the + operator can be applied to two integers to yield a larger integer.

// The + operator with ints. int a = 100;
int b = 240;
int c = a + b; // c is now 340

Once again, this is no major news flash, but have you ever stopped and noticed how the same + operator can be applied to most intrinsic C# data types? For example, consider this code:

// + operator with strings.

string s1 = "Hello"; string s2 = " world!";
string s3 = s1 + s2; // s3 is now "Hello World!"

The + operator functions in specific ways based on the supplied data types (strings or integers, in this case). When the + operator is applied to numerical types, the result is the summation of the operands.
However, when the + operator is applied to string types, the result is string concatenation.
The C# language gives you the capability to build custom classes and structures that also respond uniquely to the same set of basic tokens (such as the + operator). While not every possible C# operator can be overloaded, many can, as shown in Table 11-1.

Table 11-1. Overloadability of C# Operators

C# Operator Overloadability
+, -, !, ~, ++, --, true,
false These unary operators can be overloaded. C# demands that if true or false is overloaded, both must be overloaded.
+, -, *, /, %, &, |, ^, <<,

These binary operators can be overloaded.
==,!=, <, >, <=, >= These comparison operators can be overloaded. C# demands that “like” operators (i.e., < and >, <= and >=, == and !=) are overloaded together.
[] The [] operator cannot be overloaded. As you saw earlier in this chapter, however, the indexer construct provides the same functionality.
() The () operator cannot be overloaded. As you will see later in this chapter, however, custom conversion methods provide the same functionality.
+=, -=, *=, /=, %=, &=,
|=, ^=, <<=, >>= Shorthand assignment operators cannot be overloaded; however, you receive them as a freebie when you overload the related binary operator.

Overloading Binary Operators
To illustrate the process of overloading binary operators, assume the following simple Point class is defined in a new Console Application project named OverloadedOps:

namespace OverloadedOps;
// Just a simple, everyday C# class. public class Point
{
public int X {get; set;} public int Y {get; set;}

public Point(int xPos, int yPos)
{
X = xPos;
Y = yPos;
}
public override string ToString()
=> $"[{this.X}, {this.Y}]";

}

Now, logically speaking, it makes sense to “add” Points together. For example, if you added together two Point variables, you should receive a new Point that is the summation of the X and Y values. Of course, it might also be helpful to subtract one Point from another. Ideally, you would like to be able to author the following code:

using OverloadedOps;

// Adding and subtracting two points?
Console.WriteLine(" Fun with Overloaded Operators \n");

// Make two points.
Point ptOne = new Point(100, 100); Point ptTwo = new Point(40, 40);
Console.WriteLine("ptOne = {0}", ptOne); Console.WriteLine("ptTwo = {0}", ptTwo);
// Add the points to make a bigger point? Console.WriteLine("ptOne + ptTwo: {0} ", ptOne + ptTwo);

// Subtract the points to make a smaller point? Console.WriteLine("ptOne - ptTwo: {0} ", ptOne - ptTwo); Console.ReadLine();

However, as your Point now stands, you will receive compile-time errors, as the Point type does not know how to respond to the + or - operator. To equip a custom type to respond uniquely to intrinsic operators, C# provides the operator keyword, which you can use only in conjunction with the static keyword. When you overload a binary operator (such as + and -), you will most often pass in two arguments that are the same type as the defining class (a Point in this example), as illustrated in the following code update:

// A more intelligent Point type. public class Point
{
...
// Overloaded operator +.
public static Point operator + (Point p1, Point p2)
=> new Point(p1.X + p2.X, p1.Y + p2.Y);

// Overloaded operator -.
public static Point operator - (Point p1, Point p2)
=> new Point(p1.X - p2.X, p1.Y - p2.Y);
}

The logic behind operator + is simply to return a new Point object based on the summation of the fields of the incoming Point parameters. Thus, when you write pt1 + pt2, under the hood you can envision the following hidden call to the static operator + method:

// Pseudo-code: Point p3 = Point.operator+ (p1, p2) Point p3 = p1 + p2;

Likewise, p1–p2 maps to the following:

// Pseudo-code: Point p4 = Point.operator- (p1, p2) Point p4 = p1 - p2;

With this update, your program now compiles, and you find you can add and subtract Point objects, as shown in the following output:

Fun with Overloaded Operators ptOne = [100, 100]
ptTwo = [40, 40]
ptOne + ptTwo: [140, 140]
ptOne - ptTwo: [60, 60]

When you are overloading a binary operator, you are not required to pass in two parameters of the same type. If it makes sense to do so, one of the arguments can differ. For example, here is an overloaded operator

  • that allows the caller to obtain a new Point that is based on a numerical adjustment:

public class Point
{
...
public static Point operator + (Point p1, int change)
=> new Point(p1.X + change, p1.Y + change);

public static Point operator + (int change, Point p1)
=> new Point(p1.X + change, p1.Y + change);
}

Notice that you need both versions of the method if you want the arguments to be passed in either order (i.e., you can’t just define one of the methods and expect the compiler to automatically support the other one). You are now able to use these new versions of operator + as follows:

// Prints [110, 110].
Point biggerPoint = ptOne + 10; Console.WriteLine("ptOne + 10 = {0}", biggerPoint);

// Prints [120, 120].
Console.WriteLine("10 + biggerPoint = {0}", 10 + biggerPoint); Console.WriteLine();

What of the += and –= Operators?
If you are coming to C# from a C++ background, you might lament the loss of overloading the shorthand assignment operators (+=, -=, etc.). Don’t despair. In terms of C#, the shorthand assignment operators are automatically simulated if a type overloads the related binary operator. Thus, given that the Point structure has already overloaded the + and - operators, you can write the following:

// Overloading binary operators results in a freebie shorthand operator.
...
// Freebie +=
Point ptThree = new Point(90, 5);

Console.WriteLine("ptThree = {0}", ptThree); Console.WriteLine("ptThree += ptTwo: {0}", ptThree += ptTwo);

// Freebie -=
Point ptFour = new Point(0, 500); Console.WriteLine("ptFour = {0}", ptFour);
Console.WriteLine("ptFour -= ptThree: {0}", ptFour -= ptThree); Console.ReadLine();

Overloading Unary Operators
C# also allows you to overload various unary operators, such as ++ and --. When you overload a unary operator, you also must use the static keyword with the operator keyword; however, in this case, you simply pass in a single parameter that is the same type as the defining class/structure. For example, if you were to update the Point with the following overloaded operators:

public class Point
{
...
// Add 1 to the X/Y values for the incoming Point. public static Point operator ++(Point p1)
=> new Point(p1.X+1, p1.Y+1);

// Subtract 1 from the X/Y values for the incoming Point. public static Point operator --(Point p1)
=> new Point(p1.X-1, p1.Y-1);
}

you could increment and decrement Point’s x and y values like this:

...
// Applying the ++ and -- unary operators to a Point. Point ptFive = new Point(1, 1); Console.WriteLine("++ptFive = {0}", ++ptFive); // [2, 2]
Console.WriteLine("--ptFive = {0}", --ptFive); // [1, 1]

// Apply same operators as postincrement/decrement. Point ptSix = new Point(20, 20);
Console.WriteLine("ptSix++ = {0}", ptSix++); // [20, 20]
Console.WriteLine("ptSix-- = {0}", ptSix--); // [21, 21] Console.ReadLine();

Notice in the preceding code example you are applying the custom ++ and -- operators in two different manners. In C++, it is possible to overload pre- and post-increment/decrement operators separately. This
is not possible in C#. However, the return value of the increment/decrement is automatically handled

“correctly” free of charge (i.e., for an overloaded ++ operator, pt++ has the value of the unmodified object as its value within an expression, while ++pt has the new value applied before use in the expression).

Overloading Equality Operators
As you might recall from Chapter 6, System.Object.Equals() can be overridden to perform value-based (rather than referenced-based) comparisons between reference types. If you choose to override Equals() (and the often-related System.Object.GetHashCode() method), it is trivial to overload the equality operators (== and !=). To illustrate, here is the updated Point type:

// This incarnation of Point also overloads the == and != operators. public class Point
{
...
public override bool Equals(object o)
=> o.ToString() == this.ToString();

public override int GetHashCode()
=> this.ToString().GetHashCode();

// Now let's overload the == and != operators. public static bool operator ==(Point p1, Point p2)
=> p1.Equals(p2);

public static bool operator !=(Point p1, Point p2)
=> !p1.Equals(p2);
}

Notice how the implementation of operator == and operator != simply makes a call to the overridden Equals() method to get the bulk of the work done. Given this, you can now exercise your Point class as follows:

// Make use of the overloaded equality operators.
...
Console.WriteLine("ptOne == ptTwo : {0}", ptOne == ptTwo); Console.WriteLine("ptOne != ptTwo : {0}", ptOne != ptTwo); Console.ReadLine();

As you can see, it is quite intuitive to compare two objects using the well-known == and != operators, rather than making a call to Object.Equals(). If you do overload the equality operators for a given class, keep in mind that C# demands that if you override the == operator, you must also override the != operator (if you forget, the compiler will let you know).

Overloading Comparison Operators
In Chapter 8, you learned how to implement the IComparable interface to compare the relationship between two like objects. You can, in fact, also overload the comparison operators (<, >, <=, and >=) for the same class.

As with the equality operators, C# demands that if you overload <, you must also overload >. The same holds true for the <= and >= operators. If the Point type overloaded these comparison operators, the object user could now compare Points, as follows:

// Using the overloaded < and > operators.
...
Console.WriteLine("ptOne < ptTwo : {0}", ptOne < ptTwo); Console.WriteLine("ptOne > ptTwo : {0}", ptOne > ptTwo); Console.ReadLine();

Assuming you have implemented the IComparable interface (or better yet, the generic equivalent), overloading the comparison operators is trivial. Here is the updated class definition:

// Point is also comparable using the comparison operators. public class Point : IComparable
{
...
public int CompareTo(Point other)
{
if (this.X > other.X && this.Y > other.Y)
{
return 1;
}
if (this.X < other.X && this.Y < other.Y)
{
return -1;
}
return 0;
}
public static bool operator <(Point p1, Point p2)
=> p1.CompareTo(p2) < 0;

public static bool operator >(Point p1, Point p2)
=> p1.CompareTo(p2) > 0;

public static bool operator <=(Point p1, Point p2)
=> p1.CompareTo(p2) <= 0;

public static bool operator >=(Point p1, Point p2)
=> p1.CompareTo(p2) >= 0;
}

Final Thoughts Regarding Operator Overloading
As you have seen, C# provides the capability to build types that can respond uniquely to various intrinsic, well-known operators. Now, before you go and retrofit all your classes to support such behavior, you must be sure that the operators you are about to overload make some sort of logical sense in the world at large.
For example, let’s say you overloaded the multiplication operator for the MiniVan class. What exactly would it mean to multiply two MiniVan objects? Not much. In fact, it would be confusing for teammates to see the following use of MiniVan objects:

// Huh?! This is far from intuitive... MiniVan newVan = myVan * yourVan;

Overloading operators is generally useful only when you’re building atomic data types. Vectors, matrices, text, points, shapes, sets, etc., make good candidates for operator overloading. People, managers, cars, database connections, and web pages do not. As a rule of thumb, if an overloaded operator makes it harder for the user to understand a type’s functionality, don’t do it. Use this feature wisely.

Understanding Custom Type Conversions
Let’s now examine a topic closely related to operator overloading: custom type conversions. To set the stage for the discussion, let’s quickly review the notion of explicit and implicit conversions between numerical data and related class types.

Recall: Numerical Conversions
In terms of the intrinsic numerical types (sbyte, int, float, etc.), an explicit conversion is required when you attempt to store a larger value in a smaller container, as this could result in a loss of data. Basically, this is your way to tell the compiler, “Leave me alone, I know what I am trying to do.” Conversely, an implicit conversion happens automatically when you attempt to place a smaller type in a destination type that will not result in a loss of data.

int a = 123;
long b = a; // Implicit conversion from int to long. int c = (int) b; // Explicit conversion from long to int.

Recall: Conversions Among Related Class Types
As shown in Chapter 6, class types may be related by classical inheritance (the “is-a” relationship). In this case, the C# conversion process allows you to cast up and down the class hierarchy. For example, a derived class can always be implicitly cast to a base type. However, if you want to store a base class type in a derived variable, you must perform an explicit cast, like so:

// Two related class types. class Base{}
class Derived : Base{}

// Implicit cast between derived to base. Base myBaseType;
myBaseType = new Derived();
// Must explicitly cast to store base reference
// in derived type.
Derived myDerivedType = (Derived)myBaseType;

This explicit cast works because the Base and Derived classes are related by classical inheritance and
myBaseType is constructed as an instance of Derived. However, if myBaseType is an instance of Base, the cast

throws an InvalidCastException. If there is any doubt that the cast will fail, you should use the as keyword, as discussed in Chapter 6. Here is the sample reworked to demonstrate this:

// Implicit cast between derived to base. Base myBaseType2 = new();
// Throws InvalidCastException
//Derived myDerivedType2 = (Derived)myBaseType2 as Derived;
//No exception, myDerivedType2 is null
Derived myDerivedType2 = myBaseType2 as Derived;

However, what if you have two class types in different hierarchies with no common parent (other than System.Object) that require conversions? Given that they are not related by classical inheritance, typical casting operations offer no help (and you would get a compiler error to boot!).
On a related note, consider value types (structures). Assume you have two structures named Square and Rectangle. Given that structures cannot leverage classic inheritance (as they are always sealed), you have no natural way to cast between these seemingly related types.
While you could create helper methods in the structures (such as Rectangle.ToSquare()), C# lets you build custom conversion routines that allow your types to respond to the () casting operator. Therefore, if you configured the structures correctly, you would be able to use the following syntax to explicitly convert between them as follows:

// Convert a Rectangle to a Square! Rectangle rect = new Rectangle
{
Width = 3;
Height = 10;
}
Square sq = (Square)rect;

Creating Custom Conversion Routines
Begin by creating a new Console Application project named CustomConversions. C# provides two keywords, explicit and implicit, that you can use to control how your types respond during an attempted conversion. Assume you have the following structure definitions:

namespace CustomConversions; public struct Rectangle
{
public int Width {get; set;} public int Height {get; set;}

public Rectangle(int w, int h)
{
Width = w;
Height = h;
}

public void Draw()
{

for (int i = 0; i < Height; i++)
{
for (int j = 0; j < Width; j++)
{
Console.Write("*");
}
Console.WriteLine();
}
}

public override string ToString()
=> $"[Width = {Width}; Height = {Height}]";
}

namespace CustomConversions; public struct Square
{
public int Length {get; set;} public Square(int l) : this()
{
Length = l;
}

public void Draw()
{
for (int i = 0; i < Length; i++)
{
for (int j = 0; j < Length; j++)
{
Console.Write("*");
}
Console.WriteLine();
}
}

public override string ToString() => $"[Length = {Length}]";

// Rectangles can be explicitly converted into Squares. public static explicit operator Square(Rectangle r)
{
Square s = new Square {Length = r.Height}; return s;
}
}
}

Notice that this iteration of the Square type defines an explicit conversion operator. Like the process of overloading an operator, conversion routines make use of the C# operator keyword, in conjunction with the explicit or implicit keyword, and must be defined as static. The incoming parameter is the entity you are converting from, while the operator type is the entity you are converting to.

In this case, the assumption is that a square (being a geometric pattern in which all sides are of equal length) can be obtained from the height of a rectangle. Thus, you are free to convert a Rectangle into a Square, as follows:

using CustomConversions;

Console.WriteLine(" Fun with Conversions \n");
// Make a Rectangle.
Rectangle r = new Rectangle(15, 4); Console.WriteLine(r.ToString()); r.Draw();

Console.WriteLine();

// Convert r into a Square,
// based on the height of the Rectangle. Square s = (Square)r; Console.WriteLine(s.ToString()); s.Draw();
Console.ReadLine();

You can see the output here:

Fun with Conversions [Width = 15; Height = 4]





[Length = 4]





While it may not be all that helpful to convert a Rectangle into a Square within the same scope, assume you have a function that has been designed to take Square parameters.

// This method requires a Square type. static void DrawSquare(Square sq)
{
Console.WriteLine(sq.ToString()); sq.Draw();
}

Using your explicit conversion operation on the Square type, you can now pass in Rectangle types for processing using an explicit cast, like so:

...

// Convert Rectangle to Square to invoke method. Rectangle rect = new Rectangle(10, 5); DrawSquare((Square)rect);
Console.ReadLine();

Additional Explicit Conversions for the Square Type
Now that you can explicitly convert Rectangles into Squares, let’s examine a few additional explicit conversions. Given that a square is symmetrical on all sides, it might be helpful to provide an explicit conversion routine that allows the caller to cast from an integer type into a Square (which, of course, will have a side length equal to the incoming integer). Likewise, what if you were to update Square such that the caller can cast from a Square into an int? Here is the calling logic:

...
// Converting an int to a Square. Square sq2 = (Square)90; Console.WriteLine("sq2 = {0}", sq2);

// Converting a Square to an int. int side = (int)sq2;
Console.WriteLine("Side length of sq2 = {0}", side); Console.ReadLine();

and here is the update to the Square class:

public struct Square
{
...
public static explicit operator Square(int sideLength)
{
Square newSq = new Square {Length = sideLength}; return newSq;
}

public static explicit operator int (Square s) => s.Length;
}

To be honest, converting from a Square into an integer may not be the most intuitive (or useful) operation (after all, chances are you could just pass such values to a constructor). However, it does point out an important fact regarding custom conversion routines: the compiler does not care what you convert to or from, if you have written syntactically correct code.
Thus, as with overloading operators, just because you can create an explicit cast operation for a given type does not mean you should. Typically, this technique will be most helpful when you’re creating structure types, given that they are unable to participate in classical inheritance (where casting comes for free).

Defining Implicit Conversion Routines
So far, you have created various custom explicit conversion operations. However, what about the following
implicit conversion?

...
Square s3 = new Square {Length = 83};

// Attempt to make an implicit cast? Rectangle rect2 = s3;

Console.ReadLine();

This code will not compile, given that you have not provided an implicit conversion routine for the Rectangle type. Now here is the catch: it is illegal to define explicit and implicit conversion functions on the same type if they do not differ by their return type or parameter set. This might seem like a limitation; however, the second catch is that when a type defines an implicit conversion routine, it is legal for the caller to make use of the explicit cast syntax!
Confused? To clear things up, let’s add an implicit conversion routine to the Rectangle structure using the C# implicit keyword (note that the following code assumes the width of the resulting Rectangle is computed by multiplying the side of the Square by 2):

public struct Rectangle
{
...
public static implicit operator Rectangle(Square s)
{
Rectangle r = new Rectangle
{
Height = s.Length,
Width = s.Length * 2 // Assume the length of the new Rectangle with (Length x 2).
};
return r;
}
}

With this update, you are now able to convert between types, as follows:

...
// Implicit cast OK!
Square s3 = new Square { Length= 7};

Rectangle rect2 = s3; Console.WriteLine("rect2 = {0}", rect2);

// Explicit cast syntax still OK! Square s4 = new Square {Length = 3}; Rectangle rect3 = (Rectangle)s4;

Console.WriteLine("rect3 = {0}", rect3); Console.ReadLine();

That wraps up your look at defining custom conversion routines. As with overloaded operators, remember that this bit of syntax is simply a shorthand notation for “normal” member functions, and in this light, it is always optional. When used correctly, however, custom structures can be used more naturally, as they can be treated as true class types related by inheritance.

Understanding Extension Methods
.NET 3.5 introduced the concept of extension methods, which allow you to add new methods or properties to a class or structure, without modifying the original type in any direct manner. So, where might this be helpful? Consider the following possibilities.
First, say you have a given class that is in production. It becomes clear over time that this class should support a handful of new members. If you modify the current class definition directly, you risk the possibility of breaking backward compatibility with older code bases making use of it, as they might not have been compiled with the latest and greatest class definition. One way to ensure backward compatibility is to create a new derived class from the existing parent; however, now you have two classes to maintain. As we all know, code maintenance is the least glamorous part of a software engineer’s job description.
Now consider this situation. Let’s say you have a structure (or maybe a sealed class) and want to add new members so that it behaves polymorphically in your system. Since structures and sealed classes cannot be extended, your only choice is to add the members to the type, once again risking breaking backward compatibility!
Using extension methods, you can modify types without subclassing and without modifying the type directly. The catch is that the new functionality is offered to a type only if the extension methods have been referenced for use in your current project.

Defining Extension Methods
When you define extension methods, the first restriction is that they must be defined within a static class (see Chapter 5); therefore, each extension method must be declared with the static keyword. The second point is that all extension methods are marked as such by using the this keyword as a modifier on the first (and only the first) parameter of the method in question. The “this qualified” parameter represents the item being extended.
To illustrate, create a new Console Application project named ExtensionMethods. Now, assume you are authoring a class named MyExtensions that defines two extension methods. The first method allows any object to use a new method named DisplayDefiningAssembly() that makes use of types in the System.
Reflection namespace to display the name of the assembly containing the type in question.

■Note You will formally examine the reflection apI in Chapter 17. If you are new to the topic, simply understand that reflection allows you to discover the structure of assemblies, types, and type members at runtime.

The second extension method, named ReverseDigits(), allows any int to obtain a new version of itself where the value is reversed digit by digit. For example, if an integer with the value 1234 called
ReverseDigits(), the integer returned is set to the value 4321. Consider the following class implementation (be sure to import the System.Reflection namespace if you are following along):

using System.Reflection;

namespace MyExtensionMethods; static class MyExtensions
{
// This method allows any object to display the assembly
// it is defined in.
public static void DisplayDefiningAssembly(this object obj)

{
Console.WriteLine("{0} lives here: => {1}\n", obj.GetType().Name, Assembly.GetAssembly(obj.GetType()).GetName().Name);
}

// This method allows any integer to reverse its digits.
// For example, 56 would return 65.
public static int ReverseDigits(this int i)
{
// Translate int into a string, and then
// get all the characters.
char[] digits = i.ToString().ToCharArray();

// Now reverse items in the array. Array.Reverse(digits);

// Put back into string.
string newDigits = new string(digits);

// Finally, return the modified string back as an int. return int.Parse(newDigits);
}
}

Again, note how the first parameter of each extension method has been qualified with the this keyword, before defining the parameter type. It is always the case that the first parameter of an extension method represents the type being extended. Given that DisplayDefiningAssembly() has been prototyped to extend System.Object, every type now has this new member, as Object is the parent to all types in the
.NET platform. However, ReverseDigits() has been prototyped to extend only integer types; therefore, if anything other than an integer attempts to invoke this method, you will receive a compile-time error.

■ Note understand that a given extension method can have multiple parameters, but only the first parameter can be qualified with this. the additional parameters would be treated as normal incoming parameters for use by the method.

Invoking Extension Methods
Now that you have these extension methods in place, consider the following code example that applies the extension method to various types in the base class libraries:

using MyExtensionMethods;

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

// The int has assumed a new identity! int myInt = 12345678;

myInt.DisplayDefiningAssembly();

// So has the DataSet!
System.Data.DataSet d = new System.Data.DataSet(); d.DisplayDefiningAssembly();

// Use new integer functionality. Console.WriteLine("Value of myInt: {0}", myInt); Console.WriteLine("Reversed digits of myInt: {0}",
myInt.ReverseDigits()); Console.ReadLine();
Here is the output:

Fun with Extension Methods Int32 lives here: => System.Private.CoreLib
DataSet lives here: => System.Data.Common Value of myInt: 12345678
Reversed digits of myInt: 87654321

Importing Extension Methods
When you define a class containing extension methods, it will no doubt be defined within a namespace. If this namespace is different from the namespace using the extension methods, you will need to make use of the expected C# using keyword. When you do, your code file has access to all extension methods for the type being extended. This is important to remember because if you do not explicitly import the correct namespace, the extension methods are not available for that C# code file.
In effect, although it can appear on the surface that extension methods are global in nature, they are in fact limited to the namespaces that define them or the namespaces that import them. Recall that you wrapped the MyExtensions class into a namespace named MyExtensionMethods, as follows:

namespace MyExtensionMethods; static class MyExtensions
{
...
}

To use the extension methods in the class, you need to explicitly import the MyExtensionMethods
namespace, as we did in the top-level statements used to exercise the examples.

Extending Types Implementing Specific Interfaces
At this point, you have seen how to extend classes (and, indirectly, structures that follow the same syntax) with new functionality via extension methods. It is also possible to define an extension method that can only extend a class or structure that implements the correct interface. For example, you could say something
to the effect of “If a class or structure implements IEnumerable, then that type gets the following new members.” Of course, it is possible to demand that a type support any interface at all, including your own custom interfaces.
To illustrate, create a new Console Application project named InterfaceExtensions. The goal here is to add a new method to any type that implements IEnumerable, which would include any array and many
nongeneric collection classes (recall from Chapter 10 that the generic IEnumerable interface extends the nongeneric IEnumerable interface). Add the following extension class to your new project:

namespace InterfaceExtensions; static class AnnoyingExtensions
{
public static void PrintDataAndBeep(
this System.Collections.IEnumerable iterator)
{
foreach (var item in iterator)
{
Console.WriteLine(item); Console.Beep();
}
}
}

Given that the PrintDataAndBeep() method can be used by any class or structure that implements
IEnumerable, you could test via the following code:

using InterfaceExtensions;

Console.WriteLine(" Extending Interface Compatible Types \n");

// System.Array implements IEnumerable! string[] data =
{ "Wow", "this", "is", "sort", "of", "annoying",
"but", "in", "a", "weird", "way", "fun!"}; data.PrintDataAndBeep();

Console.WriteLine();

// List implements IEnumerable!
List myInts = new List() {10, 15, 20}; myInts.PrintDataAndBeep();

Console.ReadLine();

That wraps up your examination of C# extension methods. Remember that this language feature can be useful whenever you want to extend the functionality of a type but do not want to subclass (or cannot subclass if the type is sealed), for the purposes of polymorphism. As you will see later in the text, extension methods play a key role for LINQ APIs. In fact, you will see that under the LINQ APIs, one of the most common items being extended is a class or structure implementing (surprise!) the generic version of IEnumerable.

Extension Method GetEnumerator Support (New 9.0)
Prior to C# 9.0, to use foreach on a class, the GetEnumerator() method had to be defined on that class directly. With C# 9.0, the foreach method will examine extension methods on the class and, if a
GetEnumerator() method is found, will use that method to get the IEnumerator for that class. To see this in action, add a new Console application named ForEachWithExtensionMethods, and add simplified versions of the Car and Garage classes from Chapter 8.

//Car.cs
namespace ForEachWithExtensionMethods; class Car
{
// Car properties.
public int CurrentSpeed {get; set;} = 0; public string PetName {get; set;} = "";

// Constructors. public Car() {}
public Car(string name, int speed)
{
CurrentSpeed = speed;
PetName = name;
}

// See if Car has overheated.
}

//Garage.cs
namespace ForEachWithExtensionMethods; class Garage
{
public Car[] CarsInGarage { get; set; }

// Fill with some Car objects upon startup. public Garage()
{
CarsInGarage = new Car[4]; CarsInGarage[0] = new Car("Rusty", 30); CarsInGarage[1] = new Car("Clunker", 55); CarsInGarage[2] = new Car("Zippy", 30); CarsInGarage[3] = new Car("Fred", 30);
}

}

Note that the Garage class does not implement IEnumerable, nor does it have a GetEnumerator()
method. The GetEnumerator() method is added through the GarageExtensions class, shown here:

using System.Collections;

namespace ForEachWithExtensionMethods; static class GarageExtensions
{
public static IEnumerator GetEnumerator(this Garage g)
=> g.CarsInGarage.GetEnumerator();
}

The code to test this new feature is the same code used to test the GetEnumerator() method in Chapter 8.
Update Program.cs to the following:

using ForEachWithExtensionMethods;

Console.WriteLine(" Support for Extension Method GetEnumerator \n"); Garage carLot = new Garage();

// Hand over each car in the collection? foreach (Car c in carLot)
{
Console.WriteLine("{0} is going {1} MPH", c.PetName, c.CurrentSpeed);
}

You will see that the code works, printing to the console the list of cars and their speed.
Support for Extension Method GetEnumerator Rusty is going 30 MPH
Clunker is going 55 MPH Zippy is going 30 MPH Fred is going 30 MPH

■ Note there is a potential drawback to this new feature, in that classes that were never meant to be
foreached could now be foreached.

Understanding Anonymous Types
As an object-oriented programmer, you know the benefits of defining classes to represent the state and functionality of a given item you are attempting to model. To be sure, whenever you need to define a class that is intended to be reused across projects and that provides numerous bits of functionality through a set of methods, events, properties, and custom constructors, creating a new C# class is common practice.

However, there are other times when you want to define a class simply to model a set of encapsulated (and somehow related) data points without any associated methods, events, or other specialized functionality. Furthermore, what if this type is to be used by only a handful of methods in your program? It would be rather a bother to define a full class definition as shown next when you know full well this class will be used in only a handful of places. To accentuate this point, here is the rough outline of what you might need to do when you need to create a “simple” data type that follows typical value-based semantics:

class SomeClass
{
// Define a set of private member variables...

// Make a property for each member variable...

// Override ToString() to account for key member variables...

// Override GetHashCode() and Equals() to work with value-based equality...
}

As you can see, it is not necessarily so simple. Not only do you need to author a fair amount of code, but you have another class to maintain in your system. For temporary data such as this, it would be useful to whip up a custom data type on the fly. For example, let’s say you need to build a custom method that
receives a set of incoming parameters. You would like to take these parameters and use them to create a new data type for use in this method scope. Further, you would like to quickly print out this data using the typical ToString() method and perhaps use other members of System.Object. You can do this very thing using anonymous type syntax.

Defining an Anonymous Type
When you define an anonymous type, you do so by using the var keyword (see Chapter 3) in conjunction with object initialization syntax (see Chapter 5). You must use the var keyword because the compiler will automatically generate a new class definition at compile time (and you never see the name of this class in your C# code). The initialization syntax is used to tell the compiler to create private backing fields and (read- only) properties for the newly created type.
To illustrate, create a new Console Application project named AnonymousTypes. Now, add the following method to your Program.cs file, which composes a new type, on the fly, using the incoming parameter data:

static void BuildAnonymousType( string make, string color, int currSp )
{
// Build anonymous type using incoming args.
var car = new { Make = make, Color = color, Speed = currSp };

// Note you can now use this type to get the property data!
Console.WriteLine("You have a {0} {1} going {2} MPH", car.Color, car.Make, car.Speed);

// Anonymous types have custom implementations of each virtual
// method of System.Object. For example: Console.WriteLine("ToString() == {0}", car.ToString());
}

Note that an anonymous type can also be created inline in addition to wrapping the code in a function, as shown here:

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

// Make an anonymous type representing a car.
var myCar = new { Color = "Bright Pink", Make = "Saab", CurrentSpeed = 55 };

// Now show the color and make.
Console.WriteLine("My car is a {0} {1}.", myCar.Color, myCar.Make);

// Now call our helper method to build anonymous type via args. BuildAnonymousType("BMW", "Black", 90);

Console.ReadLine();

So, at this point, simply understand that anonymous types allow you to quickly model the “shape” of data with little overhead. This technique is little more than a way to whip up a new data type on the fly,
which supports bare-bones encapsulation via properties and acts according to value-based semantics. To understand that last point, let’s see how the C# compiler builds out anonymous types at compile time and, specifically, how it overrides the members of System.Object.

The Internal Representation of Anonymous Types
All anonymous types are automatically derived from System.Object and, therefore, support each of the members provided by this base class. Given this, you could invoke ToString(), GetHashCode(), Equals(), or GetType() on the implicitly typed myCar object. Assume your Program.cs file defines the following static helper function:

static void ReflectOverAnonymousType(object obj)
{
Console.WriteLine("obj is an instance of: {0}", obj.GetType().Name);
Console.WriteLine("Base class of {0} is {1}", obj.GetType().Name, obj.GetType().BaseType);
Console.WriteLine("obj.ToString() == {0}", obj.ToString()); Console.WriteLine("obj.GetHashCode() == {0}",
obj.GetHashCode());
Console.WriteLine();
}

Now assume you invoke this method, passing in the myCar object as the parameter, like so:

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

// Make an anonymous type representing a car.
var myCar = new {Color = "Bright Pink", Make = "Saab",

CurrentSpeed = 55};

// Reflect over what the compiler generated. ReflectOverAnonymousType(myCar);
...

Console.ReadLine();

The output will look like the following:

Fun with Anonymous Types
obj is an instance of: <>f AnonymousType03 Base class of <>f AnonymousType03 is System.Object
obj.ToString() = { Color = Bright Pink, Make = Saab, CurrentSpeed = 55 } obj.GetHashCode() = -564053045

First, notice that, in this example, the myCar object is of type <>f AnonymousType0`3 (your name may differ). Remember that the assigned type name is completely determined by the compiler and is not directly accessible in your C# code base.
Perhaps most important, notice that each name-value pair defined using the object initialization syntax is mapped to an identically named read-only property and a corresponding private init-only backing field. The following C# code approximates the compiler-generated class used to represent the myCar object (which again can be verified using ildasm.exe):

.class private sealed '<>f AnonymousType0'3'<'j TPar', 'j TPar', j TPar>'
extends [System.Runtime][System.Object]
{
// init-only fields.
private initonly j TPar i Field;
private initonly j TPar i Field; private initonly j TPar i Field;

// Default constructor.
public <>f AnonymousType0(j TPar Color,

j TPar Make, j TPar CurrentSpeed);
// Overridden methods.
public override bool Equals(object value); public override int GetHashCode();
public override string ToString();

// Read-only properties.
j TPar Color { get; }
j TPar CurrentSpeed { get; }
j TPar Make { get; }
}

The Implementation of ToString() and GetHashCode()
All anonymous types automatically derive from System.Object and are provided with an overridden version of Equals(), GetHashCode(), and ToString(). The ToString() implementation simply builds a string from each name-value pair. Here’s an example:

public override string ToString()
{
StringBuilder builder = new StringBuilder(); builder.Append("{ Color = "); builder.Append(this.i Field); builder.Append(", Make = "); builder.Append(this.i Field); builder.Append(", CurrentSpeed = "); builder.Append(this.i Field); builder.Append(" }");
return builder.ToString();
}

The GetHashCode() implementation computes a hash value using each anonymous type’s member variables as input to the System.Collections.Generic.EqualityComparer type. Using this implementation of GetHashCode(), two anonymous types will yield the same hash value if they have the
same set of properties that have been assigned the same values. Given this implementation, anonymous types are well suited to be contained within a Hashtable container.

The Semantics of Equality for Anonymous Types
While the implementation of the overridden ToString() and GetHashCode() methods is straightforward, you might be wondering how the Equals() method has been implemented. For example, if you were to define two “anonymous cars” variables that specify the same name-value pairs, would these two variables be considered equal? To see the results firsthand, update your Program.cs class with the following
new method:

static void EqualityTest()
{
// Make 2 anonymous classes with identical name/value pairs. var firstCar = new { Color = "Bright Pink", Make = "Saab",
CurrentSpeed = 55 };
var secondCar = new { Color = "Bright Pink", Make = "Saab", CurrentSpeed = 55 };

// Are they considered equal when using Equals()? if (firstCar.Equals(secondCar))
{
Console.WriteLine("Same anonymous object!");
}
else
{
Console.WriteLine("Not the same anonymous object!");
}

// Are they considered equal when using ==? if (firstCar == secondCar)
{
Console.WriteLine("Same anonymous object!");
}
else
{
Console.WriteLine("Not the same anonymous object!");
}

// Are these objects the same underlying type?
if (firstCar.GetType().Name == secondCar.GetType().Name)
{
Console.WriteLine("We are both the same type!");
}
else
{
Console.WriteLine("We are different types!");
}

// Show all the details. Console.WriteLine(); ReflectOverAnonymousType(firstCar); ReflectOverAnonymousType(secondCar);
}

When you call this method, the output might be somewhat surprising.

My car is a Bright Pink Saab.
You have a Black BMW going 90 MPH
ToString() == { Make = BMW, Color = Black, Speed = 90 }

Same anonymous object!
Not the same anonymous object! We are both the same type!

obj is an instance of: <>f AnonymousType0`3
Base class of <>f AnonymousType0`3 is System.Object
obj.ToString() == { Color = Bright Pink, Make = Saab, CurrentSpeed = 55 } obj.GetHashCode() == -925496951

obj is an instance of: <>f AnonymousType0`3
Base class of <>f AnonymousType0`3 is System.Object
obj.ToString() == { Color = Bright Pink, Make = Saab, CurrentSpeed = 55 } obj.GetHashCode() == -925496951

When you run this test code, you will see that the first conditional test where you call Equals() returns true and, therefore, the message “Same anonymous object!” prints out to the screen. This is because the compiler-generated Equals() method uses value-based semantics when testing for equality (e.g., checking the value of each field of the objects being compared).

However, the second conditional test, which makes use of the C# equality operator (==), prints out “Not the same anonymous object!” This might seem at first glance to be a bit counterintuitive. This result is because anonymous types do not receive overloaded versions of the C# equality operators (== and !=). Given this, when you test for equality of anonymous types using the C# equality operators (rather than the Equals() method), the references, not the values maintained by the objects, are being tested for equality.
Finally, in the final conditional test (where you examine the underlying type name), you find that the anonymous types are instances of the same compiler-generated class type (in this example,
<>f AnonymousType0`3) because firstCar and secondCar have the same properties (Color, Make, and
CurrentSpeed).
This illustrates an important but subtle point: the compiler will generate a new class definition only when an anonymous type contains unique names of the anonymous type. Thus, if you declare identical anonymous types (again, meaning the same names) within the same assembly, the compiler generates only a single anonymous type definition.

Anonymous Types Containing Anonymous Types
It is possible to create an anonymous type that is composed of other anonymous types. For example, assume you want to model a purchase order that consists of a timestamp, a price point, and the automobile purchased. Here is a new (slightly more sophisticated) anonymous type representing such an entity:

// Make an anonymous type that is composed of another. var purchaseItem = new {
TimeBought = DateTime.Now,
ItemBought = new {Color = "Red", Make = "Saab", CurrentSpeed = 55}, Price = 34.000};

ReflectOverAnonymousType(purchaseItem);

At this point, you should understand the syntax used to define anonymous types, but you might still be wondering exactly where (and when) to use this new language feature. To be blunt, anonymous type declarations should be used sparingly, typically only when making use of the LINQ technology set (see Chapter 13). You would never want to abandon the use of strongly typed classes/structures simply for the sake of doing so, given anonymous types’ numerous limitations, which include the following:
•You don’t control the name of the anonymous type.
•Anonymous types always extend System.Object.
•The fields and properties of an anonymous type are always read-only.
•Anonymous types cannot support events, custom methods, custom operators, or custom overrides.
•Anonymous types are always implicitly sealed.
•Anonymous types are always created using the default constructor.
However, when programming with the LINQ technology set, you will find that in many cases this syntax can be helpful when you want to quickly model the overall shape of an entity rather than its functionality.

Working with Pointer Types
And now for the final topic of the chapter, which most likely will be the least used of all C# features for most of your .NET projects.

■ Note In the examples that follow, I’m assuming you have some background in C++ pointer manipulation. If this is not true, feel free to skip this topic entirely. using pointers will not be a common task for most C# applications.

In Chapter 4, you learned that the .NET platform defines two major categories of data: value types and reference types. Truth be told, however, there is a third category: pointer types. To work with pointer types, you get specific operators and keywords that allow you to bypass the .NET Runtime’s memory management scheme and take matters into your own hands (see Table 11-2).

Table 11-2. Pointer-centric C# Operators and Keywords

Operator/Keyword Meaning in Life
* This operator is used to create a pointer variable (i.e., a variable that represents a direct location in memory). As in C++, this same operator is used for pointer indirection.
& This operator is used to obtain the address of a variable in memory.
-> This operator is used to access fields of a type that is represented by a pointer (the unsafe version of the C# dot operator).
[] This operator (in an unsafe context) allows you to index the slot pointed to by a pointer variable (if you’re a C++ programmer, you will recall the interplay between a pointer variable and the [] operator).
++, -- In an unsafe context, the increment and decrement operators can be applied to pointer types.
+, - In an unsafe context, the addition and subtraction operators can be applied to pointer types.
==, !=, <, >, <=, => In an unsafe context, the comparison and equality operators can be applied to pointer types.
Stackalloc In an unsafe context, the stackalloc keyword can be used to allocate C# arrays directly on the stack.
Fixed In an unsafe context, the fixed keyword can be used to temporarily fix a variable so that its address can be found.

Now, before digging into the details, let me again point out that you will seldom if ever need to make use of pointer types. Although C# does allow you to drop down to the level of pointer manipulations, understand that the .NET runtime has absolutely no clue of your intentions. Thus, if you mismanage a pointer, you are the one in charge of dealing with the consequences. Given these warnings, when exactly would you need to work with pointer types? There are two common situations.

• You are looking to optimize select parts of your application by directly manipulating memory outside the management of the .NET 5 Runtime.
• You are calling methods of a C-based .dll or COM server that demand pointer types as parameters. Even in this case, you can often bypass pointer types in favor of the System.IntPtr type and members of the System.Runtime.InteropServices. Marshal type.
When you decide to make use of this C# language feature, you are required to inform the C# compiler of your intentions by enabling your project to support “unsafe code.” Create a new Console Application project named UnsafeCode, and set the project to support unsafe code by adding the following to the UnsafeCode. csproj file:


true

Visual Studio 2022 provides a GUI to set this property. Access your project’s property page, navigate to the Build tab, and then select the “Allow unsafe code” box. See Figure 11-1.

Figure 11-1. Enabling unsafe code using Visual Studio 2022

To select the build configuration for the settings, hover over the check box or to the left side of the “Unsafe code” label, and a gear will appear. Click the gear to select where the setting will be applied. See Figure 11-2.

Figure 11-2. Specifying the configuration(s) for the unsafe code setting

The unsafe Keyword
When you want to work with pointers in C#, you must specifically declare a block of “unsafe code” using the unsafe keyword (any code that is not marked with the unsafe keyword is considered “safe” automatically). For example, the following Program.cs file declares a scope of unsafe code within the safe top-level statements:

using UnsafeCode;
Console.WriteLine("***** Calling method with unsafe code *****"); unsafe
{
// Work with pointer types here!
}
// Can't work with pointers here!

In addition to declaring a scope of unsafe code within a method, you can build structures, classes, type members, and parameters that are “unsafe.” Here are a few examples to gnaw on (no need to define the Node or Node2 types in your current project):

// This entire structure is "unsafe" and can
// be used only in an unsafe context. unsafe struct Node
{

public int Value; public Node* Left; public Node* Right;
}

// This struct is safe, but the Node2* members
// are not. Technically, you may access "Value" from
// outside an unsafe context, but not "Left" and "Right". public struct Node2
{
public int Value;

// These can be accessed only in an unsafe context! public unsafe Node2* Left;
public unsafe Node2* Right;
}

Methods (static or instance level) may be marked as unsafe as well. For example, assume you know that a static method will make use of pointer logic. To ensure that this method can be called only from an unsafe context, you could define the method as follows:

static unsafe void SquareIntPointer(int* myIntPointer)
{
// Square the value just for a test.
*myIntPointer *= *myIntPointer;
}

The configuration of your method demands that the caller invoke SquareIntPointer() as follows:

unsafe
{
int myInt = 10;

// OK, because we are in an unsafe context. SquareIntPointer(&myInt); Console.WriteLine("myInt: {0}", myInt);
}

int myInt2 = 5;

// Compiler error! Must be in unsafe context! SquareIntPointer(&myInt2); Console.WriteLine("myInt: {0}", myInt2);

If you would rather not force the caller to wrap the invocation within an unsafe context, you could wrap all the top-level statements with an unsafe block. If you are using a Main() method as entry point, you can update Main() with the unsafe keyword. In this case, the following code would compile:

static unsafe void Main(string[] args)
{
int myInt2 = 5;

SquareIntPointer(&myInt2); Console.WriteLine("myInt: {0}", myInt2);
}

If you run this version of the code, you will see the following output:

myInt: 25

■ Note It is important to note that the term unsafe was chosen for a reason. directly accessing the stack and working with pointers can cause unexpected issues with your application as well as the machine it is running on. If you have to work with unsafe code, be extra diligent.

Working with the * and & Operators
After you have established an unsafe context, you are then free to build pointers to data types using the
* operator and obtain the address of what is being pointed to using the & operator. Unlike in C or C++, in C# the * operator is applied to the underlying type only, not as a prefix to each pointer variable name. For example, consider the following code, which illustrates both the correct and incorrect ways to declare pointers to integer variables:

// No! This is incorrect under C#! int *pi, *pj;

// Yes! This is the way of C#. int* pi, pj;

Consider the following unsafe method:

static unsafe void PrintValueAndAddress()
{
int myInt;

// Define an int pointer, and
// assign it the address of myInt. int* ptrToMyInt = &myInt;

// Assign value of myInt using pointer indirection.
*ptrToMyInt = 123;

// Print some stats.
Console.WriteLine("Value of myInt {0}", myInt); Console.WriteLine("Address of myInt {0:X}", (int)&ptrToMyInt);
}

If you run this method from the unsafe block, you will see the following output:

**** Print Value And Address **** Value of myInt 123
Address of myInt 90F7E698

An Unsafe (and Safe) Swap Function
Of course, declaring pointers to local variables simply to assign their value (as in the previous example) is never required and not altogether useful. To illustrate a more practical example of unsafe code, assume you want to build a swap function using pointer arithmetic.

unsafe static void UnsafeSwap(int* i, int* j)
{
int temp = *i;
*i = *j;
*j = temp;
}

Very C-like, don’t you think? However, given your work previously, you should be aware that you could write the following safe version of your swap algorithm using the C# ref keyword:

static void SafeSwap(ref int i, ref int j)
{
int temp = i; i = j;
j = temp;
}

The functionality of each method is identical, thus reinforcing the point that direct pointer manipulation is not a mandatory task under C#. Here is the calling logic using safe top-level statements, with an unsafe context:

Console.WriteLine("***** Calling method with unsafe code *****");

// Values for swap. int i = 10, j = 20;

// Swap values "safely."
Console.WriteLine("\n***** Safe swap *****"); Console.WriteLine("Values before safe swap: i = {0}, j = {1}", i, j); SafeSwap(ref i, ref j);
Console.WriteLine("Values after safe swap: i = {0}, j = {1}", i, j);

// Swap values "unsafely."
Console.WriteLine("\n***** Unsafe swap *****"); Console.WriteLine("Values before unsafe swap: i = {0}, j = {1}", i, j); unsafe { UnsafeSwap(&i, &j); }

Console.WriteLine("Values after unsafe swap: i = {0}, j = {1}", i, j); Console.ReadLine();

Field Access via Pointers (the -> Operator)
Now assume you have defined a simple, safe Point structure, as follows:

struct Point
{
public int x; public int y;

public override string ToString() => $"({x}, {y})";
}

If you declare a pointer to a Point type, you will need to make use of the pointer field-access operator (represented by ->) to access its public members. As shown in Table 11-2, this is the unsafe version of
the standard (safe) dot operator (.). In fact, using the pointer indirection operator (*), it is possible to dereference a pointer to (once again) apply the dot operator notation. Check out the unsafe method:

static unsafe void UsePointerToPoint()
{
// Access members via pointer. Point;
Point* p = &point; p->x = 100;
p->y = 200;
Console.WriteLine(p->ToString());

// Access members via pointer indirection. Point point2;
Point* p2 = &point2; (*p2).x = 100;
(*p2).y = 200;
Console.WriteLine((*p2).ToString());
}

The stackalloc Keyword
In an unsafe context, you may need to declare a local variable that allocates memory directly from the call stack (and is, therefore, not subject to .NET garbage collection). To do so, C# provides the stackalloc keyword, which is the C# equivalent to the _alloca function of the C runtime library. Here is a simple example:

static unsafe string UnsafeStackAlloc()
{
char* p = stackalloc char[52]; for (int k = 0; k < 52; k++) { p[k] = (char)(k + 65)k; } return new string(p); } Pinning a Type via the fixed Keyword As you saw in the previous example, allocating a chunk of memory within an unsafe context may be facilitated via the stackalloc keyword. By the very nature of this operation, the allocated memory is cleaned up as soon as the allocating method has returned (as the memory is acquired from the stack). However, assume a more complex example. During our examination of the -> operator, you created a value type named Point. Like all value types, the allocated memory is popped off the stack once the executing scope has terminated. For the sake of argument, assume Point was instead defined as a reference type, like so:

class PointRef // <= Renamed and retyped. { public int x; public int y; public override string ToString() => $"({x}, {y})";
}

As you are aware, if the caller declares a variable of type Point, the memory is allocated on the garbage- collected heap. The burning question then becomes “What if an unsafe context wants to interact with this object (or any object on the heap)?” Given that garbage collection can occur at any moment, imagine the problems encountered when accessing the members of Point at the very point in time a sweep of the heap is underway. Theoretically, it is possible that the unsafe context is attempting to interact with a member that is no longer accessible or has been repositioned on the heap after surviving a generational sweep (which is an obvious problem).
To lock a reference type variable in memory from an unsafe context, C# provides the fixed keyword.
The fixed statement sets a pointer to a managed type and “pins” that variable during the execution of the code. Without fixed, pointers to managed variables would be of little use, since garbage collection could relocate the variables unpredictably. (In fact, the C# compiler will not allow you to set a pointer to a managed variable except in a fixed statement.)
Thus, if you create a PointRef object and want to interact with its members, you must write the following code (or receive a compiler error):

unsafe static void UseAndPinPoint()
{
PointRef pt = new PointRef
{
x = 5,
y = 6
};

// Pin pt in place so it will not
// be moved or GC-ed. fixed (int* p = &pt.x)
{
// Use int* variable here!
}

// pt is now unpinned, and ready to be GC-ed once
// the method completes. Console.WriteLine ("Point is: {0}", pt);
}

In a nutshell, the fixed keyword allows you to build a statement that locks a reference variable in memory, such that its address remains constant for the duration of the statement (or scope block). Any time you interact with a reference type from within the context of unsafe code, pinning the reference is a must.

The sizeof Keyword
The final unsafe-centric C# keyword to consider is sizeof. As in C++, the C# sizeof keyword is used to obtain the size in bytes of an intrinsic data type, but not a custom type, unless within an unsafe context. For example, the following method does not need to be declared “unsafe” as all arguments to the sizeof keyword are intrinsic types:

static void UseSizeOfOperator()
{
Console.WriteLine("The size of short is {0}.", sizeof(short)); Console.WriteLine("The size of int is {0}.", sizeof(int)); Console.WriteLine("The size of long is {0}.", sizeof(long));
}

However, if you want to get the size of your custom Point structure, you need to update this method as so (note the unsafe keyword has been added):

unsafe static void UseSizeOfOperator()
{
...
unsafe {
Console.WriteLine("The size of Point is {0}.", sizeof(Point));
}
}

That wraps up the look at some of the more advanced features of the C# programming language. To make sure we are all on the same page here, I again must say that most of your .NET projects might never need to directly use these features (especially pointers). Nevertheless, as you will see in later chapters, some topics are quite useful, if not required, when working with the LINQ APIs, most notably extension methods and anonymous types.

Summary
The purpose of this chapter was to deepen your understanding of the C# programming language. First, you investigated various advanced type construction techniques (indexer methods, overloaded operators, and custom conversion routines).
Next, you examined the role of extension methods and anonymous types. As you’ll see in some detail in Chapter 13, these features are useful when working with LINQ-centric APIs (though you can use them anywhere in your code, should they be useful). Recall that anonymous methods allow you to quickly model the “shape” of a type, while extension methods allow you to tack on new functionality to types, without the need to subclass.
You spent the remainder of this chapter examining a small set of lesser-known keywords (sizeof, unsafe, etc.) and during the process learned how to work with raw pointer types. As stated throughout the examination of pointer types, most of your C# applications will never need to use them.

Pro C#10 CHAPTER 10 Collections and Generics

CHAPTER 10

Collections and Generics

Any application you create with the .NET Core platform will need to contend with the issue of maintaining and manipulating a set of data points in memory. These data points can come from any variety of locations including a relational database, a local text file, an XML document, a web service call, or perhaps user- provided input.
When the .NET platform was first released, programmers frequently used the classes of the System. Collections namespace to store and interact with bits of data used within an application. In .NET 2.0, the C# programming language was enhanced to support a feature termed generics; with this change, a new namespace was introduced in the base class libraries: System.Collections.Generic.
This chapter will provide you with an overview of the various collection (generic and nongeneric) namespaces and types found within the .NET Core base class libraries. As you will see, generic containers are often favored over their nongeneric counterparts because they typically provide greater type safety and performance benefits. After you have learned how to create and manipulate the generic items found in the framework, the remainder of this chapter will examine how to build your own generic methods and generic types. As you do this, you will learn about the role of constraints (and the corresponding C# where keyword), which allow you to build extremely type-safe classes.

The Motivation for Collection Classes
The most primitive container you could use to hold application data is undoubtedly the array. As you saw in Chapter 4, C# arrays allow you to define a set of identically typed items (including an array of System. Objects, which essentially represents an array of any type of data) of a fixed upper limit. Also recall from Chapter 4 that all C# array variables gather a good deal of functionality from the System.Array class. By way of a quick review, consider the following code, which creates an array of textual data and manipulates its contents in various ways:

// Make an array of string data.
string[] strArray = {"First", "Second", "Third" };

// Show number of items in array using Length property. Console.WriteLine("This array has {0} items.", strArray.Length); Console.WriteLine();

// Display contents using enumerator. foreach (string s in strArray)
{
Console.WriteLine("Array Entry: {0}", s);
}

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_10

387

Console.WriteLine();

// Reverse the array and print again. Array.Reverse(strArray);
foreach (string s in strArray)
{
Console.WriteLine("Array Entry: {0}", s);
}

Console.ReadLine();

While basic arrays can be useful to manage small amounts of fixed-size data, there are many other times where you require a more flexible data structure, such as a dynamically growing and shrinking container or a container that can hold objects that meet only a specific criterion (e.g., only objects deriving from a specific base class or only objects implementing a particular interface). When you make use of a simple array, always remember they are created with a “fixed size.” If you make an array of three items, you get only three items; therefore, the following code will result in a runtime exception (an IndexOutOfRangeException, to be exact):

// Make an array of string data.
string[] strArray = { "First", "Second", "Third" };

// Try to add a new item at the end?? Runtime error! strArray[3] = "new item?";
...

■ Note It is actually possible to change the size of an array using the generic Resize() method. However, this will result in a copy of the data into a new array object and could be inefficient.

To help overcome the limitations of a simple array, the .NET Core base class libraries ship with a number of namespaces containing collection classes. Unlike a simple C# array, collection classes are built to dynamically resize themselves on the fly as you insert or remove items. Moreover, many of the collection classes offer increased type safety and are highly optimized to process the contained data in a memory-
efficient manner. As you read this chapter, you will quickly notice that a collection class can belong to one of two broad categories.
•Nongeneric collections (primarily found in the System.Collections namespace)
•Generic collections (primarily found in the System.Collections.Generic
namespace)
Nongeneric collections are typically designed to operate on System.Object types and are, therefore, loosely typed containers (however, some nongeneric collections do operate on only a specific type of data, such as string objects). In contrast, generic collections are much more type-safe, given that you must specify the “type of type” they contain upon creation. As you will see, the telltale sign of any generic item is the “type parameter” marked with angled brackets (e.g., List). You will examine the details of generics (including the many benefits they provide) a bit later in this chapter. For now, let’s examine some
of the key nongeneric collection types in the System.Collections and System.Collections.Specialized
namespaces.

The System.Collections Namespace
When the .NET platform was first released, programmers frequently used the nongeneric collection classes found within the System.Collections namespace, which contains a set of classes used to manage and organize large amounts of in-memory data. Table 10-1 documents some of the more commonly used collection classes of this namespace and the core interfaces they implement.

Table 10-1. Useful Types of System.Collections

System.Collections
Class Meaning in Life Key Implemented Interfaces
ArrayList Represents a dynamically sized collection of objects listed in sequential order IList, ICollection, IEnumerable, and ICloneable
BitArray Manages a compact array of bit values, which are represented as Booleans, where true indicates that the bit is on (1) and false indicates the bit is off (0) ICollection, IEnumerable, and ICloneable
Hashtable Represents a collection of key-value pairs that are organized based on the hash code of the key IDictionary, ICollection, IEnumerable, and ICloneable
Queue Represents a standard first-in, first-out (FIFO) collection of objects ICollection, IEnumerable, and ICloneable
SortedList Represents a collection of key-value pairs that are sorted by the keys and are accessible by key and by index IDictionary, ICollection, IEnumerable, and ICloneable
Stack A last-in, first-out (LIFO) stack providing push and pop (and peek) functionality ICollection, IEnumerable, and ICloneable

The interfaces implemented by these collection classes provide huge insights into their overall functionality. Table 10-2 documents the overall nature of these key interfaces, some of which you worked with firsthand in Chapter 8.

Table 10-2. Key Interfaces Supported by Classes of System.Collections

System.Collections
Interface Meaning in Life
ICollection Defines general characteristics (e.g., size, enumeration, and thread safety) for all nongeneric collection types
ICloneable Allows the implementing object to return a copy of itself to the caller
IDictionary Allows a nongeneric collection object to represent its contents using key-value pairs
IEnumerable Returns an object implementing the IEnumerator interface (see next table entry)
IEnumerator Enables foreach-style iteration of collection items
IList Provides behavior to add, remove, and index items in a sequential list of objects

An Illustrative Example: Working with the ArrayList
Based on your experience, you might have some firsthand experience using (or implementing) some of these classic data structures such as stacks, queues, and lists. If this is not the case, I will provide some further details on their differences when you examine their generic counterparts a bit later in this chapter. Until then, here is example code using an ArrayList object:

// You must import System.Collections to access the ArrayList. using System.Collections;
ArrayList strArray = new ArrayList();
strArray.AddRange(new string[] { "First", "Second", "Third" });

// Show number of items in ArrayList.
System.Console.WriteLine("This collection has {0} items.", strArray.Count); System.Console.WriteLine();

// Add a new item and display current count. strArray.Add("Fourth!");
System.Console.WriteLine("This collection has {0} items.", strArray.Count);

// Display contents.
foreach (string s in strArray)
{
System.Console.WriteLine("Entry: {0}", s);
}
System.Console.WriteLine();

Notice that you can add (or remove) items on the fly and the container automatically resizes itself accordingly.
As you would guess, the ArrayList class has many useful members beyond the Count property and AddRange() and Add() methods, so be sure you consult the .NET Core documentation for full details. On a related note, the other classes of System.Collections (Stack, Queue, etc.) are also fully documented in the
.NET Core help system.
However, it is important to point out that a majority of your .NET Core projects will most likely not make use of the collection classes in the System.Collections namespace! To be sure, these days it is far more common to make use of the generic counterpart classes found in the System.Collections.Generic namespace. Given this point, I won’t comment on (or provide code examples for) the remaining nongeneric classes found in System.Collections.

A Survey of System.Collections.Specialized Namespace
System.Collections is not the only .NET Core namespace that contains nongeneric collection classes. The System.Collections.Specialized namespace defines a number of (pardon the redundancy) specialized collection types. Table 10-3 documents some of the more useful types in this particular collection-centric namespace, all of which are nongeneric.

Table 10-3. Useful Classes of System.Collections.Specialized

System.Collections.
Specialized Type Meaning in Life
HybridDictionary This class implements IDictionary by using a ListDictionary while the collection is small and then switching to a Hashtable when the collection gets large.
ListDictionary This class is useful when you need to manage a small number of items (ten or so) that can change over time. This class makes use of a singly linked list to maintain its data.
StringCollection This class provides an optimal way to manage large collections of string data.
BitVector32 This class provides a simple structure that stores Boolean values and small integers in 32 bits of memory.

Beyond these concrete class types, this namespace also contains many additional interfaces and abstract base classes that you can use as a starting point for creating custom collection classes. While these “specialized” types might be just what your projects require in some situations, I won’t comment on their usage here. Again, in many cases, you will likely find that the System.Collections.Generic namespace provides classes with similar functionality and additional benefits.

■ Note there are two additional collection-centric namespaces (System.Collections.ObjectModel and System.Collections.Concurrent) in the .net Core base class libraries. You will examine the former namespace later in this chapter, after you are comfortable with the topic of generics. System.Collections. Concurrent provides collection classes well suited to a multithreaded environment (see Chapter 15 for information on multithreading).

The Problems of Nongeneric Collections
While it is true that many successful .NET and .NET Core applications have been built over the years using these nongeneric collection classes (and interfaces), history has shown that using these types can result in a number of issues.
The first issue is that using the System.Collections and System.Collections.Specialized classes can result in some poorly performing code, especially when you are manipulating numerical data (e.g., value types). As you’ll see momentarily, the CoreCLR must perform a number of memory transfer operations when you store structures in any nongeneric collection class prototyped to operate on System.Objects, which can hurt runtime execution speed.
The second issue is that most of the nongeneric collection classes are not type-safe because (again) they were developed to operate on System.Objects, and they could therefore contain anything at all.
If a developer needed to create a highly type-safe collection (e.g., a container that can hold objects implementing only a certain interface), the only real choice was to create a new collection class by hand. Doing so was not too labor intensive, but it was a tad on the tedious side.

Before you look at how to use generics in your programs, you’ll find it helpful to examine the issues of nongeneric collection classes a bit closer; this will help you better understand the problems generics intended to solve in the first place. If you want to follow along, create a new Console Application project
named IssuesWithNonGenericCollections. Next, make sure you import the System.Collections namespace to the top of the Program.cs file and clear out the rest of the code.

using System.Collections;

The Issue of Performance
As you might recall from Chapter 4, the .NET Core platform supports two broad categories of data: value types and reference types. Given that .NET Core defines two major categories of types, you might occasionally need to represent a variable of one category as a variable of the other category. To do so, C#
provides a simple mechanism, termed boxing, to store the data in a value type within a reference variable. Assume that you have created a local variable of type int in a method called SimpleBoxUnboxOperation. If, during the course of your application, you were to represent this value type as a reference type, you would box the value, as follows:

static void SimpleBoxUnboxOperation()
{
// Make a ValueType (int) variable. int myInt = 25;

// Box the int into an object reference. object boxedInt = myInt;
}

Boxing can be formally defined as the process of explicitly assigning a value type to a System.Object variable. When you box a value, the CoreCLR allocates a new object on the heap and copies the value type’s value (25, in this case) into that instance. What is returned to you is a reference to the newly allocated heap- based object.
The opposite operation is also permitted through unboxing. Unboxing is the process of converting the value held in the object reference back into a corresponding value type on the stack. Syntactically speaking, an unboxing operation looks like a normal casting operation. However, the semantics are quite different.
The CoreCLR begins by verifying that the receiving data type is equivalent to the boxed type, and if so, it copies the value back into a local stack-based variable. For example, the following unboxing operations work successfully, given that the underlying type of the boxedInt is indeed an int:

static void SimpleBoxUnboxOperation()
{
// Make a ValueType (int) variable. int myInt = 25;

// Box the int into an object reference. object boxedInt = myInt;

// Unbox the reference back into a corresponding int. int unboxedInt = (int)boxedInt;
}

When the C# compiler encounters boxing/unboxing syntax, it emits CIL code that contains the box/unbox op codes. If you were to examine your compiled assembly using ildasm.exe, you would find the following:

.method assembly hidebysig static
void '<

$>g SimpleBoxUnboxOperation|0_0'() cil managed
{
.maxstack 1
.locals init (int32 V_0, object V_1, int32 V_2) IL_0000: nop
IL_0001: ldc.i4.s 25 IL_0003: stloc.0 IL_0004: ldloc.0
IL_0005: box [System.Runtime]System.Int32 IL_000a: stloc.1
IL_000b: ldloc.1
IL_000c: unbox.any [System.Runtime]System.Int32 IL_0011: stloc.2
IL_0012: ret
} // end of method '$'::'<
$>g SimpleBoxUnboxOperation|0_0'

Remember that unlike when performing a typical cast, you must unbox into an appropriate data type. If you attempt to unbox a piece of data into the incorrect data type, an InvalidCastException exception will be thrown. To be perfectly safe, you should wrap each unboxing operation in try/catch logic; however, this would be quite labor intensive to do for every unboxing operation. Consider the following code update, which will throw an error because you’re attempting to unbox the boxed int into a long:

static void SimpleBoxUnboxOperation()
{
// Make a ValueType (int) variable. int myInt = 25;

// Box the int into an object reference. object boxedInt = myInt;

// Unbox in the wrong data type to trigger
// runtime exception.
try
{
long unboxedLong = (long)boxedInt;
}
catch (InvalidCastException ex)
{
Console.WriteLine(ex.Message);
}
}

At first glance, boxing/unboxing might seem like a rather uneventful language feature that is more academic than practical. After all, you will seldom need to store a local value type in a local object variable, as shown here. However, it turns out that the boxing/unboxing process is quite helpful because it allows you to assume everything can be treated as a System.Object, while the CoreCLR takes care of the memory- related details on your behalf.

Let’s look at a practical use of these techniques. We will examine the System.Collections.ArrayList class and use it to hold onto a batch of numeric (stack-allocated) data. The relevant members of the ArrayList class are listed as follows. Notice that they are prototyped to operate on System.Object data. Now consider the Add(), Insert(), and Remove() methods, as well as the class indexer.

public class ArrayList : IList, ICloneable
{
...
public virtual int Add(object? value);
public virtual void Insert(int index, object? value); public virtual void Remove(object? obj);
public virtual object? this[int index] {get; set; }
}

ArrayList has been built to operate on objects, which represent data allocated on the heap, so it might seem strange that the following code compiles and executes without throwing an error:

static void WorkWithArrayList()
{
// Value types are automatically boxed when
// passed to a method requesting an object. ArrayList myInts = new ArrayList(); myInts.Add(10);
myInts.Add(20); myInts.Add(35);
}

Although you pass in numerical data directly into methods requiring an object, the runtime automatically boxes the stack-based data on your behalf. Later, if you want to retrieve an item from the ArrayList using the type indexer, you must unbox the heap-allocated object into a stack-allocated integer using a casting operation. Remember that the indexer of the ArrayList is returning System.Objects, not System.Int32s.

static void WorkWithArrayList()
{
// Value types are automatically boxed when
// passed to a member requesting an object. ArrayList myInts = new ArrayList(); myInts.Add(10);
myInts.Add(20); myInts.Add(35);

// Unboxing occurs when an object is converted back to
// stack-based data.
int i = (int)myInts[0];

// Now it is reboxed, as WriteLine() requires object types!
Console.WriteLine("Value of your int: {0}", i);
}

Again, note that the stack-allocated System.Int32 is boxed prior to the call to ArrayList.Add(), so it can be passed in the required System.Object. Also note that the System.Object is unboxed back into a
System.Int32 once it is retrieved from the ArrayList via the casting operation, only to be boxed again when it is passed to the Console.WriteLine() method, as this method is operating on System.Object variables.
Boxing and unboxing are convenient from a programmer’s viewpoint, but this simplified approach to stack/heap memory transfer comes with the baggage of performance issues (in both speed of execution and code size) and a lack of type safety. To understand the performance issues, ponder these steps that must occur to box and unbox a simple integer:
1.A new object must be allocated on the managed heap.
2.The value of the stack-based data must be transferred into that memory location.
3.When unboxed, the value stored on the heap-based object must be transferred back to the stack.
4.The now unused object on the heap will (eventually) be garbage collected.
Although this particular WorkWithArrayList() method won’t cause a major bottleneck in terms of performance, you could certainly feel the impact if an ArrayList contained thousands of integers that your program manipulates on a somewhat regular basis. In an ideal world, you could manipulate stack-based data in a container without any performance issues. Ideally, it would be nice if you did not have to bother plucking data from this container using try/catch scopes (this is exactly what generics let you achieve).

The Issue of Type Safety
I touched on the issue of type safety when covering unboxing operations. Recall that you must unbox your data into the same data type it was declared as before boxing. However, there is another aspect of type safety you must keep in mind in a generic-free world: the fact that a majority of the classes of System.Collections can typically hold anything whatsoever because their members are prototyped to operate on System.
Objects. For example, this method builds an ArrayList of random bits of unrelated data:

static void ArrayListOfRandomObjects()
{
// The ArrayList can hold anything at all. ArrayList allMyObjects = new ArrayList(); allMyObjects.Add(true);
allMyObjects.Add(new OperatingSystem(PlatformID.MacOSX, new Version(10, 0))); allMyObjects.Add(66);
allMyObjects.Add(3.14);
}

In some cases, you will require an extremely flexible container that can hold literally anything (as shown here). However, most of the time you desire a type-safe container that can operate only on a particular type of data point. For example, you might need a container that can hold only database connections, bitmaps, or IPointy-compatible objects.
Prior to generics, the only way you could address this issue of type safety was to create a custom (strongly typed) collection class manually. Assume you want to create a custom collection that can contain only objects of type Person.

namespace IssuesWithNonGenericCollections; public class Person
{
public int Age {get; set;}
public string FirstName {get; set;} public string LastName {get; set;}

public Person(){}
public Person(string firstName, string lastName, int age)
{
Age = age;
FirstName = firstName;
LastName = lastName;
}

public override string ToString()
{
return $"Name: {FirstName} {LastName}, Age: {Age}";
}
}

To build a collection that can hold only Person objects, you could define a System.Collections. ArrayList member variable within a class named PersonCollection and configure all members to operate on strongly typed Person objects, rather than on System.Object types. Here is a simple example (a production-level custom collection could support many additional members and might extend an abstract base class from the System.Collections or System.Collections.Specialized namespace):

using System.Collections;
namespace IssuesWithNonGenericCollections; public class PersonCollection : IEnumerable
{
private ArrayList arPeople = new ArrayList();

// Cast for caller.
public Person GetPerson(int pos) => (Person)arPeople[pos];

// Insert only Person objects.
public void AddPerson(Person p)
{
arPeople.Add(p);
}

public void ClearPeople()
{
arPeople.Clear();
}

public int Count => arPeople.Count;

// Foreach enumeration support.
IEnumerator IEnumerable.GetEnumerator() => arPeople.GetEnumerator();
}

Notice that the PersonCollection class implements the IEnumerable interface, which allows a foreach- like iteration over each contained item. Also notice that your GetPerson() and AddPerson() methods have been prototyped to operate only on Person objects, not bitmaps, strings, database connections, nor other items. With these types defined, you are now assured of type safety, given that the C# compiler will be able
to determine any attempt to insert an incompatible data type. Update the using statements in Program.cs to the following and add the UserPersonCollection() method to the end of your current code:

using System.Collections;
using IssuesWithNonGenericCollections;
//Top level statements in Program.cs static void UsePersonCollection()
{
Console.WriteLine(" Custom Person Collection \n"); PersonCollection myPeople = new PersonCollection(); myPeople.AddPerson(new Person("Homer", "Simpson", 40)); myPeople.AddPerson(new Person("Marge", "Simpson", 38)); myPeople.AddPerson(new Person("Lisa", "Simpson", 9)); myPeople.AddPerson(new Person("Bart", "Simpson", 7)); myPeople.AddPerson(new Person("Maggie", "Simpson", 2));

// This would be a compile-time error!
// myPeople.AddPerson(new Car());

foreach (Person p in myPeople)
{
Console.WriteLine(p);
}
}

While custom collections do ensure type safety, this approach leaves you in a position where you must create an (almost identical) custom collection for each unique data type you want to contain. Thus, if you need a custom collection that can operate only on classes deriving from the Car base class, you need to build a highly similar collection class.

using System.Collections;
public class CarCollection : IEnumerable
{
private ArrayList arCars = new ArrayList();

// Cast for caller.
public Car GetCar(int pos) => (Car) arCars[pos];

// Insert only Car objects. public void AddCar(Car c)
{
arCars.Add(c);
}

public void ClearCars()
{
arCars.Clear();
}

public int Count => arCars.Count;

// Foreach enumeration support.
IEnumerator IEnumerable.GetEnumerator() => arCars.GetEnumerator();
}

However, a custom collection class does nothing to solve the issue of boxing/unboxing penalties.
Even if you were to create a custom collection named IntCollection that you designed to operate only on System.Int32 items, you would have to allocate some type of object to hold the data (e.g., System.Array and ArrayList).

using System.Collections;
public class IntCollection : IEnumerable
{
private ArrayList arInts = new ArrayList();

// Get an int (performs unboxing!).
public int GetInt(int pos) => (int)arInts[pos];

// Insert an int (performs boxing)!
public void AddInt(int i)
{
arInts.Add(i);
}

public void ClearInts()
{
arInts.Clear();
}

public int Count => arInts.Count;

IEnumerator IEnumerable.GetEnumerator() => arInts.GetEnumerator();
}

Regardless of which type you might choose to hold the integers, you cannot escape the boxing dilemma using nongeneric containers.

A First Look at Generic CollectionsT
When you use generic collection classes, you rectify all the previous issues, including boxing/unboxing penalties and a lack of type safety. Also, the need to build a custom (generic) collection class becomes quite rare. Rather than having to build unique classes that can contain people, cars, and integers, you can use a generic collection class and specify the type of type.
Consider the following method (added to the bottom of Program.cs), which uses the generic List class (in the System.Collections.Generic namespace) to contain various types of data in a strongly typed manner (don’t fret the details of generic syntax at this time):

static void UseGenericList()
{
Console.WriteLine(" Fun with Generics \n");

// This List<> can hold only Person objects. List morePeople = new List(); morePeople.Add(new Person ("Frank", "Black", 50)); Console.WriteLine(morePeople[0]);

// This List<> can hold only integers. List moreInts = new List(); moreInts.Add(10);
moreInts.Add(2);
int sum = moreInts[0] + moreInts[1];

// Compile-time error! Can't add Person object
// to a list of ints!
// moreInts.Add(new Person());
}

The first List object can contain only Person objects. Therefore, you do not need to perform a cast when plucking the items from the container, which makes this approach more type-safe. The second
List can contain only integers, all of which are allocated on the stack; in other words, there is no hidden boxing or unboxing as you found with the nongeneric ArrayList. Here is a short list of the benefits generic containers provide over their nongeneric counterparts:
• Generics provide better performance because they do not result in boxing or unboxing penalties when storing value types.
• Generics are type-safe because they can contain only the type of type you specify.
• Generics greatly reduce the need to build custom collection types because you specify the “type of type” when creating the generic container.

The Role of Generic Type Parameters
You can find generic classes, interfaces, structures, and delegates throughout the .NET Core base class libraries, and these might be part of any .NET Core namespace. Also be aware that generics have far more uses than simply defining a collection class. To be sure, you will see many different generics used in the remainder of this book for various reasons.

■ Note only classes, structures, interfaces, and delegates can be written generically; enum types cannot.

When you see a generic item listed in the .NET Core documentation or the Visual Studio Object Browser, you will notice a pair of angled brackets with a letter or other token sandwiched within. Figure 10-1 shows the Visual Studio Object Browser displaying a number of generic items located within the System.
Collections.Generic namespace, including the highlighted List class.

Figure 10-1. Generic items supporting type parameters

Formally speaking, you call these tokens type parameters; however, in more user-friendly terms, you can simply call them placeholders. You can read the symbol as “of T.” Thus, you can read IEnumerable as “IEnumerable of T” or, to say it another way, “IEnumerable of type T.”

■ Note the name of a type parameter (placeholder) is irrelevant, and it is up to the developer who created the generic item. However, typically T is used to represent types, TKey or K is used for keys, and TValue or V is used for values.

When you create a generic object, implement a generic interface, or invoke a generic member, it is up to you to supply a value to the type parameter. You’ll see many examples in this chapter and throughout the remainder of the text. However, to set the stage, let’s see the basics of interacting with generic types and members.

Specifying Type Parameters for Generic Classes/Structures
When you create an instance of a generic class or structure, you specify the type parameter when you declare the variable and when you invoke the constructor. As you saw in the preceding code example, UseGenericList() defined two List objects.

// This List<> can hold only Person objects. List morePeople = new List();
// This List<> can hold only integers. List moreInts = new List();

You can read the first line in the preceding snippet as “a List<> of T, where T is of type Person.” Or, more simply, you can read it as “a list of person objects.” After you specify the type parameter of a generic item, it cannot be changed (remember, generics are all about type safety). When you specify a type parameter for a generic class or structure, all occurrences of the placeholder(s) are now replaced with your supplied value.
If you were to view the full declaration of the generic List class using the Visual Studio Object Browser, you would see that the placeholder T is used throughout the definition of the List type. Here is a partial listing:

// A partial listing of the List class. namespace System.Collections.Generic;
public class List : IList, IList, IReadOnlyList
{
...
public void Add(T item);
public void AddRange(IEnumerable collection); public ReadOnlyCollection AsReadOnly(); public int BinarySearch(T item);
public bool Contains(T item); public void CopyTo(T[] array);
public int FindIndex(System.Predicate match); public T FindLast(System.Predicate match); public bool Remove(T item);
public int RemoveAll(System.Predicate match); public T[] ToArray();
public bool TrueForAll(System.Predicate match); public T this[int index] { get; set; }
}

When you create a List specifying Person objects, it is as if the List type were defined as follows:

namespace System.Collections.Generic; public class List
: IList, IList, IReadOnlyList
{
...
public void Add(Person item);
public void AddRange(IEnumerable collection); public ReadOnlyCollection AsReadOnly(); public int BinarySearch(Person item);
public bool Contains(Person item); public void CopyTo(Person[] array);
public int FindIndex(System.Predicate match); public Person FindLast(System.Predicate match); public bool Remove(Person item);
public int RemoveAll(System.Predicate match); public Person[] ToArray();

public bool TrueForAll(System.Predicate match); public Person this[int index] { get; set; }
}

Of course, when you create a generic List variable, the compiler does not literally create a new implementation of the List class. Rather, it will address only the members of the generic type you actually invoke.

Specifying Type Parameters for Generic Members
It is fine for a nongeneric class or structure to support generic properties. In these cases, you would also need to specify the placeholder value at the time you invoke the method. For example, System.Array supports several generic methods. Specifically, the nongeneric static Sort() method now has a generic counterpart named Sort(). Consider the following code snippet, where T is of type int:

int[] myInts = { 10, 4, 2, 33, 93 };

// Specify the placeholder to the generic
// Sort<>() method.
Array.Sort(myInts);

foreach (int i in myInts)
{
Console.WriteLine(i);
}

Specifying Type Parameters for Generic Interfaces
It is common to implement generic interfaces when you build classes or structures that need to support various framework behaviors (e.g., cloning, sorting, and enumeration). In Chapter 8, you learned about a number of nongeneric interfaces, such as IComparable, IEnumerable, IEnumerator, and IComparer. Recall that the nongeneric IComparable interface was defined like this:

public interface IComparable
{
int CompareTo(object obj);
}

In Chapter 8, you also implemented this interface on your Car class to enable sorting in a standard array. However, the code required several runtime checks and casting operations because the parameter was a general System.Object.

public class Car : IComparable
{
...
// IComparable implementation.
int IComparable.CompareTo(object obj)
{
if (obj is Car temp)

{
return this.CarID.CompareTo(temp.CarID);
}
throw new ArgumentException("Parameter is not a Car!");
}
}

Now assume you use the generic counterpart of this interface.

public interface IComparable
{
int CompareTo(T obj);
}

In this case, your implementation code will be cleaned up considerably.

public class Car : IComparable
{
...
// IComparable implementation.
int IComparable.CompareTo(Car obj)
{
if (this.CarID > obj.CarID)
{
return 1;
}
if (this.CarID < obj.CarID)
{
return -1;
}
return 0;
}
}

Here, you do not need to check whether the incoming parameter is a Car because it can only be a Car! If someone were to pass in an incompatible data type, you would get a compile-time error. Now that you have a better handle on how to interact with generic items, as well as the role of type parameters (aka placeholders), you’re ready to examine the classes and interfaces of the System.Collections.Generic namespace.

The System.Collections.Generic Namespace
When you are building a .NET Core application and need a way to manage in-memory data, the classes of System.Collections.Generic will most likely fit the bill. At the opening of this chapter, I briefly mentioned some of the core nongeneric interfaces implemented by the nongeneric collection classes. Not too surprisingly, the System.Collections.Generic namespace defines generic replacements for many of them.
In fact, you can find a number of the generic interfaces that extend their nongeneric counterparts. This might seem odd; however, by doing so, implementing classes will also support the legacy functionally found in their nongeneric siblings. For example, IEnumerable extends IEnumerable. Table 10-4 documents the core generic interfaces you’ll encounter when working with the generic collection classes.

Table 10-4. Key Interfaces Supported by Classes of System.Collections.Generic

System.Collections.Generic
Interface Meaning in Life
ICollection Defines general characteristics (e.g., size, enumeration, and thread safety) for all generic collection types.
IComparer Defines a way to compare to objects.
IDictionary<TKey, TValue> Allows a generic collection object to represent its contents using key- value pairs.
IEnumerable/ IAsyncEnumerable Returns the IEnumerator interface for a given object.
IAsyncEnumerable (new in C# 8.0) is covered in Chapter 15.
IEnumerator Enables foreach-style iteration over a generic collection.
IList Provides behavior to add, remove, and index items in a sequential list of objects.
ISet Provides the base interface for the abstraction of sets.

The System.Collections.Generic namespace also defines several classes that implement many of these key interfaces. Table 10-5 describes some commonly used classes of this namespace, the interfaces they implement, and their basic functionality.

Table 10-5. Classes of System.Collections.Generic

Generic Class Supported Key Interfaces Meaning in Life
Dictionary<TKey, TValue> ICollection, IDictionary<TKey, TValue>, IEnumerable This represents a generic collection of keys and values.
LinkedList ICollection, IEnumerable This represents a doubly linked list.
List ICollection, IEnumerable, IList This is a dynamically resizable sequential list of items.
Queue ICollection (not a typo; this is the nongeneric collection interface), IEnumerable This is a generic implementation of a first-in, first-out list.
SortedDictionary<TKey, TValue> ICollection, IDictionary<TKey, TValue>, IEnumerable This is a generic implementation of a sorted set of key-value pairs.
SortedSet ICollection, IEnumerable, ISet This represents a collection of objects that is maintained in sorted order with no duplication.
Stack ICollection (not a typo; this is the nongeneric collection interface), IEnumerable This is a generic implementation of a last-in, first-out list.

The System.Collections.Generic namespace also defines many auxiliary classes and structures that work in conjunction with a specific container. For example, the LinkedListNode type represents a node within a generic LinkedList, the KeyNotFoundException exception is raised when attempting to grab an

item from a container using a nonexistent key, and so forth. Be sure to consult the .NET Core documentation for full details of the System.Collections.Generic namespace.
In any case, your next task is to learn how to use some of these generic collection classes. Before you do, however, allow me to illustrate a C# language feature (first introduced in .NET 3.5) that simplifies the way you populate generic (and nongeneric) collection containers with data.

Understanding Collection Initialization Syntax
In Chapter 4, you learned about object initialization syntax, which allows you to set properties on a new variable at the time of construction. Closely related to this is collection initialization syntax. This C# language feature makes it possible to populate many containers (such as ArrayList or List) with items by using syntax similar to what you use to populate a basic array. Create a new .NET Core Console application named FunWithCollectionInitialization. Clear out the generated code in Program.cs and add the following using statements:

using System.Collections; using System.Drawing;

■ Note You can apply collection initialization syntax only to classes that support an Add() method, which is formalized by the ICollection/ICollection interfaces.

Consider the following examples:

// Init a standard array.
int[] myArrayOfInts = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// Init a generic List<> of ints.
List myGenericList = new List { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// Init an ArrayList with numerical data.
ArrayList myList = new ArrayList { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

If your container is managing a collection of classes or a structure, you can blend object initialization syntax with collection initialization syntax to yield some functional code. You might recall the Point class from Chapter 5, which defined two properties named X and Y. If you wanted to build a generic List of Point objects, you could write the following:

List myListOfPoints = new List
{
new Point { X = 2, Y = 2 }, new Point { X = 3, Y = 3 }, new Point { X = 4, Y = 4 }
};

foreach (var pt in myListOfPoints)
{
Console.WriteLine(pt);
}

Again, the benefit of this syntax is that you save yourself numerous keystrokes. While the nested curly brackets can become difficult to read if you don’t mind your formatting, imagine the amount of code that would be required to fill the following List of Rectangles if you did not have collection initialization syntax.

List myListOfRects = new List
{
new Rectangle {
Height = 90, Width = 90,
Location = new Point { X = 10, Y = 10 }}, new Rectangle {
Height = 50,Width = 50,
Location = new Point { X = 2, Y = 2 }},
};
foreach (var r in myListOfRects)
{
Console.WriteLine(r);
}

Working with the List Class
Create a new Console Application project named FunWithGenericCollections. Add a new file, named
Person.cs, and add the following code (which is the same code as the previous Person class):

namespace FunWithGenericCollections; public class Person
{
public int Age {get; set;}
public string FirstName {get; set;} public string LastName {get; set;}

public Person(){}
public Person(string firstName, string lastName, int age)
{
Age = age;
FirstName = firstName;
LastName = lastName;
}

public override string ToString()
{
return $"Name: {FirstName} {LastName}, Age: {Age}";
}
}

Clear out the generated code in Program.cs and add the following using statement:

using FunWithGenericCollections;

The first generic class you will examine is List, which you’ve already seen once or twice in this chapter. The List class is bound to be your most frequently used type in the System.Collections. Generic namespace because it allows you to resize the contents of the container dynamically. To illustrate the basics of this type, ponder the following method in your Program.cs file, which leverages List to manipulate the set of Person objects shown earlier in this chapter; you might recall that these Person objects defined three properties (Age, FirstName, and LastName) and a custom ToString() implementation:

static void UseGenericList()
{
// Make a List of Person objects, filled with
// collection/object init syntax. List people = new List()
{
new Person {FirstName= "Homer", LastName="Simpson", Age=47}, new Person {FirstName= "Marge", LastName="Simpson", Age=45}, new Person {FirstName= "Lisa", LastName="Simpson", Age=9}, new Person {FirstName= "Bart", LastName="Simpson", Age=8}
};

// Print out # of items in List. Console.WriteLine("Items in list: {0}", people.Count);

// Enumerate over list. foreach (Person p in people)
{
Console.WriteLine(p);
}

// Insert a new person.
Console.WriteLine("\n->Inserting new person.");
people.Insert(2, new Person { FirstName = "Maggie", LastName = "Simpson", Age = 2 }); Console.WriteLine("Items in list: {0}", people.Count);

// Copy data into a new array.
Person[] arrayOfPeople = people.ToArray(); foreach (Person p in arrayOfPeople)
{
Console.WriteLine("First Names: {0}", p.FirstName);
}
}

Here, you use collection initialization syntax to populate your List with objects, as a shorthand notation for calling Add() multiple times. After you print out the number of items in the collection (as well as enumerate over each item), you invoke Insert(). As you can see, Insert() allows you to plug a new item into the List at a specified index.
Finally, notice the call to the ToArray() method, which returns an array of Person objects based on the contents of the original List. From this array, you loop over the items again using the array’s indexer syntax. If you call this method from your top-level statements, you get the following output:

Fun with Generic Collections Items in list: 4
Name: Homer Simpson, Age: 47 Name: Marge Simpson, Age: 45 Name: Lisa Simpson, Age: 9 Name: Bart Simpson, Age: 8

->Inserting new person.
Items in list: 5
First Names: Homer First Names: Marge First Names: Maggie First Names: Lisa First Names: Bart

The List class defines many additional members of interest, so be sure to consult the documentation for more information. Next, let’s look at a few more generic collections, specifically Stack, Queue, and SortedSet. This should get you in a great position to understand your basic choices regarding how to hold your custom application data.

Working with the Stack Class
The Stack class represents a collection that maintains items using a last-in, first-out manner. As you might expect, Stack defines members named Push() and Pop() to place items onto or remove items from the stack. The following method creates a stack of Person objects:

static void UseGenericStack()
{
Stack stackOfPeople = new();
stackOfPeople.Push(new Person { FirstName = "Homer", LastName = "Simpson", Age = 47 }); stackOfPeople.Push(new Person { FirstName = "Marge", LastName = "Simpson", Age = 45 }); stackOfPeople.Push(new Person { FirstName = "Lisa", LastName = "Simpson", Age = 9 });

// Now look at the top item, pop it, and look again. Console.WriteLine("First person is: {0}", stackOfPeople.Peek()); Console.WriteLine("Popped off {0}", stackOfPeople.Pop()); Console.WriteLine("\nFirst person is: {0}", stackOfPeople.Peek()); Console.WriteLine("Popped off {0}", stackOfPeople.Pop()); Console.WriteLine("\nFirst person item is: {0}", stackOfPeople.Peek()); Console.WriteLine("Popped off {0}", stackOfPeople.Pop());

try
{
Console.WriteLine("\nnFirst person is: {0}", stackOfPeople.Peek()); Console.WriteLine("Popped off {0}", stackOfPeople.Pop());
}
catch (InvalidOperationException ex)

{
Console.WriteLine("\nError! {0}", ex.Message);
}
}

Here, you build a stack that contains three people, added in the order of their first names: Homer, Marge, and Lisa. As you peek into the stack, you will always see the object at the top first; therefore, the first call to Peek() reveals the third Person object. After a series of Pop() and Peek() calls, the stack eventually empties, at which time additional Peek() and Pop() calls raise a system exception. You can see the output for this here:

Fun with Generic Collections First person is: Name: Lisa Simpson, Age: 9 Popped off Name: Lisa Simpson, Age: 9

First person is: Name: Marge Simpson, Age: 45 Popped off Name: Marge Simpson, Age: 45

First person item is: Name: Homer Simpson, Age: 47 Popped off Name: Homer Simpson, Age: 47

Error! Stack empty.

Working with the Queue Class
Queues are containers that ensure items are accessed in a first-in, first-out manner. Sadly, we humans are subject to queues all day long: lines at the bank, lines at the movie theater, and lines at the morning coffeehouse. When you need to model a scenario in which items are handled on a first-come, first-served
basis, you will find the Queue class fits the bill. In addition to the functionality provided by the supported interfaces, Queue defines the key members shown in Table 10-6.

Table 10-6. Members of the Queue Type

Select Member of Queue Meaning in Life
Dequeue() Removes and returns the object at the beginning of the Queue
Enqueue() Adds an object to the end of the Queue
Peek() Returns the object at the beginning of the Queue without removing it

Now let’s put these methods to work. You can begin by leveraging your Person class again and building a Queue object that simulates a line of people waiting to order coffee.

static void UseGenericQueue()
{
// Make a Q with three people. Queue peopleQ = new();
peopleQ.Enqueue(new Person {FirstName= "Homer", LastName="Simpson", Age=47});

peopleQ.Enqueue(new Person {FirstName= "Marge", LastName="Simpson", Age=45}); peopleQ.Enqueue(new Person {FirstName= "Lisa", LastName="Simpson", Age=9});

// Peek at first person in Q.
Console.WriteLine("{0} is first in line!", peopleQ.Peek().FirstName);

// Remove each person from Q. GetCoffee(peopleQ.Dequeue()); GetCoffee(peopleQ.Dequeue()); GetCoffee(peopleQ.Dequeue());
// Try to de-Q again? try
{
GetCoffee(peopleQ.Dequeue());
}
catch(InvalidOperationException e)
{
Console.WriteLine("Error! {0}", e.Message);
}
//Local helper function
static void GetCoffee(Person p)
{
Console.WriteLine("{0} got coffee!", p.FirstName);
}
}

Here, you insert three items into the Queue class using its Enqueue() method. The call to Peek() allows you to view (but not remove) the first item currently in the Queue. Finally, the call to Dequeue() removes the item from the line and sends it into the GetCoffee() helper function for processing. Note that if you attempt to remove items from an empty queue, a runtime exception is thrown. Here is the output you receive when calling this method:

Fun with Generic Collections Homer is first in line!
Homer got coffee!
Marge got coffee!
Lisa got coffee!
Error! Queue empty.

Working with the PriorityQueue<TElement, TPriority> Class (New 10)
Introduced in .NET 6/C# 10, the PriorityQueue works just like the Queue except that each queued item is given a priority. When items are dequeued, they are removed from lowest to highest priority. The following updates the previous Queue example to use a PriorityQueue:

static void UsePriorityQueue()
{
Console.WriteLine(" Fun with Generic Priority Queues \n");

PriorityQueue<Person, int> peopleQ = new();
peopleQ.Enqueue(new Person { FirstName = "Lisa", LastName = "Simpson", Age = 9 }, 1); peopleQ.Enqueue(new Person { FirstName = "Homer", LastName = "Simpson", Age = 47 }, 3); peopleQ.Enqueue(new Person { FirstName = "Marge", LastName = "Simpson", Age = 45 }, 3); peopleQ.Enqueue(new Person { FirstName = "Bart", LastName = "Simpson", Age = 12 }, 2);

while (peopleQ.Count > 0)
{
Console.WriteLine(peopleQ.Dequeue().FirstName); //Displays Lisa Console.WriteLine(peopleQ.Dequeue().FirstName); //Displays Bart Console.WriteLine(peopleQ.Dequeue().FirstName); //Displays either Marge or Homer Console.WriteLine(peopleQ.Dequeue().FirstName); //Displays the other priority 3 item
}
}

If more than one item is set to the current lowest priority, the order of dequeuing is not guaranteed. As shown in code sample, the third call to Dequeue() will return either Homer or Marge, as they are both set to a priority of three. The fourth call will then return the other person. If exact order matters, you must ensure values for each priority are unique.

Working with the SortedSet Class
The SortedSet class is useful because it automatically ensures that the items in the set are sorted when you insert or remove items. However, you do need to inform the SortedSet class exactly how you
want it to sort the objects, by passing in as a constructor argument an object that implements the generic
IComparer interface.
Begin by creating a new class named SortPeopleByAge, which implements IComparer, where T is of type Person. Recall that this interface defines a single method named Compare(), where you can author whatever logic you require for the comparison. Here is a simple implementation of this class:

namespace FunWithGenericCollections; class SortPeopleByAge : IComparer
{
public int Compare(Person firstPerson, Person secondPerson)
{
if (firstPerson?.Age > secondPerson?.Age)
{
return 1;
}
if (firstPerson?.Age < secondPerson?.Age)
{
return -1;
}
return 0;
}
}

Now add the following new method that demonstrates using SortedSet:

static void UseSortedSet()
{
// Make some people with different ages.
SortedSet setOfPeople = new SortedSet(new SortPeopleByAge())
{
new Person {FirstName= "Homer", LastName="Simpson", Age=47}, new Person {FirstName= "Marge", LastName="Simpson", Age=45}, new Person {FirstName= "Lisa", LastName="Simpson", Age=9}, new Person {FirstName= "Bart", LastName="Simpson", Age=8}
};

// Note the items are sorted by age!
foreach (Person p in setOfPeople)
{
Console.WriteLine(p);
}
Console.WriteLine();

// Add a few new people, with various ages.
setOfPeople.Add(new Person { FirstName = "Saku", LastName = "Jones", Age = 1 }); setOfPeople.Add(new Person { FirstName = "Mikko", LastName = "Jones", Age = 32 });

// Still sorted by age!
foreach (Person p in setOfPeople)
{
Console.WriteLine(p);
}
}

When you run your application, the listing of objects is now always ordered based on the value of the
Age property, regardless of the order you inserted or removed objects.

Fun with Generic Collections Name: Bart Simpson, Age: 8
Name: Lisa Simpson, Age: 9 Name: Marge Simpson, Age: 45 Name: Homer Simpson, Age: 47

Name: Saku Jones, Age: 1 Name: Bart Simpson, Age: 8 Name: Lisa Simpson, Age: 9 Name: Mikko Jones, Age: 32 Name: Marge Simpson, Age: 45 Name: Homer Simpson, Age: 47

Working with the Dictionary<TKey, TValue> Class
Another handy generic collection is the Dictionary<TKey,TValue> type, which allows you to hold any number of objects that may be referred to via a unique key. Thus, rather than obtaining an item from a List using a numerical identifier (e.g., “Give me the second object”), you could use the unique text key (e.g., “Give me the object I keyed as Homer”).
Like other collection objects, you can populate a Dictionary<TKey,TValue> by calling the generic Add() method manually. However, you can also fill a Dictionary<TKey,TValue> using collection initialization syntax. Do be aware that when you are populating this collection object, key names must be unique. If you mistakenly specify the same key multiple times, you will receive a runtime exception.
Consider the following method that fills a Dictionary<K,V> with various objects. Notice when you create the Dictionary<TKey,TValue> object, you specify the key type (TKey) and underlying object type (TValue) as constructor arguments. In this example, you are using a string data type as the key and
a Person type as the value. Also note that you can combine object initialization syntax with collection initialization syntax.

private static void UseDictionary()
{
// Populate using Add() method
Dictionary<string, Person> peopleA = new Dictionary<string, Person>(); peopleA.Add("Homer", new Person { FirstName = "Homer", LastName = "Simpson", Age
= 47 });
peopleA.Add("Marge", new Person { FirstName = "Marge", LastName = "Simpson", Age
= 45 });
peopleA.Add("Lisa", new Person { FirstName = "Lisa", LastName = "Simpson", Age = 9 });

// Get Homer.
Person homer = peopleA["Homer"]; Console.WriteLine(homer);

// Populate with initialization syntax.
Dictionary<string, Person> peopleB = new Dictionary<string, Person>()
{
{ "Homer", new Person { FirstName = "Homer", LastName = "Simpson", Age = 47 } },
{ "Marge", new Person { FirstName = "Marge", LastName = "Simpson", Age = 45 } },
{ "Lisa", new Person { FirstName = "Lisa", LastName = "Simpson", Age = 9 } }
};

// Get Lisa.
Person lisa = peopleB["Lisa"]; Console.WriteLine(lisa);
}

It is also possible to populate a Dictionary<TKey,TValue> using a related initialization syntax that is specific to this type of container (not surprisingly termed dictionary initialization). Similar to the syntax used to populate the personB object in the previous code example, you still define an initialization scope for the collection object; however, you can use the indexer to specify the key and assign this to a new object as so:

// Populate with dictionary initialization syntax.
Dictionary<string, Person> peopleC = new Dictionary<string, Person>()
{
["Homer"] = new Person { FirstName = "Homer", LastName = "Simpson", Age = 47 }, ["Marge"] = new Person { FirstName = "Marge", LastName = "Simpson", Age = 45 }, ["Lisa"] = new Person { FirstName = "Lisa", LastName = "Simpson", Age = 9 }
};

The System.Collections.ObjectModel Namespace
Now that you understand how to work with the major generic classes, we will briefly examine an additional collection-centric namespace, System.Collections.ObjectModel. This is a relatively small namespace, which contains a handful of classes. Table 10-7 documents the two classes that you should most certainly be aware of.

Table 10-7. Useful Members of System.Collections.ObjectModel

System.Collections.
ObjectModel Type Meaning in Life
ObservableCollection Represents a dynamic data collection that provides notifications when items get added, when items get removed, or when the whole list is refreshed
ReadOnlyObservable Collection Represents a read-only version of ObservableCollection

The ObservableCollection class is useful, in that it has the ability to inform external objects when its contents have changed in some way (as you might guess, working with ReadOnlyObservableCollection

is similar but read-only in nature).

Working with ObservableCollection
Create a new Console Application project named FunWithObservableCollections and import the System.Collections.ObjectModel namespace into your initial C# code file. In many ways, working with ObservableCollection is identical to working with List, given that both of these classes implement the same core interfaces. What makes the ObservableCollection class unique is that this class supports an event named CollectionChanged. This event will fire whenever a new item is inserted, a current item is removed (or relocated), or the entire collection is modified.
Like any event, CollectionChanged is defined in terms of a delegate, which in this case is NotifyCollectionChangedEventHandler. This delegate can call any method that takes an object as the first parameter and takes a NotifyCollectionChangedEventArgs as the second. Consider the following code, which populates an observable collection containing Person objects and wires up the CollectionChanged event:

using System.Collections.ObjectModel; using System.Collections.Specialized; using FunWithObservableCollections;

// Make a collection to observe
//and add a few Person objects.
ObservableCollection people = new ObservableCollection()
{
new Person{ FirstName = "Peter", LastName = "Murphy", Age = 52 }, new Person{ FirstName = "Kevin", LastName = "Key", Age = 48 },
};

// Wire up the CollectionChanged event. people.CollectionChanged += people_CollectionChanged;

static void people_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
throw new NotImplementedException();
}

The incoming NotifyCollectionChangedEventArgs parameter defines two important properties, OldItems and NewItems, which will give you a list of items that were currently in the collection before the event fired and the new items that were involved in the change. However, you will want to examine these lists only under the correct circumstances. Recall that the CollectionChanged event can fire when items are added, removed, relocated, or reset. To discover which of these actions triggered the event, you can use the Action property of NotifyCollectionChangedEventArgs. The Action property can be tested against any of the following members of the NotifyCollectionChangedAction enumeration:

public enum NotifyCollectionChangedAction
{
Add = 0,
Remove = 1,
Replace = 2,
Move = 3,
Reset = 4,
}

Here is an implementation of the CollectionChanged event handler that will traverse the old and new sets when an item has been inserted into or removed from the collection at hand (notice the using for System.Collections.Specialized):

using System.Collections.Specialized;
...
static void people_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// What was the action that caused the event? Console.WriteLine("Action for this event: {0}", e.Action);

// They removed something.
if (e.Action == NotifyCollectionChangedAction.Remove)
{
Console.WriteLine("Here are the OLD items:"); foreach (Person p in e.OldItems)

{
Console.WriteLine(p.ToString());
}
Console.WriteLine();
}

// They added something.
if (e.Action == NotifyCollectionChangedAction.Add)
{
// Now show the NEW items that were inserted. Console.WriteLine("Here are the NEW items:"); foreach (Person p in e.NewItems)
{
Console.WriteLine(p.ToString());
}
}
}

Now, update your calling code to add and remove an item.

// Now add a new item.
people.Add(new Person("Fred", "Smith", 32));
// Remove an item. people.RemoveAt(0);

When you run the program, you will see output similar to the following:

Action for this event: Add Here are the NEW items:
Name: Fred Smith, Age: 32

Action for this event: Remove Here are the OLD items:
Name: Peter Murphy, Age: 52

That wraps up the examination of the various collection-centric namespaces. To conclude the chapter, you will now examine how you can build your own custom generic methods and custom generic types.

Creating Custom Generic Methods
While most developers typically use the existing generic types within the base class libraries, it is also possible to build your own generic members and custom generic types. Let’s look at how to incorporate custom generics into your own projects. The first step is to build a generic swap method. Begin by creating a new console application named CustomGenericMethods.
When you build custom generic methods, you achieve a supercharged version of traditional method overloading. In Chapter 2, you learned that overloading is the act of defining multiple versions of a single method, which differ by the number of, or type of, parameters.

While overloading is a useful feature in an object-oriented language, one problem is that you can easily end up with a ton of methods that essentially do the same thing. For example, assume you need to build some methods that can switch two pieces of data using a simple swap routine. You might begin by authoring a new static class with a method that can operate on integers, like this:

namespace CustomGenericMethods; static class SwapFunctions
{
// Swap two integers.
static void Swap(ref int a, ref int b)
{
int temp = a; a = b;
b = temp;
}
}

So far, so good. But now assume you also need to swap two Person objects; this would require authoring a new version of Swap().

// Swap two Person objects.
static void Swap(ref Person a, ref Person b)
{
Person temp = a; a = b;
b = temp;
}

No doubt, you can see where this is going. If you also needed to swap floating-point numbers, bitmaps, cars, buttons, etc., you would have to build even more methods, which would become a maintenance nightmare. You could build a single (nongeneric) method that operated on object parameters, but then you face all the issues you examined earlier in this chapter, including boxing, unboxing, a lack of type safety, explicit casting, and so on.
Whenever you have a group of overloaded methods that differ only by incoming arguments, this is your clue that generics could make your life easier. Consider the following generic Swap() method that can swap any two Ts:

// This method will swap any two items.
// as specified by the type parameter . static void Swap(ref T a, ref T b)
{
Console.WriteLine("You sent the Swap() method a {0}", typeof(T)); T temp = a;
a = b;
b = temp;
}

Notice how a generic method is defined by specifying the type parameters after the method name but before the parameter list. Here, you state that the Swap() method can operate on any two parameters of type . To spice things up a bit, you also print out the type name of the supplied placeholder to the console using C#’s typeof() operator. Now consider the following calling code, which swaps integers and strings:

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

// Swap 2 ints.
int a = 10, b = 90;
Console.WriteLine("Before swap: {0}, {1}", a, b); SwapFunctions.Swap(ref a, ref b); Console.WriteLine("After swap: {0}, {1}", a, b); Console.WriteLine();

// Swap 2 strings.
string s1 = "Hello", s2 = "There"; Console.WriteLine("Before swap: {0} {1}!", s1, s2); SwapFunctions.Swap(ref s1, ref s2); Console.WriteLine("After swap: {0} {1}!", s1, s2);

Console.ReadLine();

The output looks like this:

***** Fun with Custom Generic Methods ***** Before swap: 10, 90
You sent the Swap() method a System.Int32 After swap: 90, 10

Before swap: Hello There!
You sent the Swap() method a System.String After swap: There Hello!

The major benefit of this approach is that you have only one version of Swap() to maintain, yet it can operate on any two items of a given type in a type-safe manner. Better yet, stack-based items stay on the stack, while heap-based items stay on the heap!

Inference of Type Parameters
When you invoke generic methods such as Swap, you can optionally omit the type parameter if (and only if ) the generic method requires arguments because the compiler can infer the type parameter based on the member parameters. For example, you could swap two System.Boolean values by adding the following code to your top-level statements:

// Compiler will infer System.Boolean. bool b1 = true, b2 = false;
Console.WriteLine("Before swap: {0}, {1}", b1, b2); SwapFunctions.Swap(ref b1, ref b2); Console.WriteLine("After swap: {0}, {1}", b1, b2);

Even though the compiler can discover the correct type parameter based on the data type used to declare b1 and b2, you should get in the habit of always specifying the type parameter explicitly.

SwapFunctions.Swap(ref b1, ref b2);

This makes it clear to your fellow programmers that this method is indeed generic. Moreover, inference of type parameters works only if the generic method has at least one parameter. For example, assume you have the following generic method in your Program.cs file:

static void DisplayBaseClass()
{
// BaseType is a method used in reflection,
// which will be examined in Chapter 17
Console.WriteLine("Base class of {0} is: {1}.", typeof(T), typeof(T).BaseType);
}

In this case, you must supply the type parameter upon invocation.

...
// Must supply type parameter if
// the method does not take params. DisplayBaseClass(); DisplayBaseClass();

// Compiler error! No params? Must supply placeholder!
// DisplayBaseClass();
Console.ReadLine();

Of course, generic methods do not need to be static as they are in these examples. All rules and options for nongeneric methods also apply.

Creating Custom Generic Structures and Classes
Now that you understand how to define and invoke generic methods, it’s time to turn your attention to the construction of a generic structure (the process of building a generic class is identical) within a new Console Application project named GenericPoint. Assume you have built a generic Point structure that supports a single type parameter that represents the underlying storage for the (x, y) coordinates. The caller can then create Point types as follows:

// Point using ints.
Point p = new Point(10, 10);

// Point using double.
Point p2 = new Point(5.4, 3.3);

// Point using strings.
Point p3 = new Point(""",""3"");

Creating a point using strings might seem a bit odd at first, but consider the case of imaginary numbers. Then it might make sense to use strings for the values of X and Y of a point. Regardless, it demonstrates the power of generics. Here is the complete definition of Point:

namespace GenericPoint;
// A generic Point structure. public struct Point

{
// Generic state data. private T _xPos; private T _yPos;

// Generic constructor. public Point(T xVal, T yVal)
{
_xPos = xVal;
_yPos = yVal;
}

// Generic properties. public T X
{
get => _xPos;
set => _xPos = value;
}

public T Y
{
get => _yPos;
set => _yPos = value;
}

public override string ToString() => $"[{_xPos}, {_yPos}]";
}

As you can see, Point leverages its type parameter in the definition of the field data, constructor arguments, and property definitions.

Default Value Expressions with Generics
With the introduction of generics, the C# default keyword has been given a dual identity. In addition to its use within a switch construct, it can be used to set a type parameter to its default value. This is helpful
because a generic type does not know the actual placeholders up front, which means it cannot safely assume what the default value will be. The defaults for a type parameter are as follows:
• Numeric values have a default value of 0.
• Reference types have a default value of null.
• Fields of a structure are set to 0 (for value types) or null (for reference types).
To reset an instance of Point, you could set the X and Y values to 0 directly. This assumes the caller will supply only numerical data. What about the string version? This is where the default(T) syntax comes in handy. The default keyword resets a variable to the default value for the variable’s data type. Add a method called ResetPoint() as follows:

// Reset fields to the default value of the type parameter.
// The "default" keyword is overloaded in C#.
// When used with generics, it represents the default

// value of a type parameter. public void ResetPoint()
{
_xPos = default(T);
_yPos = default(T);
}

Now that you have the ResetPoint() method in place, you can fully exercise the methods of
Point struct. using GenericPoint;
Console.WriteLine("***** Fun with Generic Structures *****\n");
// Point using ints.
Point p = new Point(10, 10); Console.WriteLine("p.ToString()={0}", p.ToString()); p.ResetPoint(); Console.WriteLine("p.ToString()={0}", p.ToString()); Console.WriteLine();

// Point using double.
Point p2 = new Point(5.4, 3.3); Console.WriteLine("p2.ToString()={0}", p2.ToString()); p2.ResetPoint(); Console.WriteLine("p2.ToString()={0}", p2.ToString()); Console.WriteLine();

// Point using strings.
Point p3 = new Point("i", "3i"); Console.WriteLine("p3.ToString()={0}", p3.ToString()); p3.ResetPoint(); Console.WriteLine("p3.ToString()={0}", p3.ToString()); Console.ReadLine();

Here is the output:

***** Fun with Generic Structures ***** p.ToString()=[10, 10]
p.ToString()=[0, 0]

p2.ToString()=[5.4, 3.3]
p2.ToString()=[0, 0]

p3.ToString()=[i, 3i] p3.ToString()=[, ]

Default Literal Expressions (New 7.1)
In addition to setting the default value of a property, C# 7.1 introduced default literal expressions. This eliminates the need for specifying the type of the variable in the default statement. Update the ResetPoint() method to the following:

public void ResetPoint()
{
_xPos = default;
_yPos = default;
}

The default expression isn’t limited to simple variables but can also be applied to complex types. For example, to create and initialize the Point structure, you can write the following:

Point p4 = default; Console.WriteLine("p4.ToString()={0}", p4.ToString()); Console.WriteLine();
Point p5 = default; Console.WriteLine("p5.ToString()={0}", p5.ToString());

Pattern Matching with Generics (New 7.1)
Another update in C# 7.1 is the ability to pattern match on generics. Take the following method, which checks the Point instance for the data type that it is based on (arguably incomplete, but enough to show the concept):

static void PatternMatching(Point p)
{
switch (p)
{
case Point pString: Console.WriteLine("Point is based on strings"); return;
case Point pInt:
Console.WriteLine("Point is based on ints"); return;
}
}

To exercise the pattern matching code, update the top-level statements to the following:

Point p4 = default; Point p5 = default; PatternMatching(p4); PatternMatching(p5);

Constraining Type Parameters
As this chapter illustrates, any generic item has at least one type parameter that you need to specify at the time you interact with the generic type or member. This alone allows you to build some type-safe code; however, you can also use the where keyword to get extremely specific about what a given type parameter must look like.
Using this keyword, you can add a set of constraints to a given type parameter, which the C# compiler will check at compile time. Specifically, you can constrain a type parameter as described in Table 10-8.

Table 10-8. Possible Constraints for Generic Type Parameters

Generic Constraint Meaning in Life
where T : struct The type parameter must have System.ValueType in its chain of inheritance (i.e.,
must be a structure).
where T : class The type parameter must not have System.ValueType in its chain of inheritance (i.e., must be a reference type).
where T : new() The type parameter must have a default constructor. This is helpful if your generic type must create an instance of the type parameter because you cannot assume you know the format of custom constructors. Note that this constraint must be listed last on a multiconstrained type.
where T : NameOfBaseClass The type parameter must be derived from the class specified by
NameOfBaseClass.
where T : NameOfInterface The type parameter must implement the interface specified by
NameOfInterface. You can separate multiple interfaces as a comma-delimited list.

Unless you need to build some extremely type-safe custom collections, you might never need to use the where keyword in your C# projects. Regardless, the following handful of (partial) code examples illustrate how to work with the where keyword.

Examples of Using the where Keyword
Begin by assuming that you have created a custom generic class, and you want to ensure that the type parameter has a default constructor. This could be useful when the custom generic class needs to create instances of the T because the default constructor is the only constructor that is potentially common to all types. Also, constraining T in this way lets you get compile-time checking; if T is a reference type, the programmer remembered to redefine the default in the class definition (you might recall that the default constructor is removed in classes when you define your own).

// MyGenericClass derives from object, while
// contained items must have a default ctor. public class MyGenericClass where T : new()
{
...
}

Notice that the where clause specifies which type parameter is being constrained, followed by a colon operator. After the colon operator, you list each possible constraint (in this case, a default constructor). Here is another example:

// MyGenericClass derives from object, while
// contained items must be a class implementing IDrawable
// and must support a default ctor.
public class MyGenericClass where T : class, IDrawable, new()
{
...
}

In this case, T has three requirements. It must be a reference type (not a structure), as marked with the class token. Second, T must implement the IDrawable interface. Third, it must also have a default
constructor. Multiple constraints are listed in a comma-delimited list; however, you should be aware that the
new() constraint must always be listed last! Thus, the following code will not compile:

// Error! new() constraint must be listed last!
public class MyGenericClass where T : new(), class, IDrawable
{
...
}

If you ever create a custom generic collection class that specifies multiple type parameters, you can specify a unique set of constraints for each, using separate where clauses.

// must extend SomeBaseClass and have a default ctor,
// while must be a structure and implement the
// generic IComparable interface.
public class MyGenericClass where K : SomeBaseClass, new() where T : struct, IComparable
{
...
}

You will rarely encounter cases where you need to build a complete custom generic collection class; however, you can use the where keyword on generic methods as well. For example, if you want to specify that your generic Swap() method can operate only on structures, you will update the method like this:

// This method will swap any structure, but not classes. static void Swap(ref T a, ref T b) where T : struct
{
...
}

Note that if you were to constrain the Swap() method in this manner, you would no longer be able to swap string objects (as is shown in the sample code) because string is a reference type.

The Lack of Operator Constraints
I want to make one more comment about generic methods and constraints as this chapter ends. It might come as a surprise to you to find out that when creating generic methods, you will get a compiler error if you apply any C# operators (+, -, *, ==, etc.) on the type parameters. For example, imagine the usefulness of a class that can add, subtract, multiply, and divide generic types.

// Compiler error! Cannot apply
// operators to type parameters! public class BasicMath
{
public T Add(T arg1, T arg2)
{ return arg1 + arg2; }
public T Subtract(T arg1, T arg2)
{ return arg1 - arg2; }
public T Multiply(T arg1, T arg2)
{ return arg1 * arg2; }
public T Divide(T arg1, T arg2)
{ return arg1 / arg2; }
}

Unfortunately, the preceding BasicMath class will not compile. While this might seem like a major restriction, you need to remember that generics are generic. Of course, the numerical data can work with the binary operators of C#. However, for the sake of argument, if were a custom class or structure type, the compiler could assume the class supports the +, -, *, and / operators. Ideally, C# would allow a generic type to be constrained by supported operators, as in this example:

// Illustrative code only!
public class BasicMath where T : operator +, operator -, operator *, operator /
{
public T Add(T arg1, T arg2)
{ return arg1 + arg2; }
public T Subtract(T arg1, T arg2)
{ return arg1 - arg2; }
public T Multiply(T arg1, T arg2)
{ return arg1 * arg2; }
public T Divide(T arg1, T arg2)
{ return arg1 / arg2; }
}

Alas, operator constraints are not supported under the current version of C#. However, it is possible (albeit it requires a bit more work) to achieve the desired effect by defining an interface that supports these operators (C# interfaces can define operators!) and then specifying an interface constraint of the generic class. In any case, this wraps up this book’s initial look at building custom generic types. In Chapter 12, I will pick up the topic of generics once again while examining the delegate type.

Summary
This chapter began by examining the nongeneric collection types of System.Collections and System. Collections.Specialized, including the various issues associated with many nongeneric containers, such as a lack of type safety and the runtime overhead of boxing and unboxing operations. As mentioned, for these very reasons, modern-day .NET programs will typically make use of the generic collection classes found in System.Collections.Generic and System.Collections.ObjectModel.
As you have seen, a generic item allows you to specify placeholders (type parameters) at the time of object creation (or invocation, in the case of generic methods). While you will most often simply use the generic types provided in the .NET base class libraries, you will also be able to create your own generic types (and generic methods). When you do so, you have the option of specifying any number of constraints (using the where keyword) to increase the level of type safety and ensure that you perform operations on types of a known quantity that are guaranteed to exhibit certain basic capabilities.
As a final note, remember that generics are found in numerous locations within the .NET base class libraries. Here, you focused specifically on generic collections. However, as you work through the remainder of this book (and when you dive into the platform on your own terms), you will certainly find generic classes, structures, and delegates located in a given namespace. As well, be on the lookout for generic members of a nongeneric class!

Pro C#10 CHAPTER 9 Understanding Object Lifetime

CHAPTER 9

Understanding Object Lifetime

At this point in the book, you have learned a great deal about how to build custom class types using C#. Now you will see how the runtime manages allocated class instances (aka objects) via garbage collection (GC). C# programmers never directly deallocate a managed object from memory (recall there is no delete keyword in the C# language). Rather, .NET Core objects are allocated to a region of memory termed the managed heap, where they will be automatically destroyed by the garbage collector “sometime in the future.”
After you have looked at the core details of the collection process, you’ll learn how to programmatically interact with the garbage collector using the System.GC class type (which is something you will typically
not be required to do for a majority of your projects). Next, you’ll examine how the virtual System.Object. Finalize() method and IDisposable interface can be used to build classes that release internal unmanaged resources in a predictable and timely manner.
You will also delve into some functionality of the garbage collector introduced in .NET 4.0, including background garbage collections and lazy instantiation using the generic System.Lazy<> class. By the time you have completed this chapter, you will have a solid understanding of how .NET Core objects are managed by the runtime.

Classes, Objects, and References
To frame the topics covered in this chapter, it is important to further clarify the distinction between classes, objects, and reference variables. Recall that a class is nothing more than a blueprint that describes how an instance of this type will look and feel in memory. Classes, of course, are defined within a code file (which in C# takes a *.cs extension by convention). Consider the following simple Car class defined within a new C# Console Application project named SimpleGC:

namespace SimpleGC;
// Car.cs
public class Car
{
public int CurrentSpeed {get; set;} public string PetName {get; set;}

public Car(){}
public Car(string name, int speed)
{
PetName = name;
CurrentSpeed = speed;
}

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_9

357

public override string ToString()
=> $"{PetName} is going {CurrentSpeed} MPH";

}

After a class has been defined, you may allocate any number of objects using the C# new keyword. Understand, however, that the new keyword returns a reference to the object on the heap, not the actual object. If you declare the reference variable as a local variable in a method scope, it is stored on the stack for further use in your application. When you want to invoke members on the object, apply the C# dot operator to the stored reference, like so:

using SimpleGC;
Console.WriteLine(" GC Basics ");

// Create a new Car object on the managed heap.
// We are returned a reference to the object
// ("refToMyCar").
Car refToMyCar = new Car("Zippy", 50);

// The C# dot operator (.) is used to invoke members
// on the object using our reference variable. Console.WriteLine(refToMyCar.ToString()); Console.ReadLine();

Figure 9-1 illustrates the class, object, and reference relationship.

Figure 9-1. References to objects on the managed heap

■ Note Recall from Chapter 4 that structures are value types that are always allocated directly on the stack and are never placed on the .NET Core managed heap. Heap allocation occurs only when you are creating instances of classes.

The Basics of Object Lifetime
When you are building your C# applications, you are correct to assume that the .NET Core runtime environment will take care of the managed heap without your direct intervention. In fact, the golden rule of
.NET Core memory management is simple.

■ Rule allocate a class instance onto the managed heap using the new keyword and forget about it.

Once instantiated, the garbage collector will destroy an object when it is no longer needed. The next obvious question, of course, is “How does the garbage collector determine when an object is no longer needed?” The short (i.e., incomplete) answer is that the garbage collector removes an object from the heap only if it is unreachable by any part of your code base. Assume you have a method in your Program.cs file that allocates a local Car object as follows:

static void MakeACar()
{
// If myCar is the only reference to the Car object, it may be destroyed when this method returns.
Car myCar = new Car();
}

Notice that this Car reference (myCar) has been created directly within the MakeACar() method and has not been passed outside of the defining scope (via a return value or ref/out parameters). Thus, once this method call completes, the myCar reference is no longer reachable, and the associated Car object is now
a candidate for garbage collection. Understand, however, that you can’t guarantee that this object will be reclaimed from memory immediately after MakeACar() has completed. All you can assume at this point is that when the runtime performs the next garbage collection, the myCar object could be safely destroyed.
As you will most certainly discover, programming in a garbage-collected environment greatly simplifies your application development. In stark contrast, C++ programmers are painfully aware that if they fail
to manually delete heap-allocated objects, memory leaks are never far behind. In fact, tracking down memory leaks is one of the most time-consuming (and tedious) aspects of programming in unmanaged environments. By allowing the garbage collector to take charge of destroying objects, the burden of memory management has been lifted from your shoulders and placed onto those of the runtime.

The CIL of new
When the C# compiler encounters the new keyword, it emits a CIL newobj instruction into the method implementation. If you compile the current example code and investigate the resulting assembly using ildasm.exe, you’d find the following CIL statements within the MakeACar() method:

.method assembly hidebysig static
void '<

$>g MakeACar|0_0'() cil managed
{
// Code size 8 (0x8)
.maxstack 1
.locals init (class SimpleGC.Car V_0) IL_0000: nop

IL_0001: newobj instance void SimpleGC.Car::.ctor() IL_0006: stloc.0
IL_0007: ret
} // end of method '$'::'<

$>g MakeACar|0_0'

Before you examine the exact rules that determine when an object is removed from the managed heap, let’s check out the role of the CIL newobj instruction in a bit more detail. First, understand that the managed heap is more than just a random chunk of memory accessed by the runtime. The .NET Core garbage collector is quite a tidy housekeeper of the heap, given that it will compact empty blocks of memory (when necessary) for the purposes of optimization.
To aid in this endeavor, the managed heap maintains a pointer (commonly referred to as the next object pointer or new object pointer) that identifies exactly where the next object will be located. That said, the newobj instruction tells the runtime to perform the following core operations:
1.Calculate the total amount of memory required for the object to be allocated (including the memory required by the data members and the base classes).
2.Examine the managed heap to ensure that there is indeed enough room to host the object to be allocated. If there is, the specified constructor is called, and the caller is ultimately returned a reference to the new object in memory, whose address just happens to be identical to the last position of the next object pointer.
3.Finally, before returning the reference to the caller, advance the next object pointer to point to the next available slot on the managed heap.
Figure 9-2 illustrates the basic process.

Figure 9-2. The details of allocating objects onto the managed heap

As your application is busy allocating objects, the space on the managed heap may eventually become full. When processing the newobj instruction, if the runtime determines that the managed heap does not have sufficient memory to allocate the requested type, it will perform a garbage collection in an attempt to free up memory. Thus, the next rule of garbage collection is also quite simple.

■ Rule if the managed heap does not have sufficient memory to allocate a requested object, a garbage collection will occur.

Exactly how this garbage collection occurs, however, depends on which type of garbage collection your application uses. You’ll look at the differences a bit later in this chapter.

Setting Object References to null
C/C++ programmers often set pointer variables to null to ensure they are no longer referencing unmanaged memory. Given this, you might wonder what the end result is of assigning object references to null under C#. For example, assume the MakeACar() subroutine has now been updated as follows:

static void MakeACar()
{
Car myCar = new Car(); myCar = null;
}

When you assign object references to null, the compiler generates CIL code that ensures the reference (myCar, in this example) no longer points to any object. If you once again made use of ildasm.exe to view the CIL code of the modified MakeACar(), you would find the ldnull opcode (which pushes a null value on the virtual execution stack) followed by a stloc.0 opcode (which sets the null reference on the variable).

.method assembly hidebysig static
void '<

$>g MakeACar|0_0'() cil managed
{
// Code size 10 (0xa)
.maxstack 1
.locals init (class SimpleGC.Car V_0) IL_0000: nop
IL_0001: newobj instance void SimpleGC.Car::.ctor() IL_0006: stloc.0
IL_0007: ldnull IL_0008: stloc.0 IL_0009: ret
} // end of method '$'::'<
$>g MakeACar|0_0'

What you must understand, however, is that assigning a reference to null does not in any way force the garbage collector to fire up at that exact moment and remove the object from the heap. The only thing you have accomplished is explicitly clipping the connection between the reference and the object it previously pointed to. Given this point, setting references to null under C# is far less consequential than doing so in other C-based languages; however, doing so will certainly not cause any harm.

Determining If an Object Is Live
Now, back to the topic of how the garbage collector determines when an object is no longer needed. The garbage collector uses the following information to determine whether an object is live:
•Stack roots: Stack variables provided by the compiler and stack walker
•Garbage collection handles: Handles that point to managed objects that can be referenced from code or the runtime
•Static data: Static objects in application domains that can reference other objects

During a garbage collection process, the runtime will investigate objects on the managed heap to determine whether they are still reachable by the application. To do so, the runtime will build an object graph, which represents each reachable object on the heap. Object graphs are explained in some detail during the discussion of object serialization in Chapter 19. For now, just understand that object graphs are used to document all reachable objects. As well, be aware that the garbage collector will never graph the same object twice, thus avoiding the nasty circular reference count found in COM programming.
Assume the managed heap contains a set of objects named A, B, C, D, E, F, and G. During garbage collection, these objects (as well as any internal object references they may contain) are examined. After the graph has been constructed, unreachable objects (which you can assume are objects C and F) are marked as garbage. Figure 9-3 diagrams a possible object graph for the scenario just described (you can read the directional arrows using the phrase depends on or requires; for example, E depends on G and B, A depends on nothing, etc.).

Figure 9-3. Object graphs are constructed to determine which objects are reachable by application roots

After objects have been marked for termination (C and F in this case, as they are not accounted for in the object graph), they are swept from memory. At this point, the remaining space on the heap is
compacted, which in turn causes the runtime to modify the set of underlying pointers to refer to the correct memory location (this is done automatically and transparently). Last but not least, the next object pointer is readjusted to point to the next available slot. Figure 9-4 illustrates the resulting readjustment.

Figure 9-4. A clean and compacted heap

■ Note strictly speaking, the garbage collector uses two distinct heaps, one of which is specifically used to store large objects. This heap is less frequently consulted during the collection cycle, given possible
performance penalties involved with relocating large objects. in .NET Core, the large heap can be compacted on demand or when optional hard limits for absolute or percentage memory usage is reached.

Understanding Object Generations
When the runtime is attempting to locate unreachable objects, it does not literally examine every object placed on the managed heap. Doing so, obviously, would involve considerable time, especially in larger (i.e., real-world) applications.
To help optimize the process, each object on the heap is assigned to a specific “generation.” The idea behind generations is simple: the longer an object has existed on the heap, the more likely it is to stay there. For example, the class that defined the main window of a desktop application will be in memory until the program terminates. Conversely, objects that have only recently been placed on the heap (such as an object allocated within a method scope) are likely to be unreachable rather quickly. Given these assumptions, each object on the heap belongs to a collection in one of the following generations:
• Generation 0: Identifies a newly allocated object that has never been marked for collection (with the exception of large objects, which are initially placed in a generation 2 collection). Most objects are reclaimed for garbage collection in generation 0 and do now survive to generation 1.
• Generation 1: Identifies an object that has survived a garbage collection. This generation also serves as a buffer between short-lived objects and long-lived objects.
• Generation 2: Identifies an object that has survived more than one sweep of the garbage collector or a significantly large object that started in a generation 2 collection.

■ Note generations 0 and 1 are termed ephemeral generations. as explained in the next section, you will see that the garbage collection process does treat ephemeral generations differently.

The garbage collector will investigate all generation 0 objects first. If marking and sweeping (or said more plainly, getting rid of) these objects results in the required amount of free memory, any surviving objects are promoted to generation 1. To see how an object’s generation affects the collection process, ponder Figure 9-5, which diagrams how a set of surviving generation 0 objects (A, B, and E) are promoted once the required memory has been reclaimed.

Figure 9-5. Generation 0 objects that survive a garbage collection are promoted to generation 1

If all generation 0 objects have been evaluated but additional memory is still required, generation 1 objects are then investigated for reachability and collected accordingly. Surviving generation 1 objects are then promoted to generation 2. If the garbage collector still requires additional memory, generation 2 objects are evaluated. At this point, if a generation 2 object survives a garbage collection, it remains a generation 2 object, given the predefined upper limit of object generations.
The bottom line is that by assigning a generational value to objects on the heap, newer objects (such as local variables) will be removed quickly, while older objects (such as a program’s main window) are not “bothered” as often.
Garbage collection is triggered when the system has low physical memory, when memory allocated on the managed heap rises above an acceptable threshold, or when GC.Collect() is called in the application code.
If this all seems a bit wonderful and better than having to manage memory yourself, remember that the process of garbage collection is not without some cost. The timing of garbage collection and what gets collected when are typically out of the developers’ controls, although garbage collection can certainly
be influenced for good or bad. And when garbage collection is executing, CPU cycles are being used and can affect the performance of the application. The next sections examine the different types of garbage collection.

Ephemeral Generations and Segments
As mentioned earlier, generations 0 and 1 are short-lived and are known as ephemeral generations. These generations are allocated in a memory segment known as the ephemeral segment. As garbage collection occurs, new segments acquired by the garbage collection become new ephemeral segments, and the segment containing objects surviving past generation 1 becomes the new generation 2 segment.
The size of the ephemeral segment varies on a number of factors, such as the garbage collection type (covered next) and the bitness of the system. Table 9-1 shows the different sizes of the ephemeral segments.

Table 9-1. Ephemeral Segment Sizes

Garbage Collection Type 32-bit 64-bit
Workstation 16 MB 256 MB
Server 64 MB 4 GB
Server with > 4 logical CPUs 32 MB 2 GB
Server with > 8 logical CPUs 16 MB 1 GB

Garbage Collection Types
There are two types of garbage collection provided by the runtime:
•Workstation garbage collection: This is designed for client applications and is the default for stand-alone applications. Workstation GC can be background (covered next) or nonconcurrent.
•Server garbage collection: This is designed for server applications that require high throughput and scalability. Server GC can be background or nonconcurrent, just like workstation GC.

■ Note The names are indicative of the default settings for workstation and server applications, but the method of garbage collection is configurable through the machine’s runtimeconfig.json or system environment variables. Unless the computer has only one processor, then it will always use workstation garbage collection.

Workstation GC occurs on the same thread that triggered the garbage collection and remains at the same priority as when it was triggered. This can cause competition with other threads in the application.
Server GC occurs on multiple dedicated threads that are set to the THREAD_PRIORITY_HIGHEST priority level (threading is covered in Chapter 15). Each CPU gets a dedicated heap and dedicated thread to perform garbage collection. This can lead to server garbage collection becoming very resource intensive.

Background Garbage Collection
Beginning with .NET 4.0 (and continuing in .NET Core), the garbage collector is able to deal with thread suspension when it cleans up objects on the managed heap, using background garbage collection. Despite its name, this does not mean that all garbage collection now takes place on additional background threads of execution. Rather, if a background garbage collection is taking place for objects living in a nonephemeral

generation, the .NET Core runtime is now able to collect objects on the ephemeral generations using a dedicated background thread.
On a related note, the .NET 4.0 and higher garbage collection have been improved to further reduce the amount of time a given thread involved with garbage collection details must be suspended. The end result of these changes is that the process of cleaning up unused objects living in generation 0 or generation 1 has been optimized and can result in better runtime performance of your programs (which is really important for real-time systems that require a small, and predictable, GC stop time).
Do understand, however, that the introduction of this new garbage collection model has no effect on how you build your .NET Core applications. For all practical purposes, you can simply allow the garbage collector to perform its work without your direct intervention (and be happy that the folks at Microsoft are improving the collection process in a transparent manner).

The System.GC Type
The mscorlib.dll assembly provides a class type named System.GC that allows you to programmatically interact with the garbage collector using a set of static members. Now, do be aware that you will seldom (if ever) need to make use of this class directly in your code. Typically, the only time you will use the members of System.GC is when you are creating classes that make internal use of unmanaged resources. This could be the case if you are building a class that makes calls into the Windows C-based API using the .NET Core platform invocation protocol or perhaps because of some very low-level and complicated COM interop
logic. Table 9-2 provides a rundown of some of the more interesting members (consult the .NET Framework SDK documentation for complete details).

Table 9-2. Select Members of the System.GC Type

System.GC Member Description
AddMemoryPressure() RemoveMemoryPressure() Allows you to specify a numerical value that represents the calling object’s “urgency level” regarding the garbage collection process. Be aware that these methods should alter pressure in tandem and, thus, never remove more pressure than the total amount you have added.
Collect() Forces the GC to perform a garbage collection. This method has been overloaded to specify a generation to collect, as well as the mode of collection (via the GCCollectionMode enumeration).
CollectionCount() Returns a numerical value representing how many times a given generation has been swept.
GetGeneration() Returns the generation to which an object currently belongs.
GetTotalMemory() Returns the estimated amount of memory (in bytes) currently allocated on the managed heap. A Boolean parameter specifies whether the call should wait for garbage collection to occur before returning.
MaxGeneration Returns the maximum number of generations supported on the target system. Under Microsoft’s .NET 4.0, there are three possible generations: 0, 1, and 2.
SuppressFinalize() Sets a flag indicating that the specified object should not have its
Finalize() method called.
WaitForPendingFinalizers() Suspends the current thread until all finalizable objects have been finalized. This method is typically called directly after invoking GC. Collect().

To illustrate how the System.GC type can be used to obtain various garbage collection–centric details, update your top-level statements of the SimpleGC project to the following, which makes use of several members of GC:

Console.WriteLine(" Fun with System.GC ");

// Print out estimated number of bytes on heap. Console.WriteLine("Estimated bytes on heap: {0}",
GC.GetTotalMemory(false));

// MaxGeneration is zero based, so add 1 for display
// purposes.
Console.WriteLine("This OS has {0} object generations.\n", (GC.MaxGeneration + 1));

Car refToMyCar = new Car("Zippy", 100); Console.WriteLine(refToMyCar.ToString());

// Print out generation of refToMyCar object. Console.WriteLine("Generation of refToMyCar is: {0}",
GC.GetGeneration(refToMyCar)); Console.ReadLine();

After running this, you should see output similar to this:

Fun with System.GC

Estimated bytes on heap: 75760 This OS has 3 object generations.

Zippy is going 100 MPH Generation of refToMyCar is: 0

You will explore more of the methods from Table 9-2 in the next section.

Forcing a Garbage Collection
Again, the whole purpose of the garbage collector is to manage memory on your behalf. However, in some rare circumstances, it may be beneficial to programmatically force a garbage collection using GC.Collect(). Here are two common situations where you might consider interacting with the collection process:
• Your application is about to enter a block of code that you don’t want interrupted by a possible garbage collection.
• Your application has just finished allocating an extremely large number of objects, and you want to remove as much of the acquired memory as soon as possible.
If you determine it could be beneficial to have the garbage collector check for unreachable objects, you could explicitly trigger a garbage collection, as follows:

...
// Force a garbage collection and wait for
// each object to be finalized. GC.Collect(); GC.WaitForPendingFinalizers();
...

When you manually force a garbage collection, you should always make a call to GC. WaitForPendingFinalizers(). With this approach, you can rest assured that all finalizable objects (described in the next section) have had a chance to perform any necessary cleanup before your program continues. Under the hood, GC.WaitForPendingFinalizers() will suspend the calling thread during the collection process. This is a good thing, as it ensures your code does not invoke methods on an object currently being destroyed!
The GC.Collect() method can also be supplied a numerical value that identifies the oldest generation on which a garbage collection will be performed. For example, to instruct the runtime to investigate only generation 0 objects, you would write the following:

...
// Only investigate generation 0 objects.

GC.Collect(0); GC.WaitForPendingFinalizers();
...

As well, the Collect() method can be passed in a value of the GCCollectionMode enumeration as a second parameter, to fine-tune exactly how the runtime should force the garbage collection. This enum defines the following values:

public enum GCCollectionMode
{
Default, // Forced is the current default.
Forced, // Tells the runtime to collect immediately!
Optimized // Allows the runtime to determine whether the current time is optimal to reclaim objects.
}

As with any garbage collection, calling GC.Collect() promotes surviving generations. To illustrate, assume that your top-level statements have been updated as follows:

Console.WriteLine(" Fun with System.GC ");

// Print out estimated number of bytes on heap. Console.WriteLine("Estimated bytes on heap: {0}",
GC.GetTotalMemory(false));

// MaxGeneration is zero based.
Console.WriteLine("This OS has {0} object generations.\n", (GC.MaxGeneration + 1));
Car refToMyCar = new Car("Zippy", 100); Console.WriteLine(refToMyCar.ToString());

// Print out generation of refToMyCar. Console.WriteLine("\nGeneration of refToMyCar is: {0}",
GC.GetGeneration(refToMyCar));

// Make a ton of objects for testing purposes. object[] tonsOfObjects = new object[50000]; for (int i = 0; i < 50000; i++)
{
tonsOfObjects[i] = new object();
}

// Collect only gen 0 objects. Console.WriteLine("Force Garbage Collection"); GC.Collect(0, GCCollectionMode.Forced); GC.WaitForPendingFinalizers();

// Print out generation of refToMyCar. Console.WriteLine("Generation of refToMyCar is: {0}",
GC.GetGeneration(refToMyCar));

// See if tonsOfObjects[9000] is still alive. if (tonsOfObjects[9000] != null)
{
Console.WriteLine("Generation of tonsOfObjects[9000] is: {0}", GC.GetGeneration( tonsOfObjects[9000]));
}
else
{
Console.WriteLine("tonsOfObjects[9000] is no longer alive.");
}

// Print out how many times a generation has been swept. Console.WriteLine("\nGen 0 has been swept {0} times",
GC.CollectionCount(0));
Console.WriteLine("Gen 1 has been swept {0} times", GC.CollectionCount(1));
Console.WriteLine("Gen 2 has been swept {0} times", GC.CollectionCount(2));
Console.ReadLine();

Here, I have purposely created a large array of object types (50,000 to be exact) for testing purposes.
Here is the output from the program:

Fun with System.GC

Estimated bytes on heap: 75760 This OS has 3 object generations.

Zippy is going 100 MPH Generation of refToMyCar is: 0 Forcing Garbage Collection Generation of refToMyCar is: 1
Generation of tonsOfObjects[9000] is: 1

Gen 0 has been swept 1 times Gen 1 has been swept 0 times Gen 2 has been swept 0 times

At this point, I hope you feel more comfortable regarding the details of object lifetime. In the next section, you’ll examine the garbage collection process a bit further by addressing how you can build finalizable objects, as well as disposable objects. Be aware that the following techniques are typically necessary only if you are building C# classes that maintain internal unmanaged resources.

Building Finalizable Objects
In Chapter 6, you learned that the supreme base class of .NET Core, System.Object, defines a virtual method named Finalize(). The default implementation of this method does nothing whatsoever.

// System.Object

public class Object
{
...
protected virtual void Finalize() {}
}

When you override Finalize() for your custom classes, you establish a specific location to perform any necessary cleanup logic for your type. Given that this member is defined as protected, it is not possible to directly call an object’s Finalize() method from a class instance via the dot operator. Rather, the garbage collector will call an object’s Finalize() method (if supported) before removing the object from memory.

■ Note it is illegal to override Finalize() on structure types. This makes perfect sense given that structures are value types, which are never allocated on the heap to begin with and, therefore, are not garbage collected! However, if you create a structure that contains unmanaged resources that need to be cleaned up, you can implement the IDisposable interface (described shortly). Remember from Chapter 4 that ref structs and read-only ref structs can’t implement an interface but can implement a Dispose() method.

Of course, a call to Finalize() will (eventually) occur during a “natural” garbage collection or possibly when you programmatically force a collection via GC.Collect(). In prior versions of .NET (not .NET Core), each object’s finalizer is called on application shutdown. In .NET Core, there isn’t any way to force the finalizer to be executed, even when the app is shut down.
Now, despite what your developer instincts may tell you, the vast majority of your C# classes will not require any explicit cleanup logic or a custom finalizer. The reason is simple: if your classes are just making use of other managed objects, everything will eventually be garbage collected. The only time you would need to design a class that can clean up after itself is when you are using unmanaged resources (such
as raw OS file handles, raw unmanaged database connections, chunks of unmanaged memory, or other unmanaged resources). Under the .NET Core platform, unmanaged resources are obtained by directly calling into the API of the operating system using Platform Invocation Services (PInvoke) or as a result of some elaborate COM interoperability scenarios. Given this, consider the next rule of garbage collection.

■ Rule The only compelling reason to override Finalize() is if your C# class is using unmanaged resources via pinvoke or complex COm interoperability tasks (typically via various members defined by the System.Runtime.InteropServices.Marshal type). The reason is that under these scenarios you are manipulating memory that the runtime cannot manage.

Overriding System.Object.Finalize()
In the rare case that you do build a C# class that uses unmanaged resources, you will obviously want to ensure that the underlying memory is released in a predictable manner. Suppose you have created a new C# Console Application project named SimpleFinalize and inserted a class named MyResourceWrapper that uses an unmanaged resource (whatever that might be) and you want to override Finalize(). The odd thing about doing so in C# is that you can’t do it using the expected override keyword.

namespace SimpleFinalize; class MyResourceWrapper

{
// Compile-time error!
protected override void Finalize(){ }
}

Rather, when you want to configure your custom C# class types to override the Finalize() method, you make use of (C++-like) destructor syntax to achieve the same effect. The reason for this alternative form of overriding a virtual method is that when the C# compiler processes the finalizer syntax, it automatically adds a good deal of required infrastructure within the implicitly overridden Finalize() method (shown in just a moment).
C# finalizers look similar to constructors, in that they are named identically to the class they are defined within. In addition, finalizers are prefixed with a tilde symbol (~). Unlike a constructor, however, a finalizer never takes an access modifier (they are implicitly protected), never takes parameters, and can’t be overloaded (only one finalizer per class).
The following is a custom finalizer for MyResourceWrapper that will issue a system beep when invoked.
Obviously, this example is only for instructional purposes. A real-world finalizer would do nothing more than free any unmanaged resources and would not interact with other managed objects, even those referenced by the current object, as you can’t assume they are still alive at the point the garbage collector invokes your Finalize() method.

// Override System.Object.Finalize() via finalizer syntax. class MyResourceWrapper
{
// Clean up unmanaged resources here.
// Beep when destroyed (testing purposes only!)
~MyResourceWrapper() => Console.Beep();
}

If you were to examine this C# destructor using ildasm.exe, you would see that the compiler inserts some necessary error-checking code. First, the code statements within the scope of your Finalize() method are placed within a try block (see Chapter 7). The related finally block ensures that your base classes’ Finalize() method will always execute, regardless of any exceptions encountered within the try scope.

.method family hidebysig virtual instance void Finalize() cil managed
{
.override [System.Runtime]System.Object::Finalize
// Code size 17 (0x11)
.maxstack 1
.try
{
IL_0000: call void [System.Console]System.Console::Beep() IL_0005: nop
IL_0006: leave.s IL_0010
} // end .try finally
{
IL_0008: ldarg.0
IL_0009: call instance void [System.Runtime]System.Object::Finalize() IL_000e: nop
IL_000f: endfinally

} // end handler IL_0010: ret
} // end of method MyResourceWrapper::Finalize

If you then tested the MyResourceWrapper type, you would find that a system beep occurs when the finalizer executes.

using SimpleFinalize;

Console.WriteLine(" Fun with Finalizers \n"); Console.WriteLine("Hit return to create the objects "); Console.WriteLine("then force the GC to invoke Finalize()");
//Depending on the power of your system,
//you might need to increase these values CreateObjects(1_000_000);
//Artificially inflate the memory pressure GC.AddMemoryPressure(2147483647); GC.Collect(0, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); Console.ReadLine();

static void CreateObjects(int count)
{
MyResourceWrapper[] tonsOfObjects = new MyResourceWrapper[count];
for (int i = 0; i < count; i++)
{
tonsOfObjects[i] = new MyResourceWrapper();
}
tonsOfObjects = null;
}

■ Note The only way to guarantee that this small console app will force a garbage collection in .NET Core is to create a huge amount of the objects in memory and then set them to null. if you run this sample app, make sure to hit the Ctrl+C key combination to stop the program execution and all of the beeping!

Detailing the Finalization Process
It’s important to always remember that the role of the Finalize() method is to ensure that a .NET Core object can clean up unmanaged resources when it is garbage collected. Thus, if you are building a class that does not make use of unmanaged memory (by far the most common case), finalization is of little use. In fact, if at all possible, you should design your types to avoid supporting a Finalize() method for the simple reason that finalization takes time.
When you allocate an object onto the managed heap, the runtime automatically determines whether your object supports a custom Finalize() method. If so, the object is marked as finalizable, and a pointer to this object is stored on an internal queue named the finalization queue. The finalization queue is a table

maintained by the garbage collector that points to every object that must be finalized before it is removed from the heap.
When the garbage collector determines it is time to free an object from memory, it examines each entry on the finalization queue and copies the object off the heap to yet another managed structure termed the finalization reachable table (often abbreviated as freachable and pronounced “eff-reachable”). At this point, a separate thread is spawned to invoke the Finalize() method for each object on the freachable table at the next garbage collection. Given this, it will take, at the least, two garbage collections to truly finalize an object.
The bottom line is that while finalization of an object does ensure an object can clean up unmanaged resources, it is still nondeterministic in nature and, because of the extra behind-the-curtains processing, considerably slower.

Building Disposable Objects
As you have seen, finalizers can be used to release unmanaged resources when the garbage collector kicks in. However, given that many unmanaged objects are “precious items” (such as raw database or file
handles), it could be valuable to release them as soon as possible instead of relying on a garbage collection to occur. As an alternative to overriding Finalize(), your class could implement the IDisposable interface, which defines a single method named Dispose() as follows:

public interface IDisposable
{
void Dispose();
}

When you do implement the IDisposable interface, the assumption is that when the object user is finished using the object, the object user manually calls Dispose() before allowing the object reference to drop out of scope. In this way, an object can perform any necessary cleanup of unmanaged resources without incurring the hit of being placed on the finalization queue and without waiting for the garbage collector to trigger the class’s finalization logic.

■ Note Non-ref structures and class types both can implement IDisposable (unlike overriding Finalize(), which is reserved for class types), as the object user (not the garbage collector) invokes the Dispose() method. disposable ref structs were covered in Chapter 4.

To illustrate the use of this interface, create a new C# Console Application project named SimpleDispose. Here is an updated MyResourceWrapper class that now implements IDisposable, rather than overriding System.Object.Finalize():

namespace SimpleDispose;
// Implementing IDisposable.
class MyResourceWrapper : IDisposable
{
// The object user should call this method
// when they finish with the object. public void Dispose()
{
// Clean up unmanaged resources...
// Dispose other contained disposable objects...

// Just for a test.
Console.WriteLine(" In Dispose! ");
}
}

Notice that a Dispose() method not only is responsible for releasing the type’s unmanaged resources but can also call Dispose() on any other contained disposable methods. Unlike with Finalize(), it is perfectly safe to communicate with other managed objects within a Dispose() method. The reason is simple: the garbage collector has no clue about the IDisposable interface and will never call Dispose(). Therefore, when the object user calls this method, the object is still living a productive life on the managed heap and has access to all other heap-allocated objects. The calling logic, shown here, is straightforward:

using SimpleDispose;
Console.WriteLine(" Fun with Dispose \n");
// Create a disposable object and call Dispose()
// to free any internal resources. MyResourceWrapper rw = new MyResourceWrapper(); rw.Dispose();
Console.ReadLine();

Of course, before you attempt to call Dispose() on an object, you will want to ensure the type supports the IDisposable interface. While you will typically know which base class library types implement IDisposable by consulting the documentation, a programmatic check can be accomplished using the is or as keyword discussed in Chapter 6.

Console.WriteLine(" Fun with Dispose \n"); MyResourceWrapper rw = new MyResourceWrapper();
if (rw is IDisposable)
{
rw.Dispose();
}
Console.ReadLine();

This example exposes yet another rule regarding memory management.

■ Rule it is a good idea to call Dispose() on any object you directly create if the object supports IDisposable. The assumption you should make is that if the class designer chose to support the Dispose() method, the type has some cleanup to perform. if you forget, memory will eventually be cleaned up (so don’t panic), but it could take longer than necessary.

There is one caveat to the previous rule. A number of types in the base class libraries that do implement the IDisposable interface provide a (somewhat confusing) alias to the Dispose() method, in an attempt to make the disposal-centric method sound more natural for the defining type. By way of an example, while the System.IO.FileStream class implements IDisposable (and therefore supports a Dispose() method), it also defines the following Close() method that is used for the same purpose:

static void DisposeFileStream()
{
FileStream fs = new FileStream("myFile.txt", FileMode.OpenOrCreate);

// Confusing, to say the least!
// These method calls do the same thing! fs.Close();
fs.Dispose();
}

While it does feel more natural to “close” a file rather than “dispose” of one, this doubling up of cleanup methods can be confusing. For the few types that do provide an alias, just remember that if a type implements IDisposable, calling Dispose() is always a safe course of action.

Reusing the C# using Keyword
When you are handling a managed object that implements IDisposable, it is quite common to make use of structured exception handling to ensure the type’s Dispose() method is called in the event of a runtime exception, like so:

Console.WriteLine(" Fun with Dispose \n"); MyResourceWrapper rw = new MyResourceWrapper ();
try
{
// Use the members of rw.
}
finally
{
// Always call Dispose(), error or not. rw.Dispose();
}

While this is a fine example of defensive programming, the truth of the matter is that few developers are thrilled by the prospects of wrapping every disposable type within a try/finally block just to ensure the Dispose() method is called. To achieve the same result in a much less obtrusive manner, C# supports a special bit of syntax that looks like this:

Console.WriteLine(" Fun with Dispose \n");
// Dispose() is called automatically when the using scope exits. using(MyResourceWrapper rw = new MyResourceWrapper())
{
// Use rw object.
}

If you looked at the following CIL code of the top-level statements using ildasm.exe, you would find the
using syntax does indeed expand to try/finally logic, with the expected call to Dispose():

.method private hidebysig static void '

$'(string[] args) cil managed
{
...
.try
{
} // end .try finally
{
IL_0019: callvirt instance void [System.Runtime]System.IDisposable::Dispose()
} // end handler
} // end of method '$'::'
$'

■ Note if you attempt to “use” an object that does not implement IDisposable, you will receive a compiler error.

While this syntax does remove the need to manually wrap disposable objects within try/finally logic, the C# using keyword unfortunately now has a double meaning (importing namespaces and invoking a Dispose() method). Nevertheless, when you are working with types that support the IDisposable interface, this syntactical construct will ensure that the object “being used” will automatically have its Dispose() method called once the using block has exited.
Also, be aware that it is possible to declare multiple objects of the same type within a using scope. As you would expect, the compiler will inject code to call Dispose() on each declared object.

// Use a comma-delimited list to declare multiple objects to dispose. using(MyResourceWrapper rw = new MyResourceWrapper(), rw2 = new MyResourceWrapper())
{
// Use rw and rw2 objects.
}

Using Declarations (New 8.0)
New in C# 8.0 is the addition of using declarations. A using declaration is a variable declaration preceded by the using keyword. This is functionally the same as the syntax covered in the last question, with the exception of the explicit code block marked by braces ({}).
Add the following method to your class:

private static void UsingDeclaration()
{
//This variable will be in scope until the end of the method using var rw = new MyResourceWrapper();
//Do something here Console.WriteLine("About to dispose.");
//Variable is disposed at this point.
}

Next, add the following call to the top-level statements:

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

...
Console.WriteLine("Demonstrate using declarations"); UsingDeclaration();
Console.ReadLine();

If you examine the new method with ILDASM, you will (as you might expect) find the same code as before.

.method private hidebysig static
void UsingDeclaration() cil managed
{
...
.try
{
...
} // end .try finally
{

...

IL_0018: callvirt instance void [System.Runtime]System.IDisposable::Dispose()

} // end handler IL_001f: ret
} // end of method Program::UsingDeclaration

This new feature is essentially compiler magic, saving a few keystrokes. Be careful when using it, as the new syntax is not as explicit as the previous syntax.

Building Finalizable and Disposable Types
At this point, you have seen two different approaches to constructing a class that cleans up internal unmanaged resources. On the one hand, you can use a finalizer. Using this technique, you have the peace of mind that comes with knowing the object cleans itself up when garbage collected (whenever that may be) without the need for user interaction. On the other hand, you can implement IDisposable to provide a way for the object user to clean up the object as soon as it is finished. However, if the caller forgets to call Dispose(), the unmanaged resources may be held in memory indefinitely.
As you might suspect, it is possible to blend both techniques into a single class definition. By doing so, you gain the best of both models. If the object user does remember to call Dispose(), you can inform the garbage collector to bypass the finalization process by calling GC.SuppressFinalize(). If the object user forgets to call Dispose(), the object will eventually be finalized and have a chance to free up the internal resources. The good news is that the object’s internal unmanaged resources will be freed one way or another.
Here is the next iteration of MyResourceWrapper, which is now finalizable and disposable, defined in a C# Console Application project named FinalizableDisposableClass:

namespace FinalizableDisposableClass;
// A sophisticated resource wrapper.
public class MyResourceWrapper : IDisposable
{
// The garbage collector will call this method if the object user forgets to call Dispose().

~MyResourceWrapper()
{
// Clean up any internal unmanaged resources.
// Do not call Dispose() on any managed objects.
}
// The object user will call this method to clean up resources ASAP. public void Dispose()
{
// Clean up unmanaged resources here.
// Call Dispose() on other contained disposable objects.
// No need to finalize if user called Dispose(), so suppress finalization. GC.SuppressFinalize(this);
}
}

Notice that this Dispose() method has been updated to call GC.SuppressFinalize(), which informs the runtime that it is no longer necessary to call the destructor when this object is garbage collected, given that the unmanaged resources have already been freed via the Dispose() logic.

A Formalized Disposal Pattern
The current implementation of MyResourceWrapper does work fairly well; however, you are left with a few minor drawbacks. First, the Finalize() and Dispose() methods each have to clean up the same unmanaged resources. This could result in duplicate code, which can easily become a nightmare to maintain. Ideally, you would define a private helper function that is called by either method.
Next, you’d like to make sure that the Finalize() method does not attempt to dispose of any managed objects, while the Dispose() method should do so. Finally, you’d also like to be certain the object user can safely call Dispose() multiple times without error. Currently, the Dispose() method has no such safeguards.
To address these design issues, Microsoft defined a formal, prim-and-proper disposal pattern that strikes a balance between robustness, maintainability, and performance. Here is the final (and annotated) version of MyResourceWrapper, which makes use of this official pattern:

class MyResourceWrapper : IDisposable
{
// Used to determine if Dispose() has already been called. private bool disposed = false;

public void Dispose()
{
// Call our helper method.
// Specifying "true" signifies that the object user triggered the cleanup. CleanUp(true);

// Now suppress finalization. GC.SuppressFinalize(this);
}

private void CleanUp(bool disposing)
{
// Be sure we have not already been disposed! if (!this.disposed)

{

// If disposing equals true, dispose all managed resources. if (disposing)
{
// Dispose managed resources.
}
// Clean up unmanaged resources here.
}
disposed = true;
}
~MyResourceWrapper()
{
// Call our helper method.
// Specifying "false" signifies that the GC triggered the cleanup. CleanUp(false);
}
}

Notice that MyResourceWrapper now defines a private helper method named CleanUp(). By specifying true as an argument, you indicate that the object user has initiated the cleanup, so you should clean up all managed and unmanaged resources. However, when the garbage collector initiates the cleanup, you specify false when calling CleanUp() to ensure that internal disposable objects are not disposed (as you can’t assume they are still in memory!). Last but not least, the bool member variable (disposed) is set to true before exiting CleanUp() to ensure that Dispose() can be called numerous times without error.

■ Note after an object has been “disposed,” it’s still possible for the client to invoke members on it, as it is still in memory. Therefore, a robust resource wrapper class would also need to update each member of the class with additional coding logic that says, in effect, “if i am disposed, do nothing and return from the member.”

To test the final iteration of MyResourceWrapper, update your Program.cs file to the following:

using FinalizableDisposableClass;

Console.WriteLine(" Dispose() / Destructor Combo Platter ");

// Call Dispose() manually. This will not call the finalizer. MyResourceWrapper rw = new MyResourceWrapper();
rw.Dispose();

// Don't call Dispose(). This will trigger the finalizer when the object gets GCd. MyResourceWrapper rw2 = new MyResourceWrapper();

Notice that you are explicitly calling Dispose() on the rw object, so the destructor call is suppressed.
However, you have “forgotten” to call Dispose() on the rw2 object; no worries—the finalizer will still execute when the object is garbage collected.
That concludes your investigation of how the runtime manages your objects via garbage collection. While there are additional (somewhat esoteric) details regarding the collection process I haven’t covered here (such as weak references and object resurrection), you are now in a perfect position for further

exploration on your own. To wrap up this chapter, you will examine a programming feature called lazy instantiation of objects.

Understanding Lazy Object Instantiation
When you are creating classes, you might occasionally need to account for a particular member variable in code, which might never actually be needed, in that the object user might not call the method (or property) that makes use of it. Fair enough. However, this can be problematic if the member variable in question requires a large amount of memory to be instantiated.
For example, assume you are writing a class that encapsulates the operations of a digital music player. In addition to the expected methods, such as Play(), Pause(), and Stop(), you also want to provide the ability to return a collection of Song objects (via a class named AllTracks), which represents every single digital music file on the device.
If you’d like to follow along, create a new Console Application project named LazyObjectInstantiation, and define the following class types:

//Song.cs
namespace LazyObjectInstantiation;
// Represents a single song.
class Song
{
public string Artist { get; set; } public string TrackName { get; set; } public double TrackLength { get; set; }
}

//AllTracks.cs
namespace LazyObjectInstantiation;
// Represents all songs on a player.
class AllTracks
{
// Our media player can have a maximum
// of 10,000 songs.
private Song[] _allSongs = new Song[10000];

public AllTracks()
{
// Assume we fill up the array
// of Song objects here. Console.WriteLine("Filling up the songs!");
}
}

//MediaPlayer.cs
namespace LazyObjectInstantiation;
// The MediaPlayer has-an AllTracks object.
class MediaPlayer
{
// Assume these methods do something useful. public void Play() { / Play a song / }

public void Pause() { / Pause the song / } public void Stop() { / Stop playback / } private AllTracks _allSongs = new AllTracks();

public AllTracks GetAllTracks()
{
// Return all of the songs. return _allSongs;
}
}

The current implementation of MediaPlayer assumes that the object user will want to obtain a list of songs via the GetAllTracks() method. Well, what if the object user does not need to obtain this list? In the current implementation, the AllTracks member variable will still be allocated, thereby creating 10,000 Song objects in memory, as follows:

using LazyObjectInstantiation;

Console.WriteLine(" Fun with Lazy Instantiation \n");

// This caller does not care about getting all songs,
// but indirectly created 10,000 objects! MediaPlayer myPlayer = new MediaPlayer(); myPlayer.Play();

Console.ReadLine();

Clearly, you would rather not create 10,000 objects that nobody will use, as that will add a good deal of stress to the .NET Core garbage collector. While you could manually add some code to ensure the _allSongs object is created only if used (perhaps using the factory method design pattern), there is an easier way.
The base class libraries provide a useful generic class named Lazy<>, defined in the System namespace of mscorlib.dll. This class allows you to define data that will not be created unless your code base actually uses it. As this is a generic class, you must specify the type of item to be created on first use, which can be any type with the .NET Core base class libraries or a custom type you have authored yourself. To enable lazy instantiation of the AllTracks member variable, you can simply update the MediaPlayer code to this:

// The MediaPlayer has-an Lazy object. class MediaPlayer
{
...
private Lazy _allSongs = new Lazy(); public AllTracks GetAllTracks()
{
// Return all of the songs. return _allSongs.Value;
}
}

Beyond the fact that you are now representing the AllTracks member variable as a Lazy<> type, notice that the implementation of the previous GetAllTracks() method has also been updated. Specifically, you

must use the read-only Value property of the Lazy<> class to obtain the actual stored data (in this case, the
AllTracks object that is maintaining the 10,000 Song objects).
With this simple update, notice how the following updated code will indirectly allocate the Song objects only if GetAllTracks() is indeed called:

Console.WriteLine(" Fun with Lazy Instantiation \n");

// No allocation of AllTracks object here! MediaPlayer myPlayer = new MediaPlayer(); myPlayer.Play();

// Allocation of AllTracks happens when you call GetAllTracks().
MediaPlayer yourPlayer = new MediaPlayer(); AllTracks yourMusic = yourPlayer.GetAllTracks();

Console.ReadLine();

■ Note Lazy object instantiation is useful not only to decrease allocation of unnecessary objects. You can also use this technique if a given member has expensive creation code, such as invoking a remote method, communicating with a relational database, etc.

Customizing the Creation of the Lazy Data
When you declare a Lazy<> variable, the actual internal data type is created using the default constructor, like so:

// Default constructor of AllTracks is called when the Lazy<>
// variable is used.
private Lazy _allSongs = new Lazy();

While this might be fine in some cases, what if the AllTracks class had some additional constructors and you want to ensure the correct one is called? Furthermore, what if you have some extra work to do (beyond simply creating the AllTracks object) when the Lazy<> variable is made? As luck would have it, the Lazy<> class allows you to specify a generic delegate as an optional parameter, which will specify a method to call during the creation of the wrapped type.
The generic delegate in question is of type System.Func<>, which can point to a method that returns the same data type being created by the related Lazy<> variable and can take up to 16 arguments (which are typed using generic type parameters). In most cases, you will not need to specify any parameters
to pass to the method pointed to by Func<>. Furthermore, to greatly simplify the use of the required Func<>, I recommend using a lambda expression (see Chapter 12 to learn or review the delegate/lambda relationship).
With this in mind, the following is a final version of MediaPlayer that adds a bit of custom code when the wrapped AllTracks object is created. Remember, this method must return a new instance of the type wrapped by Lazy<> before exiting, and you can use any constructor you choose (here, you are still invoking the default constructor of AllTracks).

class MediaPlayer

{
...
// Use a lambda expression to add additional code
// when the AllTracks object is made. private Lazy _allSongs =
new Lazy( () =>
{
Console.WriteLine("Creating AllTracks object!"); return new AllTracks();
}
);

public AllTracks GetAllTracks()
{
// Return all of the songs. return _allSongs.Value;
}
}

Sweet! I hope you can see the usefulness of the Lazy<> class. Essentially, this generic class allows you to ensure expensive objects are allocated only when the object user requires them.

Summary
The point of this chapter was to demystify the garbage collection process. As you saw, the garbage collector will run only when it is unable to acquire the necessary memory from the managed heap (or when the developer calls GC.Collect()). When a collection does occur, you can rest assured that Microsoft’s collection algorithm has been optimized by the use of object generations, secondary threads for the purpose of object finalization, and a managed heap dedicated to hosting large objects.
This chapter also illustrated how to programmatically interact with the garbage collector using the System.GC class type. As mentioned, the only time you will really need to do so is when you are building finalizable or disposable class types that operate on unmanaged resources.
Recall that finalizable types are classes that have provided a destructor (effectively overriding the Finalize() method) to clean up unmanaged resources at the time of garbage collection. Disposable objects, on the other hand, are classes (or non-ref structures) that implement the IDisposable interface, which should be called by the object user when it is finished using said objects. Finally, you learned about an official “disposal” pattern that blends both approaches.
This chapter wrapped up with a look at a generic class named Lazy<>. As you saw, you can use this class to delay the creation of an expensive (in terms of memory consumption) object until the caller actually requires it. By doing so, you can help reduce the number of objects stored on the managed heap and also ensure expensive objects are created only when actually required by the caller.

Pro C#10 CHAPTER 8 Working with Interfaces

PART IV

Advanced C# Programming

CHAPTER 8

Working with Interfaces

This chapter builds upon your current understanding of object-oriented development by examining the topic of interface-based programming. Here, you will learn how to define and implement interfaces and come to understand the benefits of building types that support multiple behaviors. Along the way, you will look at several related topics, such as obtaining interface references, implementing explicit interfaces, and constructing interface hierarchies. You will also examine several standard interfaces defined within the
.NET Core base class libraries. Also covered are the new features introduced in C# 8 regarding interfaces, including default interface methods, static members, and access modifiers. As you will see, your custom classes and structures are free to implement these predefined interfaces to support several useful behaviors, such as object cloning, object enumeration, and object sorting.

Understanding Interface Types
To begin this chapter, allow me to provide a formal definition of the interface type, which has changed with the introduction of C# 8.0. Prior to C# 8.0, an interface is nothing more than a named set of abstract members. Recall from Chapter 6 that abstract methods are pure protocol, in that they do not provide a default implementation. The specific members defined by an interface depend on the exact behavior it is
modeling. Said another way, an interface expresses a behavior that a given class or structure may choose to support. Furthermore, as you will see in this chapter, a class or structure can support as many interfaces as necessary, thereby supporting (in essence) multiple behaviors.
The default interface methods feature, introduced in C# 8.0, allows for interface methods to contain an implementation that may or may not be overridden by the implementing class. More on this later in this chapter.
As you might guess, the .NET Core base class libraries ship with numerous predefined interface types that are implemented by various classes and structures. For example, as you will see in Chapter 20, ADO.NET ships with multiple data providers that allow you to communicate with a particular database management system. Thus, under ADO.NET, you have numerous connection objects to choose from (SqlConnection, OleDbConnection, OdbcConnection, etc.). In addition, third-party database vendors (as
well as numerous open source projects) provide .NET libraries to communicate with a wide number of other databases (MySQL, Oracle, etc.), all of which contain objects implementing these interfaces.
Although each connection class has a unique name, is defined within a different namespace, and (in some cases) is bundled within a different assembly, all connection classes implement a common interface named IDbConnection.

// The IDbConnection interface defines a common
// set of members supported by all connection objects. public interface IDbConnection : IDisposable
{

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_8

315

// Methods
IDbTransaction BeginTransaction();
IDbTransaction BeginTransaction(IsolationLevel il); void ChangeDatabase(string databaseName);
void Close();
IDbCommand CreateCommand(); void Open();
// Properties
string ConnectionString { get; set;} int ConnectionTimeout { get; } string Database { get; } ConnectionState State { get; }
}

■Note By convention, .NET interface names are prefixed with a capital letter I. When you are creating your own custom interfaces, it is considered a best practice to do the same.

Do not concern yourself with the details of what these members do at this point. Simply understand that the IDbConnection interface defines a set of members that are common to all ADO.NET connection classes. Given this, you are guaranteed that every connection object supports members such as Open(), Close(), CreateCommand(), and so forth. Furthermore, given that interface members are always abstract, each connection object is free to implement these methods in its own unique manner.
As you work through the remainder of this book, you will be exposed to dozens of interfaces that ship with the .NET Core base class libraries. As you will see, these interfaces can be implemented on your own custom classes and structures to define types that integrate tightly within the framework. As well, once you understand the usefulness of the interface type, you will certainly find reasons to build your own.

Interface Types vs. Abstract Base Classes
Given your work in Chapter 6, the interface type might seem somewhat like an abstract base class. Recall that when a class is marked as abstract, it may define any number of abstract members to provide a polymorphic interface to all derived types. However, even when a class does define a set of abstract members, it is also free to define any number of constructors, field data, nonabstract members (with
implementation), and so on. Interfaces (prior to C# 8.0) contain only member definitions. Now, with C# 8, interfaces can contain member definitions (like abstract members), members with default implementations (like virtual methods), and static members. There are only two real differences: interfaces cannot have nonstatic constructors, and a class can implement multiple interfaces. More on this second point next.
The polymorphic interface established by an abstract parent class suffers from one major limitation, in that only derived types support the members defined by the abstract parent. However, in larger software systems, it is common to develop multiple class hierarchies that have no common parent beyond System.
Object. Given that abstract members in an abstract base class apply only to derived types, you have no way
to configure types in different hierarchies to support the same polymorphic interface. To begin, create a new Console Application project named CustomInterfaces. Add the following abstract class to the project:

namespace CustomInterfaces;
public abstract class CloneableType
{
// Only derived types can support this
// "polymorphic interface." Classes in other

// hierarchies have no access to this abstract
// member.
public abstract object Clone();
}

Given this definition, only members that extend CloneableType can support the Clone() method. If you create a new set of classes that do not extend this base class, you cannot gain this polymorphic interface. Also, recall that C# does not support multiple inheritance for classes. Therefore, if you wanted to create a MiniVan that “is-a” Car and “is-a” CloneableType, you are unable to do so.

// Nope! Multiple inheritance is not possible in C#
// for classes.
public class MiniVan : Car, CloneableType
{
}

As you might guess, interface types come to the rescue. After an interface has been defined, it can be implemented by any class or structure, in any hierarchy, and within any namespace or any assembly (written in any .NET Core programming language). As you can see, interfaces are highly polymorphic. Consider the standard .NET Core interface named ICloneable, defined in the System namespace. This interface defines a single method named Clone().

public interface ICloneable
{
object Clone();
}

If you examine the .NET Core base class libraries, you will find that many seemingly unrelated types (System.Array, System.Data.SqlClient.SqlConnection, System.OperatingSystem, System.String, etc.) all implement this interface. Although these types have no common parent (other than System.Object), you can treat them polymorphically via the ICloneable interface type.
To get started, clear out the Program.cs code and add the following:

using CustomInterfaces;

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

Next, add the following local function named CloneMe() to your top-level statements. This function takes an ICloneable interface parameter, which accepts any object that implements this interface. Here is the function code:

static void CloneableExample()
{
// All of these classes support the ICloneable interface. string myStr = "Hello";
OperatingSystem unixOS = new OperatingSystem(PlatformID.Unix, new Version());
// Therefore, they can all be passed into a method taking ICloneable. CloneMe(myStr);

CloneMe(unixOS);
static void CloneMe(ICloneable c)
{
// Clone whatever we get and print out the name. object theClone = c.Clone();
Console.WriteLine("Your clone is a: {0}", theClone.GetType().Name);
}
}

When you run this application, the class name of each class prints to the console via the GetType() method you inherit from System.Object. As will be explained in detail in Chapter 17, this method allows you to understand the composition of any type at runtime. In any case, the output of the previous program is shown next:

A First Look at Interfaces Your clone is a: String
Your clone is a: OperatingSystem

Another limitation of abstract base classes is that each derived type must contend with the set of abstract members and provide an implementation. To see this problem, recall the shapes hierarchy you defined in Chapter 6. Assume you defined a new abstract method in the Shape base class named GetNumberOfPoints(), which allows derived types to return the number of points required to render the shape.

namespace CustomInterfaces; abstract class Shape
{
...
// Every derived class must now support this method! public abstract byte GetNumberOfPoints();
}

Clearly, the only class that has any points in the first place is Hexagon. However, with this update, every derived class (Circle, Hexagon, and ThreeDCircle) must now provide a concrete implementation of this function, even if it makes no sense to do so. Again, the interface type provides a solution. If you define an interface that represents the behavior of “having points,” you can simply plug it into the Hexagon type, leaving Circle and ThreeDCircle untouched.

■Note The changes to interfaces in C# 8 are probably the most significant changes to an existing language feature that i can recall. as described earlier, the new interface capabilities move them significantly closer to the functionality of abstract classes, with the added ability for a class to implement multiple interfaces. My advice is to tread carefully in these waters and use common sense. Just because you can do something does not mean that you should.

Defining Custom Interfaces
Now that you better understand the overall role of interface types, let’s see an example of defining and implementing custom interfaces. Copy the Shape.cs, Hexagon.cs, Circle.cs, and ThreeDCircle.cs files from the Shapes solution you created in Chapter 6. After you have done so, rename the namespace that defines your shape-centric types to CustomInterfaces (simply to avoid having to import namespace definitions in your new project). Now, insert a new file into your project named IPointy.cs.
At a syntactic level, an interface is defined using the C# interface keyword. Unlike a class, interfaces never specify a base class (not even System.Object; however, as you will see later in this chapter, an interface can specify base interfaces). Prior to C# 8.0, the members of an interface never specify an access modifier (as all interface members are implicitly public and abstract). New in C# 8.0, private, internal, protected, and even static members can also be defined. More on this later. To get the ball rolling, here is a custom interface defined in C#:

namespace CustomInterfaces;
// This interface defines the behavior of "having points." public interface IPointy
{
// Implicitly public and abstract. byte GetNumberOfPoints();
}

Interfaces in C# 8 cannot define data fields or nonstatic constructors. Hence, the following version of
IPointy will result in various compiler errors:

// Ack! Errors abound!
public interface IPointy
{
// Error! Interfaces cannot have data fields! public int numbOfPoints;
// Error! Interfaces do not have nonstatic constructors! public IPointy() { numbOfPoints = 0;}
}

In any case, this initial IPointy interface defines a single method. Interface types are also able to define any number of property prototypes. For example, we could update the IPointy interface to use a read-write property (commented out) and a read-only property. The Points property replaces the GetNumberOfPoints() method.

// The pointy behavior as a read-only property. public interface IPointy
{
// Implicitly public and abstract.
//byte GetNumberOfPoints();

// A read-write property in an interface would look like:
//string PropName { get; set; }

// while a read-only property in an interface would be: byte Points { get; }
}

■Note interface types can also contain event (see Chapter 12) and indexer (see Chapter 11) definitions.

Interface types are quite useless on their own, as you cannot allocate interface types as you would a class or structure.

// Ack! Illegal to allocate interface types. IPointy p = new IPointy(); // Compiler error!

Interfaces do not bring much to the table until they are implemented by a class or structure. Here, IPointy is an interface that expresses the behavior of “having points.” The idea is simple: some classes in the shapes hierarchy have points (such as the Hexagon), while others (such as the Circle) do not.

Implementing an Interface
When a class (or structure) chooses to extend its functionality by supporting interfaces, it does so using a comma-delimited list in the type definition. Be aware that the direct base class must be the first item listed after the colon operator. When your class type derives directly from System.Object, you are free to simply list the interface (or interfaces) supported by the class, as the C# compiler will extend your types
from System.Object if you do not say otherwise. On a related note, given that structures always derive from System.ValueType (see Chapter 4), simply list each interface directly after the structure definition. Ponder the following examples:

// This class derives from System.Object and
// implements a single interface. public class Pencil : IPointy
{...}

// This class also derives from System.Object
// and implements a single interface. public class SwitchBlade : object, IPointy
{...}

// This class derives from a custom base class
// and implements a single interface. public class Fork : Utensil, IPointy
{...}

// This struct implicitly derives from System.ValueType and
// implements two interfaces.
public struct PitchFork : ICloneable, IPointy
{...}

Understand that implementing an interface is an all-or-nothing proposition for interface items that do not include a default implementation. The supporting type is not able to selectively choose which members it will implement. Given that the IPointy interface defines a single read-only property, this is not too much of a burden. However, if you are implementing an interface that defines ten members (such as
the IDbConnection interface shown earlier), the type is now responsible for fleshing out the details of all ten abstract members.

For this example, insert a new class type named Triangle that “is-a” Shape and supports IPointy.
Note that the implementation of the read-only Points property (implemented using the expression-bodied member syntax) simply returns the correct number of points (three).

namespace CustomInterfaces;
// New Shape derived class named Triangle. class Triangle : Shape, IPointy
{
public Triangle() { }
public Triangle(string name) : base(name) { } public override void Draw()
{
Console.WriteLine("Drawing {0} the Triangle", PetName);
}

// IPointy implementation.
//public byte Points
//{
// get { return 3; }
//}
public byte Points => 3;
}

Now, update your existing Hexagon type to also support the IPointy interface type.

namespace CustomInterfaces;
// Hexagon now implements IPointy. class Hexagon : Shape, IPointy
{
public Hexagon(){ }
public Hexagon(string name) : base(name){ } public override void Draw()
{
Console.WriteLine("Drawing {0} the Hexagon", PetName);
}

// IPointy implementation.
public byte Points => 6;
}

To sum up the story so far, the Visual Studio class diagram shown in Figure 8-1 illustrates IPointy- compatible classes using the popular “lollipop” notation. Notice again that Circle and ThreeDCircle do not implement IPointy, as this behavior makes no sense for these classes.

Figure 8-1. The shapes hierarchy, now with interfaces

■Note To display or hide interface names in the class designer, right-click the interface icon and select the Collapse or Expand option.

Invoking Interface Members at the Object Level
Now that you have some classes that support the IPointy interface, the next question is how you interact with the new functionality. The most straightforward way to interact with functionality supplied by a given interface is to invoke the members directly from the object level (provided the interface members are not implemented explicitly; you can find more details later in the section “Explicit Interface Implementation”). For example, consider the following code:

using CustomInterfaces;

Console.WriteLine(" Fun with Interfaces \n");
// Call Points property defined by IPointy. Hexagon hex = new Hexagon(); Console.WriteLine("Points: {0}", hex.Points); Console.ReadLine();

This approach works fine in this case, given that you understand the Hexagon type has implemented the interface in question and, therefore, has a Points property. Other times, however, you might not be able to determine which interfaces are supported by a given type. For example, suppose you have an array containing 50 Shape-compatible types, only some of which support IPointy. Obviously, if you attempt to invoke the Points property on a type that has not implemented IPointy, you will receive an error. So, how can you dynamically determine whether a class or structure supports the correct interface?
One way to determine at runtime whether a type supports a specific interface is to use an explicit cast. If the type does not support the requested interface, you receive an InvalidCastException. To handle this possibility gracefully, use structured exception handling as in the following example:

...
// Catch a possible InvalidCastException. Circle c = new Circle("Lisa");
IPointy itfPt = null; try
{
itfPt = (IPointy)c; Console.WriteLine(itfPt.Points);
}
catch (InvalidCastException e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine();

While you could use try/catch logic and hope for the best, it would be ideal to determine which interfaces are supported before invoking the interface members in the first place. Let’s see two ways of doing so.

Obtaining Interface References: The as Keyword
You can determine whether a given type supports an interface by using the as keyword, introduced in Chapter 6. If the object can be treated as the specified interface, you are returned a reference to the interface in question. If not, you receive a null reference. Therefore, be sure to check against a null value before proceeding.

...
// Can we treat hex2 as IPointy? Hexagon hex2 = new Hexagon("Peter"); IPointy itfPt2 = hex2 as IPointy; if(itfPt2 != null)
{
Console.WriteLine("Points: {0}", itfPt2.Points);
}
else
{
Console.WriteLine("OOPS! Not pointy...");
}
Console.ReadLine();

Notice that when you use the as keyword, you have no need to use try/catch logic; if the reference is not null, you know you are calling on a valid interface reference.

Obtaining Interface References: The is Keyword (Updated 7.0)
You may also check for an implemented interface using the is keyword (also first discussed in Chapter 6). If the object in question is not compatible with the specified interface, you are returned the value false. If you supply a variable name in the statement, the type is assigned into the variable, eliminating the need to do the type check and perform a cast. The previous example is updated here:

Console.WriteLine(" Fun with Interfaces \n");
...
if(hex2 is IPointy itfPt3)
{
Console.WriteLine("Points: {0}", itfPt3.Points);
}
else
{
Console.WriteLine("OOPS! Not pointy...");
}
Console.ReadLine();

Default Implementations (New 8.0)
As mentioned earlier, C# 8.0 added the ability for interface methods and properties to have a default implementation. Add a new interface named IRegularPointy to represent a regularly shaped polygon. The code is shown here:

namespace CustomInterfaces; interface IRegularPointy : IPointy
{
int SideLength { get; set; } int NumberOfSides { get; set; }
int Perimeter => SideLength * NumberOfSides;
}

Add a new class named Square.cs to the project, inherit the Shape base class, and implement the
IRegularPointy interface, as follows:

namespace CustomInterfaces;
class Square: Shape,IRegularPointy
{
public Square() { }
public Square(string name) : base(name) { }
//Draw comes from the Shape base class public override void Draw()
{
Console.WriteLine("Drawing a square");
}

//This comes from the IPointy interface public byte Points => 4;

//These come from the IRegularPointy interface public int SideLength { get; set; }
public int NumberOfSides { get; set; }
//Note that the Perimeter property is not implemented
}

Here we have unwittingly introduced the first “catch” of using default implementations in interfaces.
The Perimeter property, defined on the IRegularPointy interface, is not defined in the Square class, making it inaccessible from an instance of Square. To see this in action, create a new instance of a Square class and output the relevant values to the console, like this:

Console.WriteLine("\n Fun with Interfaces \n");
...
var sq = new Square("Boxy")
{NumberOfSides = 4, SideLength = 4}; sq.Draw();
//This won't compile
//Console.WriteLine($"{sq.PetName} has {sq.NumberOfSides} of length {sq.SideLength} and a perimeter of {sq.Perimeter}");

Instead, the Square instance must be explicitly cast to the IRegularPointy interface (since that is where the implementation lives), and then the Perimeter property can be accessed. Update the code to the following:

Console.WriteLine($"{sq.PetName} has {sq.NumberOfSides} of length {sq.SideLength} and a perimeter of {((IRegularPointy)sq).Perimeter}");

One option to get around this problem is to always code to the interface of a type. Change the definition of the Square instance to IRegularPointy instead of Square, like this:

IRegularPointy sq = new Square("Boxy") {NumberOfSides = 4, SideLength = 4};

The problem with this approach (in our case) is that the Draw() method and PetName property are not defined on the interface, causing compilation errors.
While this is a trivial example, it does demonstrate one of the problems with default interfaces. Before using this feature in your code, make sure you measure the implications of the calling code having to know where the implementation exists.

Static Constructors and Members (New 8.0)
Another addition to interfaces in C# 8.0 is the ability to have static constructors and members, which function the same as static members on class definitions but are defined on interfaces. Update the IRegularPointy interface with an example static property and a static constructor.

interface IRegularPointy : IPointy
{
int SideLength { get; set; } int NumberOfSides { get; set; }
int Perimeter => SideLength * NumberOfSides;

//Static members are also allowed in C# 8 static string ExampleProperty { get; set; }

static IRegularPointy() => ExampleProperty = "Foo";
}

Static constructors must be parameterless and can only access static properties and methods. To access the interface static property, add the following code to the top-level statements:

Console.WriteLine($"Example property: {IRegularPointy.ExampleProperty}"); IRegularPointy.ExampleProperty = "Updated";
Console.WriteLine($"Example property: {IRegularPointy.ExampleProperty}");

Notice how the static property must be invoked from the interface and not an instance variable.

Interfaces as Parameters
Given that interfaces are valid types, you may construct methods that take interfaces as parameters, as illustrated by the CloneMe() method earlier in this chapter. For the current example, assume you have defined another interface named IDraw3D.

namespace CustomInterfaces;
// Models the ability to render a type in stunning 3D. public interface IDraw3D
{
void Draw3D();
}

Next, assume that two of your three shapes (ThreeDCircle and Hexagon) have been configured to support this new behavior.

// Circle supports IDraw3D.
class ThreeDCircle : Circle, IDraw3D
{
...
public void Draw3D()
=> Console.WriteLine("Drawing Circle in 3D!"); }
}

// Hexagon supports IPointy and IDraw3D. class Hexagon : Shape, IPointy, IDraw3D
{
...
public void Draw3D()
=> Console.WriteLine("Drawing Hexagon in 3D!");
}

Figure 8-2 presents the updated Visual Studio class diagram.

Figure 8-2. The updated shapes hierarchy

If you now define a method taking an IDraw3D interface as a parameter, you can effectively send in any object implementing IDraw3D. If you attempt to pass in a type not supporting the necessary interface, you receive a compile-time error. Consider the following method defined within your Program.cs file:

// I'll draw anyone supporting IDraw3D. static void DrawIn3D(IDraw3D itf3d)
{
Console.WriteLine("-> Drawing IDraw3D compatible type"); itf3d.Draw3D();
}

You could now test whether an item in the Shape array supports this new interface and, if so, pass it into the DrawIn3D() method for processing.

Console.WriteLine(" Fun with Interfaces \n");
...
Shape[] myShapes = { new Hexagon(), new Circle(), new Triangle("Joe"), new Circle("JoJo") } ;
for(int i = 0; i < myShapes.Length; i++)
{
// Can I draw you in 3D?
if (myShapes[i] is IDraw3D s)
{
DrawIn3D(s);
}
}

Here is the output of the updated application. Notice that only the Hexagon object prints out in 3D, as the other members of the Shape array do not implement the IDraw3D interface.

Fun with Interfaces
...
-> Drawing IDraw3D compatible type Drawing Hexagon in 3D!

Interfaces as Return Values
Interfaces can also be used as method return values. For example, you could write a method that takes an array of Shape objects and returns a reference to the first item that supports IPointy.

// This method returns the first object in the
// array that implements IPointy.
static IPointy FindFirstPointyShape(Shape[] shapes)
{
foreach (Shape s in shapes)
{
if (s is IPointy ip)
{
return ip;
}
}
return null;
}

You could interact with this method as follows:

Console.WriteLine(" Fun with Interfaces \n");
...
// Make an array of Shapes.
Shape[] myShapes = { new Hexagon(), new Circle(),
new Triangle("Joe"), new Circle("JoJo")};

// Get first pointy item.
IPointy firstPointyItem = FindFirstPointyShape(myShapes);
// To be safe, use the null conditional operator. Console.WriteLine("The item has {0} points", firstPointyItem?.Points);

Arrays of Interface Types
Recall that the same interface can be implemented by numerous types, even if they are not within the same class hierarchy and do not have a common parent class beyond System.Object. This can yield some powerful programming constructs. For example, assume you have developed three new class types

within your current project that model kitchen utensils (via Knife and Fork classes) and another modeling gardening equipment (à la PitchFork). The relevant code for the classes is shown here, and the updated class diagram is shown in Figure 8-3:

//Fork.cs
namespace CustomInterfaces; class Fork : IPointy
{
public byte Points => 4;
}
//PitchFork.cs
namespace CustomInterfaces; class PitchFork : IPointy
{
public byte Points => 3;
}
//Knife.cs.cs
namespace CustomInterfaces; class Knife : IPointy
{
public byte Points => 1;
}

Figure 8-3. Recall that interfaces can be “plugged into” any type in any part of a class hierarchy.

If you defined the PitchFork, Fork, and Knife types, you could now define an array of IPointy- compatible objects. Given that these members all support the same interface, you can iterate through the array and treat each item as an IPointy-compatible object, regardless of the overall diversity of the class hierarchies.

...
// This array can only contain types that
// implement the IPointy interface.
IPointy[] myPointyObjects = {new Hexagon(), new Knife(), new Triangle(), new Fork(), new PitchFork()};

foreach(IPointy i in myPointyObjects)
{
Console.WriteLine("Object has {0} points.", i.Points);
}
Console.ReadLine();

Just to highlight the importance of this example, remember this: when you have an array of a given interface, the array can contain any class or structure that implements that interface.

Implementing Interfaces Using Visual Studio or Visual Studio Code
Although interface-based programming is a powerful technique, implementing interfaces may entail a healthy amount of typing. Given that interfaces are a named set of abstract members, you are required to type in the definition and implementation for each interface method on each type that supports the behavior. Therefore, if you want to support an interface that defines a total of five methods and three properties, you need to account for all eight members (or else you will receive compiler errors).
As you would hope, Visual Studio and Visual Studio Code both support various tools that make the task of implementing interfaces less burdensome. By way of a simple test, insert a final class into your current project named PointyTestClass. When you add an interface such as IPointy (or any interface for that matter) to a class type, you might have noticed that when you complete typing the interface’s name (or when you position the mouse cursor on the interface name in the code window), Visual Studio and Visual Studio Code both add a lightbulb, which can also be invoked with the Ctrl+period (.) key combination. When you click the lightbulb, you will be presented with a drop-down list that allows you to implement the interface (see Figures 8-4 and 8-5).

Figure 8-4. Implementing interfaces automatically using Visual Studio Code

Figure 8-5. Implementing interfaces automatically using Visual Studio

Notice you are presented with two options, the second of which (explicit interface implementation) will be examined in the next section. For the time being, select the first option, and you will see that Visual
Studio/Visual Studio Code has generated stub code for you to update. (Note that the default implementation throws a System.NotImplementedException, which can obviously be deleted.)

namespace CustomInterfaces; class PointyTestClass : IPointy
{
public byte Points => throw new NotImplementedException();
}

■Note Visual studio /Visual studio Code also supports extract interface refactoring, available from the Extract interface option of the Quick actions menu. This allows you to pull out a new interface definition from an existing class definition. for example, you might be halfway through writing a class when it dawns on you that you can generalize the behavior into an interface (and thereby open the possibility of alternative implementations).

Explicit Interface Implementation
As shown earlier in this chapter, a class or structure can implement any number of interfaces. Given this, there is always the possibility you might implement interfaces that contain identical members and,
therefore, have a name clash to contend with. To illustrate various manners in which you can resolve this issue, create a new Console Application project named InterfaceNameClash. Now design three interfaces that represent various locations to which an implementing type could render its output.

namespace InterfaceNameClash;
// Draw image to a form. public interface IDrawToForm
{
void Draw();
}

namespace InterfaceNameClash;
// Draw to buffer in memory. public interface IDrawToMemory

{
void Draw();
}

namespace InterfaceNameClash;
// Render to the printer. public interface IDrawToPrinter
{
void Draw();
}

Notice that each interface defines a method named Draw(), with the identical signature. If you now want to support each of these interfaces on a single class type named Octagon, the compiler will allow the following definition:

namespace InterfaceNameClash;
class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter
{
public void Draw()
{
// Shared drawing logic. Console.WriteLine("Drawing the Octagon...");
}
}

Although the code compiles cleanly, you do have a possible problem. Simply put, providing a single implementation of the Draw() method does not allow you to take unique courses of action based on which interface is obtained from an Octagon object. For example, the following code will invoke the same Draw() method, regardless of which interface you obtain:

using InterfaceNameClash;

Console.WriteLine(" Fun with Interface Name Clashes \n"); Octagon oct = new Octagon();

// Both of these invocations call the
// same Draw() method!

// Shorthand notation if you don't need
// the interface variable for later use.
((IDrawToPrinter)oct).Draw();

// Could also use the "is" keyword.
if (oct is IDrawToMemory dtm)
{
dtm.Draw();
}

Console.ReadLine();

Clearly, the sort of code required to render the image to a window is quite different from the code needed to render the image to a networked printer or a region of memory. When you implement several interfaces that have identical members, you can resolve this sort of name clash using explicit interface implementation syntax. Consider the following update to the Octagon type:

class Octagon : IDrawToForm, IDrawToMemory, IDrawToPrinter
{
// Explicitly bind Draw() implementations
// to a given interface.
void IDrawToForm.Draw()
{
Console.WriteLine("Drawing to form...");
}
void IDrawToMemory.Draw()
{
Console.WriteLine("Drawing to memory...");
}
void IDrawToPrinter.Draw()
{
Console.WriteLine("Drawing to a printer...");
}
}

As you can see, when explicitly implementing an interface member, the general pattern breaks down to this:

returnType InterfaceName.MethodName(params){}

Note that when using this syntax, you do not supply an access modifier; explicitly implemented members are automatically private. For example, the following is illegal syntax:

// Error! No access modifier! public void IDrawToForm.Draw()
{
Console.WriteLine("Drawing to form...");
}

Because explicitly implemented members are always implicitly private, these members are no longer available from the object level. In fact, if you were to apply the dot operator to an Octagon type, you would find that IntelliSense does not show you any of the Draw() members. As expected, you must use explicit casting to access the required functionality. The previous code in the top-level statements is already using explicit casting, so it works with explicit interfaces.

Console.WriteLine(" Fun with Interface Name Clashes \n"); Octagon oct = new Octagon();

// We now must use casting to access the Draw()
// members.
IDrawToForm itfForm = (IDrawToForm)oct; itfForm.Draw();

// Shorthand notation if you don't need
// the interface variable for later use.
((IDrawToPrinter)oct).Draw();

// Could also use the "is" keyword.
if (oct is IDrawToMemory dtm)
{
dtm.Draw();
}
Console.ReadLine();

While this syntax is quite helpful when you need to resolve name clashes, you can use explicit interface implementation simply to hide more “advanced” members from the object level. In this way, when the object user applies the dot operator, the user will see only a subset of the type’s overall functionality.
However, those who require the more advanced behaviors can extract the desired interface via an explicit cast.

Designing Interface Hierarchies
Interfaces can be arranged in an interface hierarchy. Like a class hierarchy, when an interface extends an existing interface, it inherits the abstract members defined by the parent (or parents). Prior to C# 8, derived interfaces never inherit true implementation. Rather, a derived interface simply extends its own definition with additional abstract members. In C# 8, derived interfaces inherit the default implementations as well as extend the definition and potentially add new default implementations.
Interface hierarchies can be useful when you want to extend the functionality of an existing interface without breaking existing code bases. To illustrate, create a new Console Application project named InterfaceHierarchy. Now, let’s design a new set of rendering-centric interfaces such that IDrawable is the root of the family tree.

namespace InterfaceHierarchy; public interface IDrawable
{
void Draw();
}

Given that IDrawable defines a basic drawing behavior, you could now create a derived interface that extends this interface with the ability to render in modified formats. Here is an example:

namespace InterfaceHierarchy;
public interface IAdvancedDraw : IDrawable
{
void DrawInBoundingBox(int top, int left, int bottom, int right); void DrawUpsideDown();
}

Given this design, if a class were to implement IAdvancedDraw, it would now be required to implement every member defined up the chain of inheritance (specifically, the Draw(), DrawInBoundingBox(), and DrawUpsideDown() methods).

namespace InterfaceHierarchy;
public class BitmapImage : IAdvancedDraw
{
public void Draw()
{
Console.WriteLine("Drawing...");
}
public void DrawInBoundingBox(int top, int left, int bottom, int right)
{
Console.WriteLine("Drawing in a box...");
}
public void DrawUpsideDown()
{
Console.WriteLine("Drawing upside down!");
}
}

Now, when you use the BitmapImage, you can invoke each method at the object level (as they are all
public), as well as extract a reference to each supported interface explicitly via casting.

using InterfaceHierarchy;
Console.WriteLine(" Simple Interface Hierarchy ");
// Call from object level.
BitmapImage myBitmap = new BitmapImage(); myBitmap.Draw(); myBitmap.DrawInBoundingBox(10, 10, 100, 150); myBitmap.DrawUpsideDown();

// Get IAdvancedDraw explicitly.
if (myBitmap is IAdvancedDraw iAdvDraw)
{
iAdvDraw.DrawUpsideDown();
}
Console.ReadLine();

Interface Hierarchies with Default Implementations (New 8.0)
When interface hierarchies also include default implementations, downstream interfaces can choose to carry the implementation from the base interface or create a new default implementation. Update the IDrawable interface to the following:

public interface IDrawable
{
void Draw();
int TimeToDraw() => 5;
}

Next, update the top-level statements to the following:

Console.WriteLine(" Simple Interface Hierarchy ");
...
if (myBitmap is IAdvancedDraw iAdvDraw)
{
iAdvDraw.DrawUpsideDown();
Console.WriteLine($"Time to draw: {iAdvDraw.TimeToDraw()}");
}
Console.ReadLine();

Not only does this code compile, but it outputs a value of 5 for the TimeToDraw() method. This is because default implementations automatically carry forward to descendant interfaces. Casting the BitMapImage to the IAdvancedDraw interface provides access to the TimeToDraw() method, even though the BitMapImage instance does not have access to the default implementation. To prove this, enter the following code and see the compile error:

//This does not compile myBitmap.TimeToDraw();

If a downstream interface wants to provide its own default implementation, it must hide the upstream implementation. For example, if the IAdvancedDraw TimeToDraw() method takes 15 units to draw, update the interface to the following definition:

public interface IAdvancedDraw : IDrawable
{
void DrawInBoundingBox(
int top, int left, int bottom, int right); void DrawUpsideDown();
new int TimeToDraw() => 15;
}

Of course, the BitMapImage class is also free to implement the TimeToDraw() method. Unlike the
IAdvancedDraw TimeToDraw() method, the class only needs to implement the method, not hide it.

public class BitmapImage : IAdvancedDraw
{
...
public int TimeToDraw() => 12;
}

When casting the BitmapImage instance to either the IAdvancedDraw or IDrawable interface, the method on the instance still is executed. Add this code to the top-level statements:

//Always calls method on instance:
Console.WriteLine(" Calling Implemented TimeToDraw "); Console.WriteLine($"Time to draw: {myBitmap.TimeToDraw()}"); Console.WriteLine($"Time to draw: {((IDrawable) myBitmap).TimeToDraw()}"); Console.WriteLine($"Time to draw: {((IAdvancedDraw) myBitmap).TimeToDraw()}");

Here are the results:

Simple Interface Hierarchy
...
Calling Implemented TimeToDraw Time to draw: 12
Time to draw: 12 Time to draw: 12

Multiple Inheritance with Interface Types
Unlike class types, an interface can extend multiple base interfaces, allowing you to design some powerful and flexible abstractions. Create a new Console Application project named MiInterfaceHierarchy. Here is another collection of interfaces that model various rendering and shape abstractions. Notice that the IShape interface is extending both IDrawable and IPrintable.

//IDrawable.cs
namespace MiInterfaceHierarchy;
// Multiple inheritance for interface types is A-okay. interface IDrawable
{
void Draw();
}

//IPrintable.cs
namespace MiInterfaceHierarchy; interface IPrintable
{
void Print();
void Draw(); // <-- Note possible name clash here!
}

//IShape.cs
namespace MiInterfaceHierarchy;
// Multiple interface inheritance. OK! interface IShape : IDrawable, IPrintable
{
int GetNumberOfSides();
}

Figure 8-6 illustrates the current interface hierarchy.

Figure 8-6. Unlike classes, interfaces can extend multiple interface types

At this point, the million-dollar question is “If you have a class supporting IShape, how many methods will it be required to implement?” The answer: it depends. If you want to provide a simple implementation of the Draw() method, you need to provide only three members, as shown in the following Rectangle type:

namespace MiInterfaceHierarchy; class Rectangle : IShape
{
public int GetNumberOfSides() => 4;
public void Draw() => Console.WriteLine("Drawing..."); public void Print() => Console.WriteLine("Printing...");
}

If you would rather have specific implementations for each Draw() method (which in this case would make the most sense), you can resolve the name clash using explicit interface implementation, as shown in the following Square type:

namespace MiInterfaceHierarchy; class Square : IShape
{
// Using explicit implementation to handle member name clash. void IPrintable.Draw()
{
// Draw to printer ...
}
void IDrawable.Draw()
{
// Draw to screen ...
}
public void Print()
{
// Print ...
}

public int GetNumberOfSides() => 4;
}

Ideally, at this point you feel more comfortable with the process of defining and implementing custom interfaces using the C# syntax. To be honest, interface-based programming can take a while to get comfortable with, so if you are in fact still scratching your head just a bit, this is a perfectly normal reaction.
Do be aware, however, that interfaces are a fundamental aspect of the .NET Core Framework.
Regardless of the type of application you are developing (web-based, desktop GUIs, data access libraries, etc.), working with interfaces will be part of the process. To summarize the story thus far, remember that interfaces can be extremely useful in the following cases:
•You have a single hierarchy where only a subset of the derived types supports a common behavior.
•You need to model a common behavior that is found across multiple hierarchies with no common parent class beyond System.Object.
Now that you have drilled into the specifics of building and implementing custom interfaces, the remainder of this chapter examines several predefined interfaces contained within the .NET Core base class libraries. As you will see, you can implement standard .NET Core interfaces on your custom types to ensure they integrate into the framework seamlessly.

The IEnumerable and IEnumerator Interfaces
To begin examining the process of implementing existing .NET Core interfaces, let’s first look at the role of IEnumerable and IEnumerator. Recall that C# supports a keyword named foreach that allows you to iterate over the contents of any array type.

// Iterate over an array of items. int[] myArrayOfInts = {10, 20, 30, 40};

foreach(int i in myArrayOfInts)
{
Console.WriteLine(i);
}

While it might seem that only array types can use this construct, the truth of the matter is any type supporting a method named GetEnumerator() can be evaluated by the foreach construct. To illustrate, begin by creating a new Console Application project named CustomEnumerator. Next, copy the Car.cs and Radio.cs files defined in the SimpleException example of Chapter 7 into the new project. Make sure to update the namespaces for the classes to CustomEnumerator.
Now, insert a new class named Garage that stores a set of Car objects within a System.Array. using System.Collections;
namespace CustomEnumerator;
// Garage contains a set of Car objects. public class Garage
{
private Car[] carArray = new Car[4];

// Fill with some Car objects upon startup. public Garage()
{
carArray[0] = new Car("Rusty", 30);

carArray[1] = new Car("Clunker", 55); carArray[2] = new Car("Zippy", 30); carArray[3] = new Car("Fred", 30);
}
}

Ideally, it would be convenient to iterate over the Garage object’s subitems using the foreach construct, just like an array of data values. Update the Program.cs file to the following:

using System.Collections; using CustomEnumerator;

// This seems reasonable ...
Console.WriteLine(" Fun with IEnumerable / IEnumerator \n"); Garage carLot = new Garage();

// Hand over each car in the collection? foreach (Car c in carLot)
{
Console.WriteLine("{0} is going {1} MPH", c.PetName, c.CurrentSpeed);
}
Console.ReadLine();

Sadly, the compiler informs you that the Garage class does not implement a method named GetEnumerator(). This method is formalized by the IEnumerable interface, which is found lurking within the System.Collections namespace.

■Note in Chapter 10, you will learn about the role of generics and the System.Collections.Generic namespace. as you will see, this namespace contains generic versions of IEnumerable/IEnumerator that provide a more type-safe way to iterate over items.

Classes or structures that support this behavior advertise that they can expose contained items to the caller (in this example, the foreach keyword itself). Here is the definition of this standard interface:

// This interface informs the caller
// that the object's items can be enumerated. public interface IEnumerable
{
IEnumerator GetEnumerator();
}

As you can see, the GetEnumerator() method returns a reference to yet another interface named System.Collections.IEnumerator. This interface provides the infrastructure to allow the caller to traverse the internal objects contained by the IEnumerable-compatible container.

// This interface allows the caller to
// obtain a container's items. public interface IEnumerator

{
bool MoveNext (); // Advance the internal position of the cursor. object Current { get;} // Get the current item (read-only property). void Reset (); // Reset the cursor before the first member.
}

If you want to update the Garage type to support these interfaces, you could take the long road and implement each method manually. While you are certainly free to provide customized versions of
GetEnumerator(), MoveNext(), Current, and Reset(), there is a simpler way. As the System.Array type (as well as many other collection classes) already implements IEnumerable and IEnumerator, you can simply delegate the request to the System.Array as follows (note you will need to import the System.Collections namespace into your code file):

using System.Collections; namespace CustomEnumerator; public class Garage : IEnumerable
{
// System.Array already implements IEnumerator! private Car[] carArray = new Car[4];

public Garage()
{
carArray[0] = new Car("FeeFee", 200); carArray[1] = new Car("Clunker", 90); carArray[2] = new Car("Zippy", 30); carArray[3] = new Car("Fred", 30);
}

// Return the array object's IEnumerator. public IEnumerator GetEnumerator()
=> carArray.GetEnumerator();
}

After you have updated your Garage type, you can safely use the type within the C# foreach construct. Furthermore, given that the GetEnumerator() method has been defined publicly, the object user could also interact with the IEnumerator type.

// Manually work with IEnumerator.
IEnumerator carEnumerator = carLot.GetEnumerator(); carEnumerator.MoveNext();
Car myCar = (Car)i.Current;
Console.WriteLine("{0} is going {1} MPH", myCar.PetName, myCar.CurrentSpeed);

However, if you prefer to hide the functionality of IEnumerable from the object level, simply make use of explicit interface implementation.

// Return the array object's IEnumerator. IEnumerator IEnumerable.GetEnumerator()
=> return carArray.GetEnumerator();

By doing so, the casual object user will not find the Garage’s GetEnumerator() method, while the
foreach construct will obtain the interface in the background when necessary.

Building Iterator Methods with the yield Keyword
There is an alternative way to build types that work with the foreach loop via iterators. Simply put, an iterator is a member that specifies how a container’s internal items should be returned when processed by foreach. To illustrate, create a new Console Application project named CustomEnumeratorWithYield and insert the Car, Radio, and Garage types from the previous example (again, renaming your namespace definitions to the current project). Now, retrofit the current Garage type as follows:

public class Garage : IEnumerable
{
...
// Iterator method.

public IEnumerator GetEnumerator()
{
foreach (Car c in carArray)
{
yield return c;
}
}
}

Notice that this implementation of GetEnumerator() iterates over the subitems using internal foreach logic and returns each Car to the caller using the yield return syntax. The yield keyword is used to specify the value (or values) to be returned to the caller’s foreach construct. When the yield return statement is reached, the current location in the container is stored, and execution is restarted from this location the next time the iterator is called.
Iterator methods are not required to use the foreach keyword to return its contents. It is also permissible to define this iterator method as follows:

public IEnumerator GetEnumerator()
{
yield return carArray[0]; yield return carArray[1]; yield return carArray[2]; yield return carArray[3];
}

In this implementation, notice that the GetEnumerator() method is explicitly returning a new value to the caller with each pass through. Doing so for this example makes little sense, given that if you were to add more objects to the carArray member variable, your GetEnumerator() method would now be out of sync. Nevertheless, this syntax can be useful when you want to return local data from a method for processing by the foreach syntax.

Guard Clauses with Local Functions (New 7.0)
None of the code in the GetEnumerator() method is executed until the first time that the items are iterated over (or any element is accessed). That means if there is an exception prior to the yield statement, it will not get thrown when the method is first called, but only when the first MoveNext() is called.

To test this, update the GetEnumerator method to this:

public IEnumerator GetEnumerator()
{
//This will not get thrown until MoveNext() is called throw new Exception("This won't get called");
foreach (Car c in carArray)
{
yield return c;
}
}

If you were to call the function like this and do nothing else, the exception will never be thrown:

using System.Collections;
...
Console.WriteLine(" Fun with the Yield Keyword \n"); Garage carLot = new Garage();
IEnumerator carEnumerator = carLot.GetEnumerator(); Console.ReadLine();
It is not until MoveNext() is called that the code will execute, and the exception is thrown. Depending on the needs of your program, that might be perfectly fine. But it might not. Your GetEnumerator method might have a guard clause that needs to execute when the method is first called. For example, suppose that the list is gathered from a database. You might want to check that the database connection can be opened at the time the method is called, not when the list is iterated over. Or you might want to check the input parameters to the Iterator method (covered next) for validity.
Recall from Chapter 4 the C# 7 local function feature; local functions are private functions inside other functions. By moving the yield return into a local function that is returned from the main body of the method, the code in the top-level statements (before the local function is returned) is executed immediately. The local function is executed when MoveNext() is called.
Update the method to this:

public IEnumerator GetEnumerator()
{
//This will get thrown immediately
throw new Exception("This will get called"); return ActualImplementation();
//this is the local function and the actual IEnumerator implementation IEnumerator ActualImplementation()
{
foreach (Car c in carArray)
{
yield return c;
}
}
}

Test this by updating the calling code to this:

Console.WriteLine(" Fun with the Yield Keyword \n"); Garage carLot = new Garage();
try
{
//Error at this time
var carEnumerator = carLot.GetEnumerator();
}
catch (Exception e)
{
Console.WriteLine($"Exception occurred on GetEnumerator");
}
Console.ReadLine();

With the update to the GetEnumerator() method, the exception is thrown immediately instead of when
MoveNext() is called.

Building a Named Iterator
It is also interesting to note that the yield keyword can technically be used within any method, regardless of its name. These methods (which are technically called named iterators) are also unique in that they can take any number of arguments. When building a named iterator, be aware that the method will return the IEnumerable interface, rather than the expected IEnumerator-compatible type. To illustrate, you could add the following method to the Garage type (using a local function to encapsulate the iteration functionality):

public IEnumerable GetTheCars(bool returnReversed)
{
//do some error checking here return ActualImplementation();

IEnumerable ActualImplementation()
{
// Return the items in reverse. if (returnReversed)
{
for (int i = carArray.Length; i != 0; i--)
{
yield return carArray[i - 1];
}
}
else
{
// Return the items as placed in the array. foreach (Car c in carArray)
{
yield return c;
}
}
}
}

Notice that the new method allows the caller to obtain the subitems in sequential order, as well as in reverse order, if the incoming parameter has the value true. You could now interact with your new method as follows (be sure to comment out the throw new exception statement in the GetEnumerator() method):

Console.WriteLine(" Fun with the Yield Keyword \n"); Garage carLot = new Garage();

// Get items using GetEnumerator(). foreach (Car c in carLot)
{
Console.WriteLine("{0} is going {1} MPH", c.PetName, c.CurrentSpeed);
}

Console.WriteLine();

// Get items (in reverse!) using named iterator.
foreach (Car c in carLot.GetTheCars(true))
{
Console.WriteLine("{0} is going {1} MPH", c.PetName, c.CurrentSpeed);
}
Console.ReadLine();

As you might agree, named iterators are helpful constructs, in that a single custom container can define multiple ways to request the returned set.
So, to wrap up this look at building enumerable objects, remember that for your custom types to work with the C# foreach keyword, the container must define a method named GetEnumerator(), which has been formalized by the IEnumerable interface type. The implementation of this method is typically achieved by simply delegating it to the internal member that is holding onto the subobjects; however, it is also possible to use the yield return syntax to provide multiple “named iterator” methods.

The ICloneable Interface
As you might recall from Chapter 6, System.Object defines a method named MemberwiseClone(). This method is used to obtain a shallow copy of the current object. Object users do not call this method directly, as it is protected. However, a given object may call this method itself during the cloning process. To illustrate, create a new Console Application project named CloneablePoint that defines a class named Point.

namespace CloneablePoint;
// A class named Point. public class Point
{
public int X {get; set;} public int Y {get; set;}

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

// Override Object.ToString().
public override string ToString() => $"X = {X}; Y = {Y}";
}

Given what you already know about reference types and value types (see Chapter 4), you are aware that if you assign one reference variable to another, you have two references pointing to the same object in
memory. Thus, the following assignment operation results in two references to the same Point object on the heap; modifications using either reference affect the same object on the heap:

using CloneablePoint;
Console.WriteLine(" Fun with Object Cloning \n");
// Two references to same object! Point p1 = new Point(50, 50); Point p2 = p1;
p2.X = 0;
Console.WriteLine(p1); Console.WriteLine(p2); Console.ReadLine();

When you want to give your custom type the ability to return an identical copy of itself to the caller, you may implement the standard ICloneable interface. As shown at the start of this chapter, this type defines a single method named Clone().

public interface ICloneable
{
object Clone();
}

Obviously, the implementation of the Clone() method varies among your classes. However, the basic functionality tends to be the same: copy the values of your member variables into a new object instance of the same type and return it to the user. To illustrate, ponder the following update to the Point class:

// The Point now supports "clone-ability." public class Point : ICloneable
{
public int X { get; set; } public int Y { get; set; }

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

// Override Object.ToString().
public override string ToString() => $"X = {X}; Y = {Y}";

// Return a copy of the current object.
public object Clone() => new Point(this.X, this.Y);
}

In this way, you can create exact stand-alone copies of the Point type, as illustrated by the following code:

Console.WriteLine(" Fun with Object Cloning \n");
...
// Notice Clone() returns a plain object type.
// You must perform an explicit cast to obtain the derived type. Point p3 = new Point(100, 100);
Point p4 = (Point)p3.Clone();

// Change p4.X (which will not change p3.X). p4.X = 0;

// Print each object.
Console.WriteLine(p3);
Console.WriteLine(p4);
Console.ReadLine();

While the current implementation of Point fits the bill, you can streamline things just a bit. Because the Point type does not contain any internal reference type variables, you could simplify the implementation of the Clone() method as follows:

// Copy each field of the Point member by member. public object Clone() => this.MemberwiseClone();

Be aware, however, that if the Point did contain any reference type member variables, MemberwiseClone() would copy the references to those objects (i.e., a shallow copy). If you want to support a true deep copy, you will need to create a new instance of any reference type variables during the cloning process. Let’s see an example next.

A More Elaborate Cloning Example
Now assume the Point class contains a reference type member variable of type PointDescription. This class maintains a point’s friendly name as well as an identification number expressed as a System.Guid (a globally unique identifier [GUID] is a statistically unique 128-bit number). Here is the implementation:

namespace CloneablePoint;
// This class describes a point. public class PointDescription
{
public string PetName {get; set;} public Guid PointID {get; set;}

public PointDescription()
{
PetName = "No-name"; PointID = Guid.NewGuid();
}
}

The initial updates to the Point class itself included modifying ToString() to account for these new bits of state data, as well as defining and creating the PointDescription reference type. To allow the outside world to establish a pet name for the Point, you also update the arguments passed into the overloaded constructor.

public class Point : ICloneable
{
public int X { get; set; } public int Y { get; set; }
public PointDescription desc = new PointDescription();

public Point(int xPos, int yPos, string petName)
{
X = xPos; Y = yPos; desc.PetName = petName;
}
public Point(int xPos, int yPos)
{
X = xPos; Y = yPos;
}
public Point() { }

// Override Object.ToString(). public override string ToString()
=> $"X = {X}; Y = {Y}; Name = {desc.PetName};\nID = {desc.PointID}\n";

// Return a copy of the current object.
public object Clone() => this.MemberwiseClone();
}

Notice that you did not yet update your Clone() method. Therefore, when the object user asks for a clone using the current implementation, a shallow (member-by-member) copy is achieved. To illustrate, assume you have updated the calling code as follows:

Console.WriteLine(" Fun with Object Cloning \n");
...
Console.WriteLine("Cloned p3 and stored new Point in p4"); Point p3 = new Point(100, 100, "Jane");
Point p4 = (Point)p3.Clone();

Console.WriteLine("Before modification:"); Console.WriteLine("p3: {0}", p3);
Console.WriteLine("p4: {0}", p4); p4.desc.PetName = "My new Point"; p4.X = 9;

Console.WriteLine("\nChanged p4.desc.petName and p4.X"); Console.WriteLine("After modification:"); Console.WriteLine("p3: {0}", p3);
Console.WriteLine("p4: {0}", p4); Console.ReadLine();

Notice in the following output that while the value types have indeed been changed, the internal reference types maintain the same values, as they are “pointing” to the same objects in memory (specifically, note that the pet name for both objects is now “My new Point”).

Fun with Object Cloning Cloned p3 and stored new Point in p4 Before modification:
p3: X = 100; Y = 100; Name = Jane;
ID = 133d66a7-0837-4bd7-95c6-b22ab0434509

p4: X = 100; Y = 100; Name = Jane;
ID = 133d66a7-0837-4bd7-95c6-b22ab0434509

Changed p4.desc.petName and p4.X After modification:
p3: X = 100; Y = 100; Name = My new Point; ID = 133d66a7-0837-4bd7-95c6-b22ab0434509

p4: X = 9; Y = 100; Name = My new Point; ID = 133d66a7-0837-4bd7-95c6-b22ab0434509

To have your Clone() method make a complete deep copy of the internal reference types, you need to configure the object returned by MemberwiseClone() to account for the current point’s name (the System.Guid type is in fact a structure, so the numerical data is indeed copied). Here is one possible implementation:

// Now we need to adjust for the PointDescription member. public object Clone()
{
// First get a shallow copy.
Point newPoint = (Point)this.MemberwiseClone();

// Then fill in the gaps.
PointDescription currentDesc = new PointDescription(); currentDesc.PetName = this.desc.PetName;
newPoint.desc = currentDesc; return newPoint;
}

If you rerun the application once again and view the output (shown next), you see that the Point returned from Clone() does copy its internal reference type member variables (note the pet name is now unique for both p3 and p4).

Fun with Object Cloning Cloned p3 and stored new Point in p4 Before modification:
p3: X = 100; Y = 100; Name = Jane;
ID = 51f64f25-4b0e-47ac-ba35-37d263496406

p4: X = 100; Y = 100; Name = Jane;
ID = 0d3776b3-b159-490d-b022-7f3f60788e8a

Changed p4.desc.petName and p4.X After modification:
p3: X = 100; Y = 100; Name = Jane;
ID = 51f64f25-4b0e-47ac-ba35-37d263496406

p4: X = 9; Y = 100; Name = My new Point; ID = 0d3776b3-b159-490d-b022-7f3f60788e8a

To summarize the cloning process, if you have a class or structure that contains nothing but value types, implement your Clone() method using MemberwiseClone(). However, if you have a custom type that maintains other reference types, you might want to create a new object that considers each reference type member variable to get a “deep copy.”

The IComparable Interface
The System.IComparable interface specifies a behavior that allows an object to be sorted based on some specified key. Here is the formal definition:

// This interface allows an object to specify its
// relationship between other like objects. public interface IComparable
{
int CompareTo(object o);
}

■Note The generic version of this interface (IComparable) provides a more type-safe manner to handle comparisons between objects. You will examine generics in Chapter 10.

Create a new Console Application project named ComparableCar, copy the Car and Radio classes from the SimpleException example in Chapter 7, and rename the namespace for each file to ComparableCar.
Update the Car class by adding a new property to represent a unique ID for each car and a modified constructor:

using System.Collections;

namespace ComparableCar; public class Car
{
.
public int CarID {get; set;}
public Car(string name, int currSp, int id)
{

}
...
}

CurrentSpeed = currSp;
PetName = name;
CarID = id;

Now assume you have an array of Car objects as follows in your top-level statements:
global using System.Collections; using ComparableCar;
Console.WriteLine(" Fun with Object Sorting \n");

// Make an array of Car objects. Car[] myAutos = new Car[5]; myAutos[0] = new Car("Rusty", 80, 1);
myAutos[1] = new Car("Mary", 40, 234);
myAutos[2] = new Car("Viper", 40, 34);
myAutos[3] = new Car("Mel", 40, 4);
myAutos[4] = new Car("Chucky", 40, 5); Console.ReadLine();
The System.Array class defines a static method named Sort(). When you invoke this method on an array of intrinsic types (int, short, string, etc.), you can sort the items in the array in numeric/alphabetic order, as these intrinsic data types implement IComparable. However, what if you were to send an array of Car types into the Sort() method as follows?

// Sort my cars? Not yet! Array.Sort(myAutos);

If you run this test, you would get a runtime exception, as the Car class does not support the necessary interface. When you build custom types, you can implement IComparable to allow arrays of your types to be sorted. When you flesh out the details of CompareTo(), it will be up to you to decide what the baseline of the ordering operation will be. For the Car type, the internal CarID seems to be the logical candidate.

// The iteration of the Car can be ordered
// based on the CarID.
public class Car : IComparable
{
...
// IComparable implementation.
int IComparable.CompareTo(object obj)
{
if (obj is Car temp)
{
if (this.CarID > temp.CarID)
{
return 1;
}
if (this.CarID < temp.CarID)
{
return -1;
}
return 0;
}
throw new ArgumentException("Parameter is not a Car!");
}
}

As you can see, the logic behind CompareTo() is to test the incoming object against the current instance based on a specific point of data. The return value of CompareTo() is used to discover whether this type is less than, greater than, or equal to the object it is being compared with (see Table 8-1).

Table 8-1. CompareTo Return Values

Return Value Description
Any number less than zero This instance comes before the specified object in the sort order.
Zero This instance is equal to the specified object.
Any number greater than zero This instance comes after the specified object in the sort order.

You can streamline the previous implementation of CompareTo() given that the C# int data type (which is just a shorthand notation for System.Int32) implements IComparable. You could implement the Car’s CompareTo() as follows:

int IComparable.CompareTo(object obj)
{
if (obj is Car temp)
{
return this.CarID.CompareTo(temp.CarID);
}
throw new ArgumentException("Parameter is not a Car!");
}

In either case, so that your Car type understands how to compare itself to like objects, you can write the following user code:

// Exercise the IComparable interface.
// Make an array of Car objects.
...
// Display current array.
Console.WriteLine("Here is the unordered set of cars:"); foreach(Car c in myAutos)
{
Console.WriteLine("{0} {1}", c.CarID, c.PetName);
}

// Now, sort them using IComparable! Array.Sort(myAutos); Console.WriteLine();

// Display sorted array.
Console.WriteLine("Here is the ordered set of cars:"); foreach(Car c in myAutos)
{
Console.WriteLine("{0} {1}", c.CarID, c.PetName);
}
Console.ReadLine();

Here is the output from the previous code listing:

Fun with Object Sorting Here is the unordered set of cars:
1 Rusty
234 Mary
34 Viper
4Mel
5Chucky

Here is the ordered set of cars:
1 Rusty
4Mel
5Chucky
34 Viper
234 Mary

Specifying Multiple Sort Orders with IComparer
In this version of the Car type, you used the car’s ID as the base for the sort order. Another design might have used the pet name of the car as the basis for the sorting algorithm (to list cars alphabetically). Now, what if you wanted to build a Car that could be sorted by ID as well as by pet name? If this is the type of behavior you are interested in, you need to make friends with another standard interface named IComparer, defined within the System.Collections namespace as follows:

// A general way to compare two objects. interface IComparer
{
int Compare(object o1, object o2);
}

■Note The generic version of this interface (IComparer) provides a more type-safe manner to handle comparisons between objects. You will examine generics in Chapter 10.

Unlike the IComparable interface, IComparer is typically not implemented on the type you are trying to sort (i.e., the Car). Rather, you implement this interface on any number of helper classes, one for each sort order (pet name, car ID, etc.). Currently, the Car type already knows how to compare itself against other cars based on the internal car ID. Therefore, allowing the object user to sort an array of Car objects by pet name will require an additional helper class that implements IComparer. Here is the code:

namespace ComparableCar;
// This helper class is used to sort an array of Cars by pet name. public class PetNameComparer : IComparer
{
// Test the pet name of each object.
int IComparer.Compare(object o1, object o2)
{

if (o1 is Car t1 && o2 is Car t2)
{
return string.Compare(t1.PetName, t2.PetName, StringComparison.OrdinalIgnoreCase);
}
else
{
throw new ArgumentException("Parameter is not a Car!");
}
}
}

The object user code can use this helper class. System.Array has several overloaded Sort() methods, one that just happens to take an object implementing IComparer.

...
// Now sort by pet name. Array.Sort(myAutos, new PetNameComparer());

// Dump sorted array. Console.WriteLine("Ordering by pet name:"); foreach(Car c in myAutos)
{
Console.WriteLine("{0} {1}", c.CarID, c.PetName);
}
...

Custom Properties and Custom Sort Types
It is worth pointing out that you can use a custom static property to help the object user along when sorting your Car types by a specific data point. Assume the Car class has added a static read-only property named SortByPetName that returns an instance of an object implementing the IComparer interface (PetNameComparer, in this case; be sure to import System.Collections).

// We now support a custom property to return
// the correct IComparer interface. public class Car : IComparable
{
...
// Property to return the PetNameComparer. public static IComparer SortByPetName
=> (IComparer)new PetNameComparer();}

The object user code can now sort by pet name using a strongly associated property, rather than just “having to know” to use the stand-alone PetNameComparer class type.

// Sorting by pet name made a bit cleaner. Array.Sort(myAutos, Car.SortByPetName);

Ideally, at this point you not only understand how to define and implement your own interfaces but also understand their usefulness. To be sure, interfaces are found within every major .NET Core namespace, and you will continue working with various standard interfaces in the remainder of this book.

Summary
An interface can be defined as a named collection of abstract members. It is common to regard an interface as a behavior that may be supported by a given type. When two or more classes implement the same interface, you can treat each type the same way (interface-based polymorphism) even if the types are defined within unique class hierarchies.
C# provides the interface keyword to allow you to define a new interface. As you have seen, a type can support as many interfaces as necessary using a comma-delimited list. Furthermore, it is permissible to build interfaces that derive from multiple base interfaces.
In addition to building your custom interfaces, the .NET Core libraries define several standard (i.e., framework-supplied) interfaces. As you have seen, you are free to build custom types that implement these predefined interfaces to gain several desirable traits such as cloning, sorting, and enumerating.

Pro C#10 CHAPTER 7 Understanding Structured Exception Handling

CHAPTER 7 Understanding Structured Exception Handling

第7章 了解结构化异常处理

In this chapter, you will learn how to handle runtime anomalies in your C# code through the use of structured exception handling. Not only will you examine the C# keywords that allow you to handle such matters (try, catch, throw, finally, when), but you will also come to understand the distinction between application-level and system-level exceptions, as well as the role of the System.Exception base class.
在本章中,您将学习如何通过使用结构化异常处理来处理 C# 代码中的运行时异常。您不仅将检查允许您处理此类问题的 C# 关键字(尝试、捕获、抛出、最后、何时),而且还将了解应用程序级和系统级异常之间的区别,以及 System.Exception 基类的作用。

This discussion will lead into the topic of building custom exceptions and, finally, to a quick look at some exception-centric debugging tools of Visual Studio.
此讨论将引出生成自定义异常的主题,最后快速浏览 Visual Studio 的一些以异常为中心的调试工具。

Ode to Errors, Bugs, and Exceptions

错误、错误和异常

Despite what our (sometimes inflated) egos may tell us, no programmer is perfect. Writing software is a complex undertaking, and given this complexity, it is quite common for even the best software to ship with various problems. Sometimes the problem is caused by bad code (such as overflowing the bounds of an array). Other times, a problem is caused by bogus user input that has not been accounted for in the application’s code base (e.g., a phone number input field assigned to the value Chucky). Now, regardless of the cause of the problem, the end result is that the application does not work as expected. To help frame the upcoming discussion of structured exception handling, allow me to provide definitions for three commonly used anomaly-centric terms.
尽管我们的(有时是膨胀的)自我可能会告诉我们,但没有一个程序员是完美的。编写软件是一项复杂的工作,鉴于这种复杂性,即使是最好的软件也会遇到各种问题。有时问题是由错误代码引起的(例如溢出数组的边界)。其他时候,问题是由未在应用程序的代码库(例如,分配给值 Chucky 的电话号码输入字段)。现在,无论问题的原因是什么,最终结果都是应用程序无法按预期工作。为了帮助构建即将进行的结构化异常处理讨论,请允许我提供三个常用的以异常为中心的术语的定义。

•Bugs: These are, simply put, errors made by the programmer. For example, suppose you are programming with unmanaged C++. If you fail to delete dynamically allocated memory, resulting in a memory leak, you have a bug.
错误:简单地说,这些是程序员犯的错误。例如,假设您正在使用非托管C++进行编程。如果无法删除动态分配的内存,从而导致内存泄漏,则存在错误。
•User errors: User errors, on the other hand, are typically caused by the individual running your application, rather than by those who created it. For example, an end user who enters a malformed string into a text box could very well generate an error if you fail to handle this faulty input in your code base.
用户错误:另一方面,用户错误通常是由运行应用程序的个人引起的,而不是由创建应用程序的人引起的。例如,如果您未能在代码库中处理此错误输入,则在文本框中输入格式错误的字符串的最终用户很可能会生成错误。
•Exceptions: Exceptions are typically regarded as runtime anomalies that are difficult, if not impossible, to account for while programming your application. Possible exceptions include attempting to connect to a database that no longer exists, opening a corrupted XML file, or trying to contact a machine that is currently offline. In each of these cases, the programmer (or end user) has little control over these “exceptional” circumstances.
异常:异常通常被视为运行时异常,在对应用程序进行编程时很难(如果不是不可能的话)考虑这些异常。可能的异常包括尝试连接到不再存在的数据库、打开损坏的 XML 文件或尝试联系当前处于脱机状态的计算机。在每种情况下,程序员(或最终用户)几乎无法控制这些“特殊”情况。

Given these definitions, it should be clear that .NET structured exception handling is a technique for dealing with runtime exceptions. However, even for the bugs and user errors that have escaped your view, the runtime will often generate a corresponding exception that identifies the problem at hand. By way of a few examples, the .NET base class libraries define numerous exceptions, such as FormatException, IndexOutOfRangeException, FileNotFoundException, ArgumentOutOfRangeException, and so forth.
鉴于这些定义,应该清楚的是,.NET 结构化异常处理是一种处理运行时异常的技术。但是,即使对于已逃脱视图的 bug 和用户错误,运行时通常也会生成相应的异常来标识手头的问题。顺便说一下在几个示例中,.NET 基类库定义了大量异常,例如 FormatException、IndexOutOfRangeException、FileNotFoundException、ArgumentOutOfRangeException 等。

Within the .NET nomenclature, an exception accounts for bugs, bogus user input, and runtime errors, even though programmers may view each of these as a distinct issue. However, before I get too far ahead of myself, let’s formalize the role of structured exception handling and check out how it differs from traditional error-handling techniques.
在 .NET 命名法中,异常会考虑错误、虚假用户输入和运行时错误,即使程序员可能将其中每个错误视为一个单独的问题。但是,在我走得太远之前,让我们正式定义结构化异常处理的角色,并检查它与传统的错误处理技术有何不同。

■ Note To make the code examples used in this book as clean as possible, I will not catch every possible exception that may be thrown by a given method in the base class libraries. In your production-level projects, you should, of course, make liberal use of the techniques presented in this chapter.
注意 为了使本书中使用的代码示例尽可能干净,我不会捕获基类库中给定方法可能引发的所有可能的异常。当然,在您的生产级项目中,您应该自由使用本章中介绍的技术。

The Role of .NET Exception Handling

.NET 异常处理的作用

Prior to .NET, error handling under the Windows operating system was a confused mishmash of techniques. Many programmers rolled their own error-handling logic within the context of a given application. For example, a development team could define a set of numerical constants that represented known error conditions and make use of them as method return values. By way of an example, consider the following partial C code:
在.NET之前,Windows操作系统下的错误处理是技术混乱的大杂烩。许多程序员在给定应用程序的上下文中引入了自己的错误处理逻辑。例如,开发团队可以定义一组表示已知错误条件的数值常量,并将其用作方法返回值。作为示例,请考虑以下部分 C 代码:

/ A very C-style error trapping mechanism. / #define E_FILENOTFOUND 1000

int UseFileSystem()
{
// Assume something happens in this function
// that causes the following return value. return E_FILENOTFOUND;
}

void main()
{
int retVal = UseFileSystem(); if(retVal == E_FILENOTFOUND)
printf("Cannot find file...");
}

This approach is less than ideal, given that the constant EFILENOTFOUND is little more than a numerical value and is far from being a helpful agent regarding how to deal with the problem. Ideally, you would like to wrap the error’s name, a descriptive message, and other helpful information about this error condition into a single, well-defined package (which is exactly what happens under structured exception handling). In addition to a developer’s ad hoc techniques, the Windows API defines hundreds of error codes that come by way of #defines, HRESULTs, and far too many variations on the simple Boolean (bool, BOOL, VARIANT BOOL, etc.).
这种方法不太理想,因为常数EFILENOTFOUND只不过是一个数值,远不能成为如何处理问题的有用代理。理想情况下,您希望将错误的名称、描述性消息和有关此错误条件的其他有用信息包装到单个定义明确的包中(这正是结构化异常处理下发生的情况)。除了开发人员的临时技术之外,Windows API 还定义了数百个错误代码,这些代码来自 #defines、HRESULTs 以及简单布尔值(bool、BOOL、VARIANT BOOL 等)的太多变体。

The obvious problem with these older techniques is the tremendous lack of symmetry. Each approach is more or less tailored to a given technology, a given language, and perhaps even a given project. To
put an end to this madness, the .NET platform provides a standard technique to send and trap runtime errors: structured exception handling. The beauty of this approach is that developers now have a unified approach to error handling, which is common to all languages targeting the .NET platform. Therefore, the way in which a C# programmer handles errors is syntactically similar to that of a VB programmer, or a C++ programmer using C++/CLI.
这些旧技术的明显问题是严重缺乏对称性。每种方法都或多或少地针对给定的技术、给定的语言,甚至可能是给定的项目量身定制的。自为了结束这种疯狂,.NET 平台提供了一种发送和捕获运行时错误的标准技术:结构化异常处理。这种方法的优点在于,开发人员现在具有统一的错误处理方法,这对于面向 .NET 平台的所有语言都是通用的。因此,C# 程序员处理错误的方式在语法上类似于使用 C++/CLI 的 VB 程序员或C++程序员。

As an added bonus, the syntax used to throw and catch exceptions across assemblies and machine boundaries is identical. For example, if you use C# to build a ASP.NET Core RESTful service, you can throw a JSON fault to a remote caller, using the same keywords that allow you to throw an exception between methods in the same application.
作为额外的好处,用于跨程序集和计算机边界引发和捕获异常的语法是相同的。例如,如果使用 C# 生成 ASP.NET 核心 RESTful 服务,则可以使用允许在同一应用程序中的方法之间引发异常的相同关键字,将 JSON 错误抛给远程调用方。

Another bonus of .NET exceptions is that rather than receiving a cryptic numerical value, exceptions are objects that contain a human-readable description of the problem, as well as a detailed snapshot of the call stack that triggered the exception in the first place. Furthermore, you are able to give the end user help-link information that points the user to a URL that provides details about the error, as well as custom programmer-defined data.
作为额外的好处,用于跨程序集和计算机边界引发和捕获异常的语法是相同的。例如,如果使用 C# 生成 ASP.NET 核心 RESTful 服务,则可以使用允许在同一应用程序中的方法之间引发异常的相同关键字,将 JSON 错误抛给远程调用方。

The Building Blocks of .NET Exception Handling

.NET 异常处理的构建基块

Programming with structured exception handling involves the use of four interrelated entities.
使用结构化异常处理进行编程涉及使用四个相互关联的实体。

•A class type that represents the details of the exception
表示异常详细信息的类类型
•A member that throws an instance of the exception class to the caller under the correct circumstances
在正确情况下将异常类的实例抛给调用方的成员
•A block of code on the caller’s side that invokes the exception-prone member
调用方端调用容易发生异常的成员的代码块
•A block of code on the caller’s side that will process (or catch) the exception, should it occur
调用方端的代码块,如果发生异常,它将处理(或捕获)异常

The C# programming language offers five keywords (try, catch, throw, finally, and when) that allow you to throw and handle exceptions. The object that represents the problem at hand is a class extending System.Exception (or a descendent thereof). Given this fact, let’s check out the role of this exception-centric base class.
C# 编程语言提供了五个关键字(try、catch、throw 、finally 和 when),可用于引发和处理异常。表示手头问题的对象是一个扩展 System.Exception 的类(或其后代)。鉴于这一事实,让我们看看这个以异常为中心的基类的作用。

The System.Exception Base Class

System.Exception 基类

All exceptions ultimately derive from the System.Exception base class, which in turn derives from System. Object. Here is the main definition of this class (note that some of these members are virtual and may thus be overridden by derived classes):
所有异常最终都派生自 System.Exception 基类,而基类又派生自 System。对象。下面是此类的主要定义(请注意,其中一些成员是虚拟的,因此可能被派生类覆盖):

public class Exception : ISerializable
{
// Public constructors
public Exception(string message, Exception innerException); public Exception(string message);
public Exception();
...
// Methods
public virtual Exception GetBaseException();
public virtual void GetObjectData(SerializationInfo info, StreamingContext context);

// Properties
public virtual IDictionary Data { get; } public virtual string HelpLink { get; set; } public int HResult {get;set;}
public Exception InnerException { get; } public virtual string Message { get; } public virtual string Source { get; set; } public virtual string StackTrace { get; } public MethodBase TargetSite { get; }
}

As you can see, many of the properties defined by System.Exception are read-only in nature. This is because derived types will typically supply default values for each property. For example, the default message of the IndexOutOfRangeException type is “Index was outside the bounds of the array.”
如您所见,System.Exception 定义的许多属性本质上都是只读的。这是因为派生类型通常会为每个属性提供默认值。例如,IndexOutOfRangeException 类型的默认消息是“索引超出数组的边界”。

Table 7-1 describes the most important members of System.Exception.
表 7-1 描述了 System.Exception 最重要的成员。

Table 7-1. Core Members of the System.Exception Type
表 7-1. 系统的核心成员。异常类型

System.Exception Property Meaning in Life
Data This read-only property retrieves a collection of key-value pairs (represented by an object implementing IDictionary) that provide additional, programmer-defined information about the exception. By default, this collection is empty.
此只读属性检索键值对的集合(由实现 IDictionary 的对象表示),这些键值对提供有关异常的其他程序员定义的信息。默认情况下,此集合为空。
HelpLink This property gets or sets a URL to a help file or website describing the error in full detail.
此属性获取或设置指向帮助文件或网站的 URL,以完整详细描述错误。
InnerException This read-only property can be used to obtain information about the previous exceptions that caused the current exception to occur. The previous exceptions are recorded by passing them into the constructor of the most current exception.
此只读属性可用于获取有关导致当前异常发生的先前异常的信息。通过将以前的异常传递到最新异常的构造函数中来记录它们。
Message This read-only property returns the textual description of a given error. The error message itself is set as a constructor parameter.
此只读属性返回给定错误的文本说明。错误消息本身设置为构造函数参数。
Source This property gets or sets the name of the assembly, or the object, that threw the current exception.
此属性获取或设置引发当前异常的程序集或对象的名称。
StackTrace This read-only property contains a string that identifies the sequence of calls that triggered the exception. As you might guess, this property is useful during debugging or if you want to dump the error to an external error log.
此只读属性包含一个字符串,用于标识触发异常的调用序列。正如您可能猜到的那样,此属性在调试期间很有用,或者如果要将错误转储到外部错误日志中。
TargetSite This read-only property returns a MethodBase object, which describes numerous details about the method that threw the exception (invoking ToString() will identify the method by name).
此只读属性返回一个 MethodBase 对象,该对象描述了有关引发异常的方法的大量详细信息(调用 ToString() 将按名称标识该方法)。

The Simplest Possible Example

最简单的例子

To illustrate the usefulness of structured exception handling, you need to create a class that will throw an exception under the correct (or one might say exceptional) circumstances. Assume you have created a new C# Console Application project (named SimpleException) that defines two class types (Car and Radio)
为了说明结构化异常处理的有用性,您需要创建一个类,该类将在正确(或可以说是异常)情况下引发异常。假设您已经创建了一个新的 C# 控制台应用程序项目(名为 SimpleException),该项目定义了两种类类型(汽车和收音机)。)

associated by the “has-a” relationship. The Radio type defines a single method that turns the radio’s power on or off.
与“HAS-A”关系相关联。Radio 类型定义打开或关闭无线电电源的单个方法。

namespace SimpleException; class Radio
{
public void TurnOn(bool on)
{
Console.WriteLine(on ? "Jamming..." : "Quiet time...");
}
}

In addition to leveraging the Radio class via containment/delegation, the Car class (shown next) is defined in such a way that if the user accelerates a Car object beyond a predefined maximum speed (specified using a constant member variable named MaxSpeed), its engine explodes, rendering the Car unusable (captured by a private bool member variable named _carIsDead).
除了通过包含/委派利用 Radio 类之外,Car 类(如下所示)的定义方式是,如果用户将 Car 对象加速到超过预定义的最大速度(使用名为 MaxSpeed 的常量成员变量指定),其引擎将爆炸,使 Car 不可用(由名为 _carIsDead 的私有布尔成员变量捕获)。

Beyond these points, the Car type has a few properties to represent the current speed and a user- supplied “pet name,” as well as various constructors to set the state of a new Car object. Here is the complete definition (with code comments):
除了这些点之外,Car 类型还有一些属性来表示当前速度和用户提供的“宠物名称”,以及用于设置新 Car 对象状态的各种构造函数。以下是完整的定义(带有代码注释):

namespace SimpleException; class Car
{
// Constant for maximum speed. public const int MaxSpeed = 100;

// Car properties.
public int CurrentSpeed {get; set;} = 0; public string PetName {get; set;} = "";

// Is the car still operational? private bool _carIsDead;

// A car has-a radio.
private readonly Radio _theMusicBox = new Radio();

// Constructors. public Car() {}
public Car(string name, int speed)
{
CurrentSpeed = speed;
PetName = name;
}

public void CrankTunes(bool state)
{
// Delegate request to inner object.
_theMusicBox.TurnOn(state);
}

// See if Car has overheated.

public void Accelerate(int delta)
{
if (_carIsDead)
{
Console.WriteLine("{0} is out of order...", PetName);
}
else
{
CurrentSpeed += delta;
if (CurrentSpeed > MaxSpeed)
{
Console.WriteLine("{0} has overheated!", PetName); CurrentSpeed = 0;
_carIsDead = true;
}
else
{
Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed);
}
}
}
}

Next, update your Program.cs file to force a Car object to exceed the predefined maximum speed (set to 100, in the Car class), as shown here:
接下来,更新 Program.cs 文件以强制 Car 对象超过预定义的最大速度(在 Car 类中设置为 100),如下所示:

using System.Collections; using SimpleException;

Console.WriteLine(" Simple Exception Example "); Console.WriteLine("=> Creating a car and stepping on it!"); Car myCar = new Car("Zippy", 20);
myCar.CrankTunes(true);

for (int i = 0; i < 10; i++)
{
myCar.Accelerate(10);
}
Console.ReadLine();

When executing the code, you would see the following output:
执行代码时,您将看到以下输出:

Simple Exception Example
=> Creating a car and stepping on it! Jamming...
=> CurrentSpeed = 30
=> CurrentSpeed = 40
=> CurrentSpeed = 50
=> CurrentSpeed = 60

=> CurrentSpeed = 70
=> CurrentSpeed = 80
=> CurrentSpeed = 90
=> CurrentSpeed = 100 Zippy has overheated! Zippy is out of order...

Throwing a General Exception

引发常规异常

Now that you have a functional Car class, I’ll demonstrate the simplest way to throw an exception. The current implementation of Accelerate() simply displays an error message if the caller attempts to speed up the Car beyond its upper limit.
现在你有一个函数式 Car 类,我将演示引发异常的最简单方法。Accelerate() 的当前实现只是在调用方尝试将 Car 加速到超出其上限时显示错误消息。

To retrofit this method to throw an exception if the user attempts to speed up the automobile after it has met its maker, you want to create and configure a new instance of the System.Exception class, setting the value of the read-only Message property via the class constructor. When you want to send the exception object back to the caller, use the C# throw() statement. Here is the relevant code update to the
Accelerate() method:
若要改造此方法,以便在用户遇到其制造商后尝试加速汽车时引发异常,您需要创建和配置 System.Exception 类的新实例,通过类构造函数设置只读 Message 属性的值。当您要发送异常对象回到调用方,使用 C# throw() 语句。以下是对加速() 方法:

// This time, throw an exception if the user speeds up beyond MaxSpeed. public void Accelerate(int delta)
{
if (_carIsDead)
{
Console.WriteLine("{0} is out of order...", PetName);
}
else
{
CurrentSpeed += delta;
if (CurrentSpeed >= MaxSpeed)
{
CurrentSpeed = 0;
_carIsDead = true;

// Use the "throw" keyword to raise an exception. throw new Exception($"{PetName} has overheated!");
}
Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed);
}
}

Before examining how a caller would catch this exception, let’s look at a few points of interest. First, when you are throwing an exception, it is always up to you to decide exactly what constitutes the error in question and when an exception should be thrown. Here, you are making the assumption that if the program attempts to increase the speed of a Car object beyond the maximum, a System.Exception object should be thrown to indicate the Accelerate() method cannot continue (which may or may not be a valid assumption; this will be a judgment call on your part based on the application you are creating).
若要改造此方法,以便在用户遇到其制造商后尝试加速汽车时引发异常,您需要创建和配置 System.Exception 类的新实例,通过类构造函数设置只读 Message 属性的值。当您要发送异常对象回到调用方,使用 C# throw() 语句。以下是对加速() 方法:

Alternatively, you could implement Accelerate() to recover automatically without needing to throw an exception in the first place. By and large, exceptions should be thrown only when a more terminal condition has been met (e.g., not finding a necessary file, failing to connect to a database, and the like) and not used as a logic flow mechanism. Deciding exactly what justifies throwing an exception is a design issue you must always contend with. For the current purposes, assume that asking a doomed automobile to increase its speed is cause to throw an exception.
或者,您可以实现 Accelerate() 自动恢复,而无需首先抛出异常。总的来说,只有当满足更终端的条件(例如,找不到必要的文件、无法连接到数据库等)并且不使用时,才应该抛出异常。作为一种逻辑流机制。确定引发异常的确切理由是您必须始终应对的设计问题。出于当前目的,假设要求注定要失败的汽车提高速度会导致引发异常。

Second, notice how the final else was removed from the method. When an exception is thrown (either by the framework or by manually using a throw() statement), control is returned to the calling method (or by the catch block in a try catch). This eliminates the need for the final else. Whether you leave it in for readability is up to you and your coding standards.
其次,请注意最终的 else 是如何从方法中删除的。当引发异常时(由框架或手动使用 throw() 语句),控制权将返回到调用方法(或由 try catch 中的 catch 块)。这消除了对最终其他的需要。是否将其保留为可读性取决于您和您的编码标准。

In any case, if you were to rerun the application at this point using the previous logic in the top-level statements, the exception would eventually be thrown. As shown in the following output, the result of not handling this error is less than ideal, given you receive a verbose error dump followed by the program’s termination (with your specific file path and line numbers):
无论如何,如果此时要使用顶级语句中的先前逻辑重新运行应用程序,则最终将引发异常。如以下输出所示,不处理此错误的结果不太理想,因为您会收到一个详细的错误转储,然后是程序的终止(使用您的特定文件路径和行号):

Simple Exception Example
=> Creating a car and stepping on it! Jamming...
=> CurrentSpeed = 30
=> CurrentSpeed = 40
=> CurrentSpeed = 50
=> CurrentSpeed = 60
=> CurrentSpeed = 70
=> CurrentSpeed = 80
=> CurrentSpeed = 90
=> CurrentSpeed = 100

Unhandled exception. System.Exception: Zippy has overheated!
at SimpleException.Car.Accelerate(Int32 delta) in [path to file]\Car.cs:line 52
at SimpleException.Program.Main(String[] args) in [path to file]\Program.cs:line 16

Catching Exceptions

捕获异常

■ Note For those coming to C# from a Java background, understand that type members are not prototyped with the set of exceptions they may throw (in other words, .neT Core does not support checked exceptions). For better or for worse, you are not required to handle every exception thrown from a given member.
注意 对于那些从 Java 背景来到 C# 的人,请了解类型成员不是使用它们可能引发的异常集进行原型设计的(换句话说,.neT Core 不支持检查异常)。无论好坏,您都不需要处理从给定成员引发的每个异常。

Because the Accelerate() method now throws an exception, the caller needs to be ready to handle the exception, should it occur. When you are invoking a method that may throw an exception, you make use of a try/catch block. After you have caught the exception object, you are able to invoke the members of the exception object to extract the details of the problem.
由于 Accelerate() 方法现在抛出异常,因此调用方需要准备好在异常发生时处理异常。当您调用可能引发异常的方法时,您可以使用 try/catch 块。捕获异常对象后,可以调用异常对象的成员来提取问题的详细信息。

What you do with this data is largely up to you. You might want to log this information to a report file, write the data to the event log, email a system administrator, or display the problem to the end user. Here, you will simply dump the contents to the console window:
如何处理这些数据在很大程度上取决于您。您可能希望将此信息记录到报告文件、将数据写入事件日志、向系统管理员发送电子邮件或向最终用户显示问题。在这里,您只需将内容转储到控制台窗口:

// Speed up past the car's max speed to
// trigger the exception.
try

{
for(int i = 0; i < 10; i++)
{
myCar. Accelerate(10);
}
}
catch(Exception e)
{
// Handle the thrown exception. Console.WriteLine("\n Error! "); Console.WriteLine("Method: {0}", e.TargetSite);
Console.WriteLine("Message: {0}", e.Message);
Console.WriteLine("Source: {0}", e.Source);
}
// The error has been handled, processing continues with the next statement. Console.WriteLine("\n Out of exception logic "); Console.ReadLine();

In essence, a try block is a section of statements that may throw an exception during execution. If an exception is detected, the flow of program execution is sent to the appropriate catch block. On the other hand, if the code within a try block does not trigger an exception, the catch block is skipped entirely, and all is right with the world. The following output shows a test run of this program:
本质上,try 块是在执行过程中可能引发异常的语句部分。如果检测到异常,程序执行流将发送到相应的 catch 块。另一方面,如果 try 块中的代码没有触发异常,则完全跳过 catch 块,世界一切正常。以下输出显示了此程序的测试运行:

Simple Exception Example
=> Creating a car and stepping on it! Jamming...
=> CurrentSpeed = 30
=> CurrentSpeed = 40
=> CurrentSpeed = 50
=> CurrentSpeed = 60
=> CurrentSpeed = 70
=> CurrentSpeed = 80
=> CurrentSpeed = 90
=> CurrentSpeed = 100

Error!
Method: Void Accelerate(Int32) Message: Zippy has overheated! Source: SimpleException

Out of exception logic

As you can see, after an exception has been handled, the application is free to continue from the point after the catch block. In some circumstances, a given exception could be critical enough to warrant the termination of the application. However, in a good number of cases, the logic within the exception handler will ensure the application can continue on its merry way (although it could be slightly less functional, such as not being able to connect to a remote data source).
如您所见,在处理异常后,应用程序可以从 catch 块之后的点继续。在某些情况下,给定的例外可能足够严重,足以保证终止申请。但是,在很多情况下,异常处理程序中的逻辑将确保应用程序可以继续其快乐的方式(尽管它的功能可能略低,例如无法连接到远程数据源)。

Throw As Expression (New 7.0)

投掷为表达式(新 7.0)

Prior to C# 7, throw() was a statement, which meant you could throw an exception only where statements are allowed. With C# 7.0 and later, throw() is available as an expression as well and can be called anywhere expressions are allowed.
在 C# 7 之前,throw() 是一个语句,这意味着只有在允许语句的情况下才能引发异常。在 C# 7.0 及更高版本中,throw() 也可以作为表达式使用,并且可以在任何允许表达式的地方调用。

Configuring the State of an Exception

配置异常的状态

Currently, the System.Exception object configured within the Accelerate() method simply establishes a value exposed to the Message property (via a constructor parameter). As shown previously in Table 7-1, however, the Exception class also supplies a number of additional members (TargetSite, StackTrace, HelpLink, and Data) that can be useful in further qualifying the nature of the problem. To spruce up the current example, let’s examine further details of these members on a case-by-case basis.
目前,在 Accelerate() 方法中配置的 System.Exception 对象只是建立向 Message 属性公开的值(通过构造函数参数)。但是,如前面的表 7-1 所示,Exception 类还提供了许多其他成员(TargetSite、StackTrace、HelpLink 和 Data),这些成员可用于进一步限定问题的性质。为了美化当前示例,让我们逐个检查这些成员的更多详细信息。

The TargetSite Property

目标站点属性

The System.Exception.TargetSite property allows you to determine various details about the method that threw a given exception. As shown in the previous code example, printing the value of TargetSite will display the return type, name, and parameter types of the method that threw the exception. However, TargetSite does not return just a vanilla-flavored string but rather a strongly typed System.Reflection. MethodBase object. This type can be used to gather numerous details regarding the offending method, as well as the class that defines the offending method. To illustrate, assume the previous catch logic has been updated as follows:
属性允许您确定有关引发给定异常的方法的各种详细信息。如前面的代码示例所示,打印 TargetSite 的值将显示引发异常的方法的返回类型、名称和参数类型。但是,TargetSite 不仅返回香草味的字符串,还返回强类型的 System.Reflection。方法库对象。此类型可用于收集有关违规方法以及定义违规方法的类的大量详细信息。为了说明这一点,假设前面的 catch 逻辑已更新,如下所示:

...
// TargetSite actually returns a MethodBase object. catch(Exception e)
{
Console.WriteLine("\n Error! "); Console.WriteLine("Member name: {0}", e.TargetSite);
Console.WriteLine("Class defining member: {0}", e.TargetSite.DeclaringType); Console.WriteLine("Member type: {0}", e.TargetSite.MemberType); Console.WriteLine("Message: {0}", e.Message);
Console.WriteLine("Source: {0}", e.Source);
}
Console.WriteLine("\n Out of exception logic "); Console.ReadLine();

This time, you use the MethodBase.DeclaringType property to determine the fully qualified name of the class that threw the error (SimpleException.Car, in this case) as well as the MemberType property of the MethodBase object to identify the type of member (such as a property versus a method) where this exception originated. In this case, the catch logic will display the following:
这一次,使用 MethodBase.DeclaringType 属性来确定引发错误的类的完全限定名(在本例中为 SimpleException.Car),并使用 MethodBase 对象的 MemberType 属性来标识产生此异常的成员类型(如属性与方法)。在这种情况下,catch 逻辑将显示以下内容:

Error!
Member name: Void Accelerate(Int32)
Class defining member: SimpleException.Car Member type: Method
Message: Zippy has overheated! Source: SimpleException

The StackTrace Property

堆栈跟踪属性

The System.Exception.StackTrace property allows you to identify the series of calls that resulted in the exception. Be aware that you never set the value of StackTrace, as it is established automatically at the time the exception is created. To illustrate, assume you have once again updated your catch logic.
属性允许您标识导致异常的一系列调用。请注意,您永远不会设置 StackTrace 的值,因为它是在创建异常时自动建立的。为了说明这一点,假设您再次更新了捕获逻辑。

catch(Exception e)
{
...
Console.WriteLine("Stack: {0}", e.StackTrace);
}

If you were to run the program, you would find the following stack trace is printed to the console (your line numbers and file paths may differ, of course):
如果要运行该程序,您会发现以下堆栈跟踪已打印到控制台(当然,您的行号和文件路径可能会有所不同):

Stack: at SimpleException.Car.Accelerate(Int32 delta)
in [path to file]\car.cs:line 57 at $.

$(String[] args) in [path to file]\Program.cs:line 20

The string returned from StackTrace documents the sequence of calls that resulted in the throwing of this exception. Notice how the bottommost line number of this string identifies the first call in the
sequence, while the topmost line number identifies the exact location of the offending member. Clearly, this information can be quite helpful during the debugging or logging of a given application, as you are able to “follow the flow” of the error’s origin.
从 StackTrace 返回的字符串记录了导致引发此异常的调用序列。请注意此字符串最底部的行号如何标识序列,而最上面的行号标识违规成员的确切位置。显然,在调试或记录给定应用程序期间,此信息非常有用,因为您可以“跟踪错误来源的流程”。

The HelpLink Property

帮助链接属性

While the TargetSite and StackTrace properties allow programmers to gain an understanding of a given exception, this information is of little use to the end user. As you have already seen, the System.Exception. Message property can be used to obtain human-readable information that can be displayed to the current user. In addition, the HelpLink property can be set to point the user to a specific URL or standard help file that contains more detailed information.
虽然 TargetSite 和 StackTrace 属性允许程序员了解给定的异常,但此信息对最终用户几乎没有用处。正如您已经看到的,System.Exception。消息属性可用于获取可显示给当前用户的人类可读信息。此外,还可以将 HelpLink 属性设置为将用户指向包含更多详细信息的特定 URL 或标准帮助文件。

By default, the value managed by the HelpLink property is an empty string. Update the exception using object initialization to provide a more interesting value. Here are the relevant updates to the Car. Accelerate() method:
默认情况下,由帮助链接属性管理的值为空字符串。使用对象初始化更新异常以提供更有趣的值。以下是汽车的相关更新。加速() 方法:

public void Accelerate(int delta)
{
if (_carIsDead)
{
Console.WriteLine("{0} is out of order...", PetName);
}
else
{
CurrentSpeed += delta;
if (CurrentSpeed >= MaxSpeed)
{
CurrentSpeed = 0;
_carIsDead = true;

// Use the "throw" keyword to raise an exception and
// return to the caller.
throw new Exception($"{PetName} has overheated!")
{
HelpLink = "http://www.CarsRUs.com"
};
}
Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed);
}
}

The catch logic could now be updated to print this help-link information as follows:

catch(Exception e)
{
...
Console.WriteLine("Help Link: {0}", e.HelpLink);
}

The Data Property

数据属性

The Data property of System.Exception allows you to fill an exception object with relevant auxiliary information (such as a timestamp). The Data property returns an object implementing an interface named IDictionary, defined in the System.Collections namespace. Chapter 8 examines the role of interface- based programming, as well as the System.Collections namespace. For the time being, just understand that dictionary collections allow you to create a set of values that are retrieved using a specific key. Observe the next update to the Car.Accelerate() method:
System.Exception 的 Data 属性允许您使用相关的辅助信息(如时间戳)填充异常对象。Data 属性返回一个对象,该对象实现在 System.Collections 命名空间中定义的名为 IDictionary 的接口。第8章探讨了基于接口的编程以及System.Collections命名空间的作用。目前,只需了解字典集合允许您创建一组使用特定键检索的值。观察 Car.Accelerate() 方法的下一次更新:

public void Accelerate(int delta)
{
if (_carIsDead)
{
Console.WriteLine("{0} is out of order...", PetName);
}
else
{
CurrentSpeed += delta;
if (CurrentSpeed >= MaxSpeed)
{
Console.WriteLine("{0} has overheated!", PetName); CurrentSpeed = 0;
_carIsDead = true;
// Use the "throw" keyword to raise an exception
// and return to the caller.
throw new Exception($"{PetName} has overheated!")
{
HelpLink = "http://www.CarsRUs.com", Data = {
{"TimeStamp",$"The car exploded at {DateTime.Now}"},
{"Cause","You have a lead foot."}
}

};
}
Console.WriteLine("=> CurrentSpeed = {0}", CurrentSpeed);
}
}

To successfully enumerate over the key-value pairs, make sure you added a using directive for the System.Collections namespace since you will use a DictionaryEntry type in the file containing the class implementing your top-level statements.
若要成功枚举键值对,请确保为 System.Collections 命名空间添加了 using 指令,因为您将在包含实现顶级语句的类的文件中使用 DictionaryEntry 类型。

using System.Collections;

Next, you need to update the catch logic to test that the value returned from the Data property is not null (the default value). After that, you use the Key and Value properties of the DictionaryEntry type to print the custom data to the console.
接下来,需要更新 catch 逻辑以测试从 Data 属性返回的值是否不为 null(默认值)。之后,您可以使用 DictionaryEntry 类型的键和值属性将自定义数据打印到控制台。

catch (Exception e)
{
...
Console.WriteLine("\n-> Custom Data:"); foreach (DictionaryEntry de in e.Data)
{
Console.WriteLine("-> {0}: {1}", de.Key, de.Value);
}
}

With this, here’s the final output you’d see:
有了这个,这是您将看到的最终输出:

Simple Exception Example
=> Creating a car and stepping on it! Jamming...
=> CurrentSpeed = 30
=> CurrentSpeed = 40
=> CurrentSpeed = 50
=> CurrentSpeed = 60
=> CurrentSpeed = 70
=> CurrentSpeed = 80
=> CurrentSpeed = 90
Error!
Member name: Void Accelerate(Int32)
Class defining member: SimpleException.Car Member type: Method
Message: Zippy has overheated! Source: SimpleException
Stack: at SimpleException.Car.Accelerate(Int32 delta) ... at SimpleException.Program.Main(String[] args) ...
Help Link: http://www.CarsRUs.com

-> Custom Data:
-> TimeStamp: The car exploded at 3/15/2020 16:22:59
-> Cause: You have a lead foot.

Out of exception logic

The Data property is useful in that it allows you to pack in custom information regarding the error at hand, without requiring the building of a new class type to extend the Exception base class. As helpful as the Data property may be, however, it is still common for developers to build strongly typed exception classes, which handle custom data using strongly typed properties.
Data 属性非常有用,因为它允许您打包有关当前错误的自定义信息,而无需生成新的类类型来扩展 Exception 基类。但是,尽管 Data 属性可能很有帮助,但开发人员仍然很常见地生成强类型异常类,这些异常类使用强类型属性处理自定义数据。

This approach allows the caller to catch a specific exception-derived type, rather than having to dig into a data collection to obtain additional details. To understand how to do this, you need to examine the distinction between system-level and application-level exceptions.
Data 属性非常有用,因为它允许您打包有关当前错误的自定义信息,而无需生成新的类类型来扩展 Exception 基类。但是,尽管 Data 属性可能很有帮助,但开发人员仍然很常见地生成强类型异常类,这些异常类使用强类型属性处理自定义数据。

System-Level Exceptions (System.SystemException)

系统级异常(System.SystemException)

The .NET base class libraries define many classes that ultimately derive from System.Exception. For example, the System namespace defines core exception objects such as ArgumentOutOfRangeException, IndexOutOfRangeException, StackOverflowException, and so forth. Other namespaces define exceptions that reflect the behavior of that namespace. For example, System.Drawing.Printing defines printing exceptions, System.IO defines input/output-based exceptions, System.Data defines database-centric exceptions, and so forth.
.NET 基类库定义了许多最终派生自 System.Exception 的类。例如,命名空间定义核心异常对象,如 ArgumentOutOfRangeException、IndexOutOfRangeException、StackOverflowException 等。其他命名空间定义反映该命名空间行为的异常。例如,System.Drawing.Print 定义打印异常,System.IO 定义基于输入/输出的异常,System.Data 定义以数据库为中心的异常,等等。

Exceptions that are thrown by the .NET platform are (appropriately) called system exceptions. These exceptions are generally regarded as nonrecoverable, fatal errors. System exceptions derive directly from a base class named System.SystemException, which in turn derives from System.Exception (which derives from System.Object).
.NET 平台引发的异常(适当地)称为系统异常。这些异常通常被视为不可恢复的致命错误。系统异常直接派生自名为 System.SystemException 的基类,而基类又派生自 System.Exception(派生自 System.Object)。

public class SystemException : Exception
{
// Various constructors.
}

Given that the System.SystemException type does not add any functionality beyond a set of custom constructors, you might wonder why SystemException exists in the first place. Simply put, when an exception type derives from System.SystemException, you are able to determine that the .NET runtime is the entity that has thrown the exception, rather than the code base of the executing application. You can verify this quite simply using the is keyword.
鉴于 System.SystemException 类型除了一组自定义构造函数之外不会添加任何功能,您可能想知道为什么 SystemException 首先存在。简而言之,当异常类型派生自 System.SystemException 时,您可以确定 .NET 运行时是引发异常的实体,而不是正在执行的应用程序的代码库。您可以使用 is 关键字非常简单地验证这一点。

// True! NullReferenceException is-a SystemException. NullReferenceException nullRefEx = new NullReferenceException(); Console.WriteLine(
"NullReferenceException is-a SystemException? : {0}", nullRefEx is SystemException);

Application-Level Exceptions (System.ApplicationException)

应用程序级异常(System.ApplicationException)

Given that all .NET exceptions are class types, you are free to create your own application-specific exceptions. However, because the System.SystemException base class represents exceptions thrown from the runtime, you might naturally assume that you should derive your custom exceptions from the System.Exception type. You could do this, but you could instead derive from the System. ApplicationException class.
鉴于所有 .NET 异常都是类类型,您可以自由创建自己的特定于应用程序的异常。但是,由于 System.SystemException 基类表示从运行时引发的异常,因此您可能自然而然地假定应派生自定义异常从系统异常类型。您可以这样做,但您可以从系统派生。应用程序异常类。

public class ApplicationException : Exception
{
// Various constructors.
}

Like SystemException, ApplicationException does not define any additional members beyond a set of constructors. Functionally, the only purpose of System.ApplicationException is to identify the source of the error. When you handle an exception deriving from System.ApplicationException, you can assume the exception was raised by the code base of the executing application, rather than by the .NET Core base class libraries or .NET runtime engine.
与 SystemException 一样,ApplicationException 除了一组构造函数之外,不会定义任何其他成员。从功能上讲,System.ApplicationException 的唯一目的是识别错误的源。处理从 System.ApplicationException 派生的异常时,可以假定异常是由执行应用程序的代码库引发的,而不是由 .NET Core 基类库或 .NET 运行时引擎引发的。

Building Custom Exceptions, Take 1

构建自定义异常,采用 1

While you can always throw instances of System.Exception to signal a runtime error (as shown in the first example), it is sometimes advantageous to build a strongly typed exception that represents the unique details of your current problem. For example, assume you want to build a custom exception (named CarIsDeadException) to represent the error of speeding up a doomed automobile. The first step is to derive a new class from System.Exception/System.ApplicationException (by convention, all exception class names end with the Exception suffix).
虽然您始终可以抛出 System.Exception 的实例来发出运行时错误的信号(如第一个示例中所示),但有时构建一个表示当前问题的唯一详细信息的强类型异常是有利的。例如,假设您要构建一个自定义异常(名为CarIsDeadException)来表示加速注定要失败的汽车的错误。第一步是从 System.Exception/System.ApplicationException 派生一个新类(按照惯例,所有异常类名都以 Exception 后缀结尾)。

■ Note as a rule, all custom exception classes should be defined as public classes (recall that the default access modifier of a non-nested type is internal). The reason is that exceptions are often passed outside of assembly boundaries and should therefore be accessible to the calling code base.
请注意,作为一项规则,所有自定义异常类都应定义为公共类(回想一下,非嵌套类型的默认访问修饰符是内部的)。原因是异常通常在程序集边界之外传递,因此调用代码库应该可以访问。

Create a new Console Application project named CustomException, copy the previous Car.cs and Radio.cs files into your new project, and change the namespace that defines the Car and Radio types from SimpleException to CustomException. Next, add a new file named CarIsDeadException.cs and add the following class definition:
创建一个名为 CustomException 的新控制台应用程序项目,将以前的 Car.cs 和 Radio.cs 文件复制到新项目中,并将定义 Car 和 Radio 类型的命名空间从 SimpleException 更改为 CustomException。接下来,添加一个名为 CarIsDeadException 的新文件.cs并添加以下类定义:

namespace CustomException;
// This custom exception describes the details of the car-is-dead condition.
// (Remember, you can also simply extend Exception.) public class CarIsDeadException : ApplicationException
{
}

As with any class, you are free to include any number of custom members that can be called within the catch block of the calling logic. You are also free to override any virtual members defined by your parent classes. For example, you could implement CarIsDeadException by overriding the virtual Message property.
与任何类一样,您可以自由地包含任意数量的自定义成员,这些成员可以在调用逻辑的 catch 块中调用。您还可以自由覆盖父类定义的任何虚拟成员。例如,您可以通过重写虚拟消息属性来实现 CarIsDeadException。

As well, rather than populating a data dictionary (via the Data property) when throwing the exception, the constructor allows the sender to pass in a timestamp and reason for the error. Finally, the timestamp data and cause of the error can be obtained using strongly typed properties.
同样,构造函数允许发送方传入时间戳和错误原因,而不是在引发异常时填充数据字典(通过 Data 属性)。最后,可以使用强类型属性获取时间戳数据和错误原因。

public class CarIsDeadException : ApplicationException
{
private string _messageDetails = String.Empty; public DateTime ErrorTimeStamp {get; set;} public string CauseOfError {get; set;}

public CarIsDeadException(){}
public CarIsDeadException(string message, string cause, DateTime time)
{
_messageDetails = message; CauseOfError = cause; ErrorTimeStamp = time;
}

// Override the Exception.Message property. public override string Message
=> $"Car Error Message: {_messageDetails}";
}

Here, the CarIsDeadException class maintains a private field (_messageDetails) that represents data regarding the current exception, which can be set using a custom constructor. Throwing this exception from the Accelerate() method is straightforward. Simply allocate, configure, and throw a CarIsDeadException type rather than a System.Exception.
在这里,CarIsDeadException 类维护一个私有字段 (_messageDetails),该字段表示有关当前异常的数据,可以使用自定义构造函数进行设置。从 Accelerate() 方法引发此异常非常简单。只需分配、配置和抛出 CarIsDeadException 类型,而不是 System.Exception。

// Throw the custom CarIsDeadException. public void Accelerate(int delta)
{
...
throw new CarIsDeadException(
$"{PetName} has overheated!",
"You have a lead foot", DateTime.Now )
{
HelpLink = "http://www.CarsRUs.com",
};
...
}

To catch this incoming exception, your catch scope can now be updated to catch a specific CarIsDeadException type (however, given that CarIsDeadException “is-a” System.Exception, it is still permissible to catch a System.Exception as well).
要捕获此传入异常,现在可以更新捕获范围以捕获特定的CarIsDeadException类型(但是,鉴于CarIsDeadException“是-a”System.Exception,仍然允许捕获System.Exception)。

using CustomException;

Console.WriteLine(" Fun with Custom Exceptions \n"); Car myCar = new Car("Rusty", 90);

try
{
// Trip exception. myCar.Accelerate(50);
}
catch (CarIsDeadException e)

{
Console.WriteLine(e.Message); Console.WriteLine(e.ErrorTimeStamp); Console.WriteLine(e.CauseOfError);
}
Console.ReadLine();

So, now that you understand the basic process of building a custom exception, it’s time to build on that knowledge.
因此,现在您已经了解了构建自定义异常的基本过程,是时候基于这些知识进行构建了。

Building Custom Exceptions, Take 2

构建自定义异常,采用 2

The current CarIsDeadException type has overridden the virtual System.Exception.Message property to configure a custom error message and has supplied two custom properties to account for additional bits of data. In reality, however, you are not required to override the virtual Message property, as you could simply pass the incoming message to the parent’s constructor as follows:
当前的 CarIsDeadException 类型已覆盖虚拟 System.Exception.Message 属性以配置自定义错误消息,并提供了两个自定义属性来考虑其他数据位。但是,实际上,您不需要重写虚拟 Message 属性,因为您只需将传入消息传递给父级的构造函数,如下所示:

public class CarIsDeadException : ApplicationException
{
public DateTime ErrorTimeStamp { get; set; } public string CauseOfError { get; set; }

public CarIsDeadException() { }

// Feed message to parent constructor.
public CarIsDeadException(string message, string cause, DateTime time)
:base(message)
{
CauseOfError = cause;
ErrorTimeStamp = time;
}
}

Notice that this time you have not defined a string variable to represent the message and have not overridden the Message property. Rather, you are simply passing the parameter to your base class constructor. With this design, a custom exception class is little more than a uniquely named class deriving from System. ApplicationException (with additional properties if appropriate), devoid of any base class overrides.
请注意,这次您没有定义一个字符串变量来表示消息,也没有重写 Message 属性。相反,您只需将参数传递给基类构造函数。通过这种设计,自定义异常类只不过是从 System 派生的唯一命名类。ApplicationException(如果适用,具有其他属性),没有任何基类重写。

Don’t be surprised if most (if not all) of your custom exception classes follow this simple pattern. Many times, the role of a custom exception is not necessarily to provide additional functionality beyond what is inherited from the base classes but to supply a strongly named type that clearly identifies the nature of the error so the client can provide different handler logic for different types of exceptions.
如果大多数(如果不是全部)自定义异常类都遵循这种简单的模式,请不要感到惊讶。很多时候,自定义异常的作用不一定是提供从基类继承的功能之外的其他功能,而是提供明确标识错误性质的强名称类型,以便客户端可以为不同类型的异常提供不同的处理程序逻辑。

Building Custom Exceptions, Take 3

构建自定义异常,采用 3

If you want to build a truly prim-and-proper custom exception class, you want to make sure your custom exception does the following:
如果要生成一个真正原始且正确的自定义异常类,则需要确保自定义异常执行以下操作:
• Derives from Exception/ApplicationException
派生自异常/应用程序异常
• Defines a default constructor
定义默认构造函数
•Defines a constructor that sets the inherited Message property
定义设置继承的 Message 属性的构造函数
•Defines a constructor to handle “inner exceptions”
定义用于处理“内部异常”的构造函数

To complete your examination of building custom exceptions, here is the final iteration of CarIsDeadException, which accounts for each of these special constructors (the properties would be as shown in the previous example):
为了完成对生成自定义异常的检查,下面是 CarIsDeadException 的最终迭代,它考虑了这些特殊构造函数中的每一个(属性如前面的示例所示):

public class CarIsDeadException : ApplicationException
{
private string _messageDetails = String.Empty; public DateTime ErrorTimeStamp {get; set;} public string CauseOfError {get; set;}

public CarIsDeadException(){}
public CarIsDeadException(string cause, DateTime time) : this(cause,time,string.Empty)
{
}
public CarIsDeadException(string cause, DateTime time, string message) : this(cause,time,message, null)
{
}

public CarIsDeadException(string cause, DateTime time, string message, System. Exception inner)
: base(message, inner)
{
CauseOfError = cause;
ErrorTimeStamp = time;
}
}

With this update to your custom exception, update the Accelerate() method to the following:
通过对自定义异常的此更新,将 Accelerate() 方法更新为以下内容:

throw new CarIsDeadException("You have a lead foot", DateTime.Now,$"{PetName} has overheated!")
{
HelpLink = "http://www.CarsRUs.com",
};

Given that building custom exceptions that adhere to .NET Core best practices really differ by only their name, you will be happy to know that Visual Studio provides a code snippet template named Exception that will autogenerate a new exception class that adheres to .NET best practices. To activate it, type exc in the editor and hit the Tab key (in Visual Studio, hit the Tab key twice).
鉴于生成遵循 .NET Core 最佳做法的自定义异常实际上仅在名称上有所不同,您会很高兴知道 Visual Studio 提供了一个名为 Exception 的代码段模板,该模板将自动生成符合 .NET 最佳做法的新异常类。要激活它,请在编辑器中键入 exc 并按 Tab 键(在 Visual Studio 中,按 Tab 键两次)。

Multiple Exceptions

处理多个异常

In its simplest form, a try block has a single catch block. In reality, though, you often run into situations where the statements within a try block could trigger numerous possible exceptions. Create a new
C# Console Application project named ProcessMultipleExceptions; copy the Car.cs, Radio.cs, and CarIsDeadException.cs files from the previous CustomException example into the new project, and update your namespace names accordingly.
在最简单的形式中,try 块只有一个 catch 块。但是,在现实中,您经常会遇到 try 块中的语句可能会触发许多可能的异常的情况。创建一个新的名为 ProcessMultipleExceptions 的 C# 控制台应用程序项目;将 Car.cs、Radio.cs 和 CarIsDeadException.cs 文件从以前的 CustomException 示例复制到新项目中,并相应地更新命名空间名称。

Now, update Car’s Accelerate() method to also throw a predefined base class library ArgumentOutOfRangeException if you pass an invalid parameter (which you can assume is any value less than zero). Note the constructor of this exception class takes the name of the offending argument as the first string, followed by a message describing the error.
现在,更新 Car 的 Accelerate() 方法,以便在传递无效参数(可以假定该参数小于零的任何值)时也抛出预定义的基类库 ArgumentOutOfRangeException。请注意,此异常类的构造函数将有问题的参数的名称作为第一个字符串,后跟描述错误的消息。

// Test for invalid argument before proceeding. public void Accelerate(int delta)
{
if (delta < 0)
{
throw new ArgumentOutOfRangeException(nameof(delta), "Speed must be greater than zero");
}
...
}

■ Note The nameof() operator returns a string representing the name of the object, in this example the variable delta. This is a safer way to refer to C# objects, methods, and variables when the string version is required.
注意 nameof() 运算符返回一个表示对象名称的字符串,在本例中为变量 delta。当需要字符串版本时,这是引用 C# 对象、方法和变量的更安全方法。

The catch logic could now specifically respond to each type of exception.
catch 逻辑现在可以专门响应每种类型的异常。

using ProcessMultipleExceptions;

Console.WriteLine(" Handling Multiple Exceptions \n"); Car myCar = new Car("Rusty", 90);
try
{
// Trip Arg out of range exception. myCar.Accelerate(-10);
}
catch (CarIsDeadException e)
{
Console.WriteLine(e.Message);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine();

When you are authoring multiple catch blocks, you must be aware that when an exception is thrown, it will be processed by the first appropriate catch. To illustrate exactly what the “first appropriate” catch means, assume you retrofitted the previous logic with an additional catch scope that attempts to handle all exceptions beyond CarIsDeadException and ArgumentOutOfRangeException by catching a general System. Exception as follows:
创作多个 catch 块时,必须注意,当引发异常时,它将由第一个适当的 catch 处理。为了准确说明“第一个适当的”捕获的含义,假设您使用尝试处理所有捕获的附加捕获范围来改造前面的逻辑CarIsDeadException 和 ArgumentOutOfRangeException 之外的异常,通过捕获一般系统。例外情况如下:

// This code will not compile!
Console.WriteLine(" Handling Multiple Exceptions \n"); Car myCar = new Car("Rusty", 90);

try
{
// Trigger an argument out of range exception. myCar.Accelerate(-10);
}
catch(Exception e)
{
// Process all other exceptions? Console.WriteLine(e.Message);
}
catch (CarIsDeadException e)
{
Console.WriteLine(e.Message);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine();

This exception handling logic generates compile-time errors. The problem is that the first catch block can handle anything derived from System.Exception (given the “is-a” relationship), including the CarIsDeadException and ArgumentOutOfRangeException types. Therefore, the final two catch blocks are unreachable!
此异常处理逻辑生成编译时错误。问题在于,第一个 catch 块可以处理从 System.Exception 派生的任何内容(给定“is-a”关系),包括 CarIsDeadException 和 ArgumentOutOfRangeException 类型。因此,最后两个捕获块是无法访问的!

The rule of thumb to keep in mind is to make sure your catch blocks are structured such that the first catch is the most specific exception (i.e., the most derived type in an exception-type inheritance chain), leaving the final catch for the most general (i.e., the base class of a given exception inheritance chain, in this case System.Exception).
要记住的经验法则是确保你的 catch 块的结构使得第一个 catch 是最具体的异常(即异常类型继承链中派生最多的类型),将最终的 catch 留给最一般的(即,给定异常继承链的基类,在本例中为 System.Exception)。

Thus, if you want to define a catch block that will handle any errors beyond CarIsDeadException and
ArgumentOutOfRangeException, you could write the following:
因此,如果你想定义一个捕获块来处理CarIsDeadException之外的任何错误,并且ArgumentOutOfRangeException,你可以写以下内容:

// This code compiles just fine.
Console.WriteLine(" Handling Multiple Exceptions \n"); Car myCar = new Car("Rusty", 90);
try
{
// Trigger an argument out of range exception. myCar.Accelerate(-10);
}
catch (CarIsDeadException e)
{
Console.WriteLine(e.Message);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine(e.Message);
}

// This will catch any other exception
// beyond CarIsDeadException or
// ArgumentOutOfRangeException. catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine();

■ Note Where at all possible, always favor catching specific exception classes, rather than a general System.Exception. Though it might appear to make life simple in the short term (you may think “ah! This catches all the other things I don’t care about.”), in the long term you could end up with strange runtime crashes, as a more serious error was not directly dealt with in your code. remember, a final catch block that deals with System.Exception tends to be very general indeed.
注意 在可能的情况下,始终倾向于捕获特定的异常类,而不是一般的 System.Exception。虽然它可能在短期内看起来使生活变得简单(你可能会想“啊!这抓住了我不关心的所有其他事情“),从长远来看,你最终可能会遇到奇怪的运行时崩溃,因为更严重的错误没有在你的代码中直接处理。请记住,处理System.Exception的最后一个捕获块确实非常通用。

General catch Statements

一般捕获声明

C# also supports a “general” catch scope that does not explicitly receive the exception object thrown by a given member.
C# 还支持“常规”捕获范围,该范围不显式接收给定成员引发的异常对象。

// A generic catch.
Console.WriteLine(" Handling Multiple Exceptions \n"); Car myCar = new Car("Rusty", 90);
try
{
myCar.Accelerate(90);
}
catch
{
Console.WriteLine("Something bad happened...");
}
Console.ReadLine();

Obviously, this is not the most informative way to handle exceptions since you have no way to obtain meaningful data about the error that occurred (such as the method name, call stack, or custom message). Nevertheless, C# does allow for such a construct, which can be helpful when you want to handle all errors in a general fashion.
显然,这不是处理异常的最有用的方法,因为您无法获取有关所发生错误的有意义的数据(例如方法名称、调用堆栈或自定义消息)。尽管如此,C# 确实允许这样的构造,当您希望以常规方式处理所有错误时,这会很有帮助。

Rethrowing Exceptions

重新引发异常

When you catch an exception, it is permissible for the logic in a try block to rethrow the exception up the call stack to the previous caller. To do so, simply use throw() within a catch block. This passes the exception up the chain of calling logic, which can be helpful if your catch block is only able to partially handle the error at hand.
捕获异常时,允许 try 块中的逻辑将异常重新抛出到调用堆栈中到前一个调用方。为此,只需在 catch 块中使用 throw()。 这会将异常传递到调用逻辑链上,如果您的 catch 块只能部分处理手头的错误,这会很有帮助。

// Passing the buck.
...
try
{
// Speed up car logic...
}
catch(CarIsDeadException e)
{
// Do any partial processing of this error and pass the buck. throw;
}
...

Be aware that, in this example code, the ultimate receiver of CarIsDeadException is the .NET runtime because it is the top-level statements rethrowing the exception. Because of this, your end user is presented with a system-supplied error dialog box. Typically, you would only rethrow a partial handled exception to a caller that has the ability to handle the incoming exception more gracefully.
请注意,在此示例代码中,CarIsDeadException 的最终接收方是 .NET 运行时,因为它是重新引发异常的顶级语句。因此,最终用户会看到系统提供的错误对话框。通常,您只会将部分处理的异常重新引发给能够更优雅地处理传入异常的调用方。

Notice as well that you are not explicitly rethrowing the CarIsDeadException object but rather making use of throw() with no argument. You’re not creating a new exception object; you’re just rethrowing
the original exception object (with all its original information). Doing so preserves the context of the original target.
还要注意的是,你没有显式地重新抛出 CarIsDeadException 对象,而是在没有参数的情况下使用 throw()。 您没有创建新的异常对象;你只是在重新投掷原始异常对象(及其所有原始信息)。这样做可以保留原始目标的上下文。

Inner Exceptions

内部异常

As you might suspect, it is entirely possible to trigger an exception at the time you are handling another exception. For example, assume you are handling a CarIsDeadException within a particular catch scope and during the process you attempt to record the stack trace to a file on your C: drive named carErrors. txt (the implicit global using statements grant you access to the System.IO namespace and its I/O- centric types).
正如您可能怀疑的那样,完全有可能在处理另一个异常时触发异常。例如,假设您正在特定的 catch 范围内处理 CarIsDeadException,并且在此过程中尝试将堆栈跟踪记录到 C: 驱动器上名为 carErrors 的文件.txt (隐式全局 using 语句授予您访问 System.IO 命名空间及其以 I/O 为中心的类型)。

catch(CarIsDeadException e)
{
// Attempt to open a file named carErrors.txt on the C drive. FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open);
...
}

Now, if the specified file is not located on your C: drive, the call to File.Open() results in a FileNotFoundException! Later in this book, you will learn all about the System.IO namespace where you’ll discover how to programmatically determine whether a file exists on the hard drive before attempting to open the file in the first place (thereby avoiding the exception altogether). However, to stay focused on the topic of exceptions, assume the exception has been raised.
现在,如果指定的文件不在 C: 驱动器上,则调用 File.Open() 会导致 FileNotFoundException!在本书的后面部分,您将了解有关 System.IO 命名空间的所有信息,您将在其中了解如何在尝试打开文件之前以编程方式确定硬盘驱动器上是否存在文件(从而完全避免异常)。但是,为了继续关注异常主题,请假设已引发异常。

When you encounter an exception while processing another exception, best practice states that you should record the new exception object as an “inner exception” within a new object of the same type as the initial exception. (That was a mouthful!) The reason you need to allocate a new object of the exception being handled is that the only way to document an inner exception is via a constructor parameter. Consider the following code:
现在,如果指定的文件不在 C: 驱动器上,则调用 File.Open() 会导致 FileNotFoundException!在本书的后面部分,您将了解有关 System.IO 命名空间的所有信息,您将在其中了解如何在尝试打开文件之前以编程方式确定硬盘驱动器上是否存在文件(从而完全避免异常)。但是,为了继续关注异常主题,请假设已引发异常。

//Update the exception handler catch (CarIsDeadException e)
{

try
{
FileStream fs = File.Open(@"C:\carErrors.txt", FileMode.Open);
...
}
catch (Exception e2)
{
//This causes a compile error-InnerException is read only
//e.InnerException = e2;
// Throw an exception that records the new exception,
// as well as the message of the first exception.
throw new CarIsDeadException( e.CauseOfError, e.ErrorTimeStamp, e.Message, e2); }
}

Notice, in this case, I have passed in the FileNotFoundException object as the fourth parameter to the CarIsDeadException constructor. After you have configured this new object, you throw it up the call stack to the next caller, which in this case would be the top-level statements.
请注意,在本例中,我已经将FileNotFoundException对象作为第四个参数传递给CarIsDeadException构造函数。配置此新对象后,将其将调用堆栈抛出给下一个调用方,在本例中为顶级语句。

Given that there is no “next caller” after the top-level statements to catch the exception, you would be again presented with an error dialog box. Much like the act of rethrowing an exception, recording inner exceptions is usually useful only when the caller has the ability to gracefully catch the exception in the first place. If this is the case, the caller’s catch logic can use the InnerException property to extract the details of the inner exception object.
鉴于顶级语句后没有“下一个调用方”来捕获异常,您将再次看到一个错误对话框。与重新引发异常的行为非常相似,记录内部异常通常仅在调用方能够首先优雅地捕获异常时才有用。如果是这种情况,调用方的 catch 逻辑可以使用 InnerException 属性提取内部异常对象的详细信息。

The finally Block

最后的街区
A try/catch scope may also define an optional finally block. The purpose of a finally block is to ensure that a set of code statements will always execute, exception (of any type) or not. To illustrate, assume you want to always power down the car’s radio before exiting the program, regardless of any handled exception.
try/catch 范围也可以定义一个可选的 finally 块。finally 块的目的是确保一组代码语句始终执行,无论是否异常(任何类型的)。为了说明这一点,假设您希望始终在退出程序之前关闭汽车的收音机,而不管任何处理的异常。

Console.WriteLine(" Handling Multiple Exceptions \n"); Car myCar = new Car("Rusty", 90);
myCar.CrankTunes(true); try
{
// Speed up car logic.
}
catch(CarIsDeadException e)
{
// Process CarIsDeadException.
}
catch(ArgumentOutOfRangeException e)
{
// Process ArgumentOutOfRangeException.
}
catch(Exception e)
{
// Process any other Exception.
}

finally
{
// This will always occur. Exception or not. myCar.CrankTunes(false);
}
Console.ReadLine();

If you did not include a finally block, the radio would not be turned off if an exception were encountered (which might or might not be problematic). In a more real-world scenario, when you need to dispose of objects, close a file, or detach from a database (or whatever), a finally block ensures a location for proper cleanup.
如果未包含 finally 块,则在遇到异常时不会关闭无线电(可能会也可能没有问题)。在更真实的场景中,当您需要释放对象、关闭文件或从数据库分离(或其他任何内容)时,finally 块可确保正确清理的位置。

Exception Filters

异常筛选器

C# 6 introduced a new clause that can be placed on a catch scope, via the when keyword. When you add this clause, you have the ability to ensure that the statements within a catch block are executed only if some condition in your code holds true. This expression must evaluate to a Boolean (true or false) and can be obtained by using a simple code statement in the when definition itself or by calling an additional method in your code. In a nutshell, this approach allows you to add “filters” to your exception logic.
C# 6 引入了一个新子句,可以通过 when 关键字将其放置在 catch 作用域中。添加此子句时,能够确保仅当代码中的某些条件成立时,才执行 catch 块中的语句。此表达式的计算结果必须为布尔值(真或假),可以通过在 when 定义本身中使用简单的代码语句或在代码中调用其他方法来获取。简而言之,此方法允许您向异常逻辑添加“筛选器”。

Consider the following modified exception logic. I have added a when clause to the CarIsDeadException handler to ensure the catch block is never executed on a Friday (a contrived example, but who wants their automobile to break down right before the weekend?). Notice that the single Boolean statement in the when clause must be wrapped in parentheses.
请考虑以下修改后的异常逻辑。我在 CarIsDeadException 处理程序中添加了一个 when 子句,以确保 catch 块永远不会在星期五执行(一个人为的例子,但谁希望他们的汽车在周末之前发生故障?请注意,when 子句中的单个布尔语句必须括在括号中。

catch (CarIsDeadException e) when (e.ErrorTimeStamp.DayOfWeek != DayOfWeek.Friday)
{
// This new line will only print if the when clause evaluates to true.
// 仅当 when 子句的计算结果为 true 时,才会打印此新行。
Console.WriteLine("Catching car is dead!");

Console.WriteLine(e.Message);
}

While this example is very contrived, a more realistic use for using an exception filter is to catch SystemExceptions. For example, suppose your code is saving data to the database, a general exception is thrown. By examining the message and exception data, you can create specific handlers based on what caused the exception.
虽然此示例非常人为,但使用异常筛选器的更实际用途是捕获 SystemException。例如,假设您的代码正在将数据保存到数据库,则会引发常规异常。通过检查消息和异常数据,可以根据导致异常的原因创建特定的处理程序。

// Debugging Unhandled Exceptions Using Visual Studio
使用 Visual Studio 调试未经处理的异常

Visual Studio supplies a number of tools that help you debug unhandled exceptions. Assume you have increased the speed of a Car object beyond the maximum but this time did not bother to wrap your call within a try block.
Visual Studio 提供了许多工具来帮助你调试未经处理的异常。假设您已将 Car 对象的速度提高到超过最大值,但这次没有费心将您的调用包装在 try 块中。
Car myCar = new Car("Rusty", 90); myCar.Accelerate(100);

If you start a debugging session within Visual Studio (using the Debug ➤ Start Debugging menu selection), Visual Studio automatically breaks at the time the uncaught exception is thrown. Better yet, you are presented with a window (see Figure 7-1) displaying the value of the Message property.
如果在 Visual Studio 中启动调试会话(使用“调试”➤“启动调试”菜单选项),则在引发未捕获的异常时,Visual Studio 会自动中断。更好的是,您将看到一个窗口(请参阅图 7-1),其中显示了 Message 属性的值。

Alt text

Figure 7-1. Debugging unhandled custom exceptions with Visual Studio
图 7-1。 使用 Visual Studio 调试未经处理的自定义异常

■ Note If you fail to handle an exception thrown by a method in the .neT base class libraries, the Visual studio debugger breaks at the statement that called the offending method.
注意 如果无法处理 .neT 基类库中的方法引发的异常,Visual Studio 调试器将在调用违规方法的语句处中断。

If you click the View Detail link, you will find the details regarding the state of the object (see Figure 7-2).
如果单击“查看详细信息”链接,将找到有关对象状态的详细信息(请参阅图 7-2)。

Alt text

Figure 7-2. Viewing exception details
图 7-2。 查看异常详细信息

Summary

总结

In this chapter, you examined the role of structured exception handling. When a method needs to send an error object to the caller, it will allocate, configure, and throw a specific System.Exception-derived type via throw(). The caller is able to handle any possible incoming exceptions using the C# catch keyword and an optional finally scope. Since C# 6.0, the ability to create exception filters using the optional when keyword was added, and C# 7 has expanded the locations from where you can throw exceptions.
在本章中,您研究了结构化异常处理的作用。当方法需要向调用方发送错误对象时,它将通过 throw() 分配、配置和抛出特定的 System.Exception 派生类型。调用方能够使用 C# catch 关键字和可选的 finally 作用域处理任何可能的传入异常。自 C# 6.0 起,添加了使用可选 when 关键字创建异常筛选器的功能,并且 C# 7 扩展了可以引发异常的位置。

When you are creating your own custom exceptions, you ultimately create a class type deriving from System.ApplicationException, which denotes an exception thrown from the currently executing
application. In contrast, error objects deriving from System.SystemException represent critical (and fatal)
errors thrown by the .NET runtime. Last but not least, this chapter illustrated various tools within Visual Studio that can be used to create custom exceptions (according to .NET best practices) as well as debug exceptions.
在创建自己的自定义异常时,最终会创建一个派生自 System.ApplicationException 的类类型,该类类型表示从当前执行的应用。相反,派生自 System.SystemException 的错误对象表示严重(和严重).NET 运行时引发的错误。最后但并非最不重要的一点是,本章演示了Visual Studio中的各种工具,这些工具可用于创建自定义异常(根据.NET最佳实践)以及调试异常。

Pro C#10 CHAPTER 6 Understanding Inheritance and Polymorphism

CHAPTER 6 Understanding Inheritance and Polymorphism

第6章 了解继承和多态性

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

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

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

Understanding the Basic Mechanics of Inheritance

了解继承的基本机制

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

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

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

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

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

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

using BasicInheritance;

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

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

Specifying the Parent Class of an Existing Class

指定现有类的父类

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Regarding Multiple Base Classes

关于多个基类

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

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

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

Using the sealed Keyword

使用密封的关键字

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

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

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

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

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

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

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

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

Revisiting Visual Studio Class Diagrams

重新访问 Visual Studio 类图

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

Alt text

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

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

Alt text

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

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

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

Understanding the Second Pillar of OOP: The Details of Inheritance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Calling Base Class Constructors with the base Keyword

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Keeping Family Secrets: The protected Keyword

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

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

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

// Protected state data. partial class Employee

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

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

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

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

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

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

Adding a sealed Class
添加密封类

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

Alt text

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

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

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

Understanding Inheritance with Record Types (New 9.0)

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

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

Inheritance for Record Types with Standard Properties

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Inheritance for Record Types with Positional Parameters

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

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

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

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

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

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

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

Nondestructive Mutation with Inherited Record Types

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

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

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

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

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

Equality with Inherited Record Types

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

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

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

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

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

Record type inheritance!
MotorCycle and Scooter are equal: False

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

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

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

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

Deconstructor Behavior with Inherited Record Types

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

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

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

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

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

Programming for Containment/Delegation

遏制/委派编程

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

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

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

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

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

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

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

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

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

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

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

Understanding Nested Type Definitions

了解嵌套类型定义

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

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

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

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

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

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

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

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

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

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

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

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

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

public double ComputePayDeduction()
{
return 125.0;
}
}
...
}

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

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

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

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

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

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

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

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

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

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

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

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

Using the virtual and override Keywords

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

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

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

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

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

//SalesPerson.cs namespace Employees;

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

//Manager.cs namespace Employees;
class Manager : Employee
{
...
public override void GiveBonus(float amount)
{
base.GiveBonus(amount); Random r = new Random(); StockOptions += r.Next(500);
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

Overriding Virtual Members with Visual Studio/Visual Studio Code

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

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

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

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

Sealing Virtual Members (Updated 10.0)

密封虚拟成员(10.0 更新)

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

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

// SalesPerson has sealed the GiveBonus() method! class SalesPerson : Employee
{
...
public override sealed void GiveBonus(float amount)
{
...
}
}

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

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

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

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

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

Understanding Abstract Classes

了解抽象类

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

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

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

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

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

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

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

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

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

Alt text

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

Understanding the Polymorphic Interface

了解多态界面

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

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

Alt text

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

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

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

public string PetName { get; set; }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Understanding Member Shadowing

了解成员重影

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Understanding Base Class/Derived Class Casting Rules

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(ClassIWantToCastTo)referenceIHave

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

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

Using the C# as Keyword

使用 C# 作为关键字

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Discards with the is Keyword (New 7.0)

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

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

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

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

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

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

Revisiting Pattern Matching (New 7.0)

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

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

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

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

case SalesPerson s when s.SalesNumber > 5:

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

Discards with switch Statements (New 7.0)

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

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

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

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

Understanding the Super Parent Class: System.Object

了解超父类:System.Object

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Here is the output of the current code:

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

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

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

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

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

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

Overriding System.Object.ToString( )

覆盖 System.Object.ToString( )

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

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

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

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

Overriding System.Object.Equals( )

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

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

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

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

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

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

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

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

Overriding System.Object.GetHashCode()

覆盖System.Object.GetHashCode()

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

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

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

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

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

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

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

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

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

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

Testing Your Modified Person Class

测试修改后的人员类

Now that you have overridden the virtual members of Object, update the top-level statements to test your updates.
现在,您已经覆盖了 Object 的虚拟成员,请更新顶级语句以测试更新。
...
// NOTE: We want these to be identical to test
// the Equals() and GetHashCode() methods.
Person p1 = new Person("Homer", "Simpson", 50, "111-11-1111"); Person p2 = new Person("Homer", "Simpson", 50, "111-11-1111");
// Get stringified version of objects. Console.WriteLine("p1.ToString() = {0}", p1.ToString()); Console.WriteLine("p2.ToString() = {0}", p2.ToString());

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

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

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

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

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

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

Using the Static Members of System.Object

使用 System.Object 的静态成员

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

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

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

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

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

Summary

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

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

Pro C#10 CHAPTER 5 Understanding Encapsulation

PART III Object Oriented Programming with C

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

CHAPTER 5 Understanding Encapsulation

了解封装

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

Introducing the C# Class Type

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

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

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

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

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

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

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

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

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

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

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

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

Fun with Class Types

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

Allocating Objects with the new Keyword

使用新关键字分配对象

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

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

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

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

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

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

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

Understanding Constructors

了解构造函数

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

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

Understanding the Role of the Default Constructor

了解默认构造函数的角色

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

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

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

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

}
...
}

petName = "Chuck"; currSpeed = 10;

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

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

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

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

Defining Custom Constructors

定义自定义构造函数

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

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

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

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

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

}
...
}

petName = pn; currSpeed = cs;

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

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

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

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

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

Constructors As Expression-Bodied Members (New 7.0)

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

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

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

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

Constructors with out Parameters (New 7.3)

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

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

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

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

Understanding the Default Constructor Revisited

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

namespace SimpleClassExample; class Motorcycle
{
public void PopAWheely()

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

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

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

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

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

class Motorcycle
{
public int driverIntensity;

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

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

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

Understanding the Role of the this Keyword

了解此关键字的作用

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

class Motorcycle
{
public int driverIntensity;

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

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

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

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

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

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

class Motorcycle
{
public int driverIntensity; public string driverName;

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

this.driverName = name;
}
...
}

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

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

Chaining Constructor Calls Using this

以此链接构造函数调用

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

class Motorcycle
{
public int driverIntensity; public string driverName;

public Motorcycle() { }

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

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

}
...
}

driverIntensity = intensity; driverName = name;

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

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

class Motorcycle
{
public int driverIntensity; public string driverName;

// Constructors.
public Motorcycle() { }

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

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

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

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

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

class Motorcycle
{
public int driverIntensity; public string driverName;

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

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

}
...
}

driverIntensity = intensity; driverName = name;

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

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

Observing Constructor Flow

观察构造函数流

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

class Motorcycle
{
public int driverIntensity; public string driverName;

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

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

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

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

}
...
}

driverIntensity = intensity; driverName = name;

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

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

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

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

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

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

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

Revisiting Optional Arguments

重新访问可选参数

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

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

}
...
}

driverIntensity = intensity; driverName = name;

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

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

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

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

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

Understanding the static Keyword

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

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

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

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

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

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

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

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

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

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

Defining Static Field Data

定义静态字段数据

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

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

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

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

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

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

// Instance-level data. public double currBalance;

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

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

using StaticDataAndMembers;

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

Alt text

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

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

Defining Static Methods

定义静态方法

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

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

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

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

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

public static double GetInterestRate()
=> currInterestRate;
}

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

using StaticDataAndMembers;

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

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

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

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

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

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

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

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

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

Defining Static Constructors

定义静态构造函数

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

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

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

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

}
...
}

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

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

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

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

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

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

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

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

class SavingsAccount
{
public double currBalance;

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

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

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

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

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

// A static constructor!
static SavingsAccount()
{

}
...
}

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

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

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

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

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

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

Defining Static Classes

定义静态类

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

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

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

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

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

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

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

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

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

Importing Static Members via the C# using Keyword

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

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

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

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

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

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

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

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

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

Defining the Pillars of OOP

定义 OOP 的支柱

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

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

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

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

Understanding the Role of Encapsulation

了解封装的作用

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

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

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

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

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

Understanding the Role of Inheritance

了解继承的作用

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

Alt text

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

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

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

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

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

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

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

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

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

using OopExamples;

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

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

Understanding the Role of Polymorphism

了解多态性的作用

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

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

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

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

Alt text

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

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

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

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

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

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

Understanding C# Access Modifiers (Updated 7.2)

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

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

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

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

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

Using the Default Access Modifiers

使用默认访问修饰符

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

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

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

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

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

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

Using Access Modifiers and Nested Types

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

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

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

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

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

Understanding the First Pillar: C#’s Encapsulation Services

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

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

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

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

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

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

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

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

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

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

Encapsulation Using Traditional Accessors and Mutators

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

using EmployeeApp;

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

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

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

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

Console.ReadLine();

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

Encapsulation Using Properties

使用属性封装

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

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

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

}
...
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Properties As Expression-Bodied Members (New 7.0)

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

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

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

Using Properties Within a Class Definition

在类定义中使用属性

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

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

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

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

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

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

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

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

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

// Properties as before...
...
}

Read-Only Properties

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

public string SocialSecurityNumber
{
get { return _empSSN; }
}

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

public string SocialSecurityNumber => _empSSN;

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

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

ID = id;
Pay = pay;

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

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

public Employee(string name, int age, int id, float pay, string ssn)
{
...
// Check incoming ssn parameter as required and then set the value. empSSN = ssn;
}

Write-Only Properties

只写属性

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

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

Mixing Private and Public Get/Set Methods on Properties

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

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

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

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

Revisiting the static Keyword: Defining Static Properties

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

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

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

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

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

// A static property.
public static double InterestRate
{

}
...
}

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

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

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

Pattern Matching with Property Patterns (New 8.0)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Extended Property Patterns (New 10.0)

扩展属性模式(新 10.0)

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

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

Understanding Automatic Properties

了解自动属性

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

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

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

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

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

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

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

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

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

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

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

Interacting with Automatic Properties

与自动属互

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

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

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

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

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

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

Console.ReadLine();

Automatic Properties and Default Values

自动属性和默认值

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

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

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

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

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

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

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

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

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

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

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

using AutoProps;

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

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

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

Console.ReadLine();

Initializing Automatic Properties

初始化自动属性

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

class Car
{
private int numberOfDoors = 2;
}

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

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

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

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

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

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

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

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

Understanding Object Initialization

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

Looking at the Object Initialization Syntax

查看对象初始化语法

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

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

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

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

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

using ObjectInitializers;

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

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

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

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

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

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

Using init-Only Setters (New 9.0)

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

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

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

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

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

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

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

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

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

Calling Custom Constructors with Initialization Syntax

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Initializing Data with Initialization Syntax

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

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

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

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

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

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

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

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

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

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

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

Working with Constant and Read-Only Field Data

使用常量和只读字段数据

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

Understanding Constant Field Data

了解常量场数据

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

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

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

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

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

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

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

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

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

Regardless of where you define a constant piece of data, the one point to always remember is that the initial value assigned to the constant must be specified at the time you define the constant. Assigning the value of pi in a class constructor, as shown in the following code, produces a compilation error:
无论在何处定义常量数据,始终要记住的一点是,必须在定义常量时指定分配给常量的初始值。在类构造函数中分配 pi 的值(如以下代码所示)会产生编译错误:

class MyMathClass
{
// Try to set PI in constructor? public const double PI;

public MyMathClass()
{
// Not possible- must assign at time of declaration. PI = 3.14;
}
}

The reason for this restriction has to do with the fact that the value of constant data must be known at compile time. Constructors (or any other method), as you know, are invoked at runtime.
此限制的原因与常量数据的值必须在编译时已知这一事实有关。如您所知,构造函数(或任何其他方法)是在运行时调用的。

Constant Interpolated Strings (New 10.0)

常量内插字符串(新 10.0)

Introduced in C# 10, const string values can use string interpolation in their assignment statements as long as all of the components that are used are also const strings. As a trivial example, add the following code to the top-level statements:
在 C# 10 中引入的 const 字符串值可以在其赋值语句中使用字符串内插,只要使用的所有组件也是 const 字符串。作为一个简单的示例,将以下代码添加到顶级语句中:

Console.WriteLine("=> Constant String Interpolation:"); const string foo = "Foo";
const string bar = "Bar";
const string foobar = $"{foo}{bar}"; Console.WriteLine(foobar);

Understanding Read-Only Fields

了解只读字段
Closely related to constant data is the notion of read-only field data (which should not be confused with a read-only property). Like a constant, a read-only field cannot be changed after the initial assignment or you will receive a compile-time error. However, unlike a constant, the value assigned to a read-only field can be determined at runtime and, therefore, can legally be assigned within the scope of a constructor but nowhere else.
与常量数据密切相关的是只读字段数据的概念(不应将其与只读属性混淆)。与常量一样,只读字段在初始赋值后无法更改,否则您将收到编译时错误。但是,与常量不同,分配给只读字段的值可以在运行时确定,因此可以合法地在构造函数的范围内分配,但在其他地方不能。
This can be helpful when you do not know the value of a field until runtime, perhaps because you need to read an external file to obtain the value but want to ensure that the value will not change after that point. For the sake of illustration, assume the following update to MyMathClass:
当您在运行时之前不知道字段的值时,这可能会很有帮助,这可能是因为您需要读取外部文件以获取该值,但希望确保该值在该点之后不会更改。为了便于说明,假设MyMathClass进行了以下更新:

class MyMathClass
{
// Read-only fields can be assigned in constructors,
// but nowhere else. public readonly double PI;

public MyMathClass ()
{
PI = 3.14;
}
}

Again, any attempt to make assignments to a field marked readonly outside the scope of a constructor results in a compiler error.
同样,任何尝试在构造函数范围之外对标记为只读的字段进行赋值都会导致编译器错误。

class MyMathClass
{
public readonly double PI; public MyMathClass ()
{
PI = 3.14;
}

// Error!
public void ChangePI()
{ PI = 3.14444;}
}

Understanding Static Read-Only Fields

了解静态只读字段

Unlike a constant field, read-only fields are not implicitly static. Thus, if you want to expose PI from the class level, you must explicitly use the static keyword. If you know the value of a static read-only field at compile time, the initial assignment looks similar to that of a constant (however, in this case, it would be easier to simply use the const keyword in the first place, as you are assigning the data field at the time of declaration).
与常量字段不同,只读字段不是隐式静态的。因此,如果要从类级别公开 PI,则必须显式使用 static 关键字。如果您在编译时知道静态只读字段的值,则初始赋值看起来类似于常量的值(但是,在这种情况下,首先简单地使用 const 关键字会更容易,因为您在声明时分配数据字段)。

class MyMathClass
{
public static readonly double PI = 3.14;
}

//Program.cs using ConstData;

Console.WriteLine(" Fun with Const "); Console.WriteLine("The value of PI is: {0}", MyMathClass.PI); Console.ReadLine();

However, if the value of a static read-only field is not known until runtime, you must use a static constructor as described earlier in this chapter.
但是,如果静态只读字段的值在运行时之前是未知的,则必须使用本章前面所述的静态构造函数。

class MyMathClass
{
public static readonly double PI;

static MyMathClass()
{ PI = 3.14; }
}

Understanding Partial Classes

了解分部类

When working with classes, it is important to understand the role of the C# partial keyword. The partial keyword allows for a single class to be partitioned across multiple code files. When you scaffold Entity Framework Core classes from a database, the created classes are all created as partial classes. This way, any code that you have written to augment those files is not overwritten, presuming your code is in separate class files marked with the partial keyword. Another reason is that maybe your class has grown over time into something difficult to manage, and as an intermediate step toward refactoring that class, you can split it up into partials.
使用类时,了解 C# 分部关键字的作用非常重要。partial 关键字允许跨多个代码文件对单个类进行分区。从数据库搭建实体框架核心类的基架时,创建的类都将创建为分部类。这样,您为扩充这些文件而编写的任何代码都不会被覆盖,前提是您的代码位于标有 partial 关键字的单独类文件中。另一个原因是,随着时间的推移,您的类可能已经发展成为难以管理的东西,作为重构该类的中间步骤,您可以将其拆分为部分。

In C#, you can partition a single class across multiple code files to isolate the boilerplate code from more readily useful (and complex) members. To illustrate where partial classes could be useful, open the EmployeeApp project you created previously in this chapter in Visual Studio or Visual Studio Code and then open the Employee.cs file for editing. As you recall, this single file contains code of all aspects of the class.
在 C# 中,可以跨多个代码文件对单个类进行分区,以将样板代码与更有用(和复杂)的成员隔离开来。若要说明分部类在哪些方面可能有用,请打开您之前在本章中创建的 Visual Studio 或 Visual Studio Code 中的 EmployeeApp 项目,然后打开 Employee.cs 文件进行编辑。您还记得,这个文件包含类的所有方面的代码。

class Employee
{
// Field Data

// Constructors

// Methods

// Properties
}

Using partial classes, you could choose to move (for example) the properties, constructors, and field data into a new file named Employee.Core.cs (the name of the file is irrelevant). The first step is to add the partial keyword to the current class definition and cut the code to be placed into the new file.
使用分部类,可以选择将属性、构造函数和字段数据(例如)移动到名为 Employee.Core.cs 的新文件中(文件名无关紧要)。第一步是将 partial 关键字添加到当前类定义中,并剪切要放入新文件中的代码。

// Employee.cs
partial class Employee
{
// Methods

// Properties
}

Next, assuming you have inserted a new class file into your project, you can move the data fields and properties to the new file using a simple cut-and-paste operation. In addition, you must add the partial keyword to this aspect of the class definition. Here is an example:
接下来,假设您已将新的类文件插入到项目中,则可以使用简单的剪切和粘贴操作将数据字段和属性移动到新文件中。此外,还必须将 partial 关键字添加到类定义的这一方面。下面是一个示例:

// Employee.Core.cs
partial class Employee
{
// Field data

// Properties
}

■ Note remember that each of the partial classes must be marked with the partial keyword!
请注意,每个分部类都必须标有分部关键字!

After you compile the modified project, you should see no difference whatsoever. The whole idea of a partial class is realized only during design time. After the application has been compiled, there is just a single, unified class within the assembly. The only requirement when defining partial types is that the type’s name (Employee in this case) is identical and defined within the same .NET Core namespace.
编译修改后的项目后,您应该看不到任何区别。分部类的整个思想仅在设计时实现。应用程序编译完成后,只有一个程序集中的单个统一类。定义分部类型时的唯一要求是类型的名称(在本例中为 Employee)相同,并且在同一 .NET Core 命名空间中定义。
Recall from the discussion of top-level statements, any methods in top-level statements must be a local function. The top-level statements are implicitly defined in a partial Program class, allowing for the creation of another partial Program class to hold regular methods.
回想一下顶级语句的讨论,顶级语句中的任何方法都必须是局部函数。顶级语句在分部 Program 类中隐式定义,允许创建另一个分部 Program 类来保存常规方法。

Create a new console application named FunWithPartials and add a new class file named Program.
创建一个名为 FunWithPartss 的新控制台应用程序,并添加一个名为 Program 的新类文件。

Partial.cs. Update the code to the following:

public partial class Program
{
public static string SayHello() => return "Hello";
}

Now you can call that method from your top-level statements in the Program.cs file, like this:
现在,您可以从 Program.cs 文件中的顶级语句调用该方法,如下所示:

Console.WriteLine(SayHello());

Which method you use is a matter of preference.
您使用哪种方法是一个偏好问题。

Records (New 9.0)

记录(新 9.0)

New in C# 9.0, record types are a special reference type that provide synthesized methods for equality using value semantics and data encapsulation. Record types can be created with immutable or standard properties. To start experimenting with records, create a new console application named FunWithRecords. Consider the following Car class, modified from the examples earlier in the chapter:
记录类型是 C# 9.0 中的新增功能,是一种特殊的引用类型,它使用值语义和数据封装提供相等的综合方法。可以使用不可变或标准属性创建记录类型。若要开始试验记录,请创建一个名为 FunWithRecords 的新控制台应用程序。请考虑以下 Car 类,该类是从本章前面的示例修改而来的:

class Car
{
public string Make { get; set; } public string Model { get; set; } public string Color { get; set; }

public Car() {}

public Car(string make, string model, string color)
{
Make = make;

Model = model;
Color = color;
}
}

As you well know by now, once you create an instance of this class, you can change any of the properties at run time. If the properties for the previous Car class need to be immutable, you can change its property definitions to use init-only setters, like this:
众所周知,一旦创建了此类的实例,就可以在运行时更改任何属性。如果以前的 Car 类的属性需要不可变,则可以将其属性定义更改为使用仅初始化资源库,如下所示:

public string Make { get; init; } public string Model { get; init; } public string Color { get; init; }

To exercise this new class, the following code creates two instances of the Car class, one through object initialization and the other through the custom constructor. Update the Program.cs file to the following:
为了练习这个新类,下面的代码创建 Car 类的两个实例,一个通过对象初始化,另一个通过自定义构造函数。将程序.cs文件更新为以下内容:

using FunWithRecords; Console.WriteLine("Fun with Records!");
//Use object initialization Car myCar = new Car
{
Make = "Honda", Model = "Pilot", Color = "Blue"
};
Console.WriteLine("My car: "); DisplayCarStats(myCar); Console.WriteLine();
//Use the custom constructor
Car anotherMyCar = new Car("Honda", "Pilot", "Blue"); Console.WriteLine("Another variable for my car: "); DisplayCarStats(anotherMyCar);
Console.WriteLine();

//Compile error if property is changed
//myCar.Color = "Red"; Console.ReadLine();
static void DisplayCarStats(Car c)
{
Console.WriteLine("Car Make: {0}", c.Make); Console.WriteLine("Car Model: {0}", c.Model); Console.WriteLine("Car Color: {0}", c.Color);
}

As expected, both methods of object creation work, properties get displayed, and trying to change a property after construction raises a compilation error.
正如预期的那样,两种对象创建方法都有效,属性都会显示,并且在构造后尝试更改属性会引发编译错误。

Immutable Record Types with Standard Property Syntax

具有标准属性语法的不可变记录类型

Creating an immutable Car record type using standard property syntax is similar to creating classes with immutable properties. To see this in action, add a new file named (CarRecord.cs) to your project and add the following code:
使用标准属性语法创建不可变的 Car 记录类型类似于创建具有不可变属性的类。若要查看此操作的实际效果,请将名为 (CarRecord.cs) 的新文件添加到项目中,并添加以下代码:

record CarRecord
{
public string Make { get; init; } public string Model { get; init; } public string Color { get; init; }

public CarRecord () {}
public CarRecord (string make, string model, string color)
{
Make = make;
Model = model;
Color = color;
}
}

■ Note record types allow using the class keyword to help distinguish them from record structs, but the keyword is optional. Therefore record class and record mean the same thing.
注意记录类型允许使用 class 关键字来帮助将它们与记录结构s 区分开来,但关键字是可选的。因此,记录类和记录的含义相同。

You can confirm that the behavior is the same as the Car class with init-only settings by running the following code in Program.cs:
您可以通过在 Program.cs 中运行以下代码来确认该行为与具有仅初始化设置的 Car 类相同:

using FunWithRecords;

Console.WriteLine(" RECORDS **");
//Use object initialization
CarRecord myCarRecord = new CarRecord
{
Make = "Honda", Model = "Pilot", Color = "Blue"
};
Console.WriteLine("My car: "); DisplayCarRecordStats(myCarRecord); Console.WriteLine();

//Use the custom constructor
CarRecord anotherMyCarRecord = new CarRecord("Honda", "Pilot", "Blue"); Console.WriteLine("Another variable for my car: "); Console.WriteLine(anotherMyCarRecord.ToString());
Console.WriteLine();

//Compile error if property is changed
//myCarRecord.Color = "Red";

Console.ReadLine();

static void DisplayCarStats(Car c)
{
Console.WriteLine("Car Make: {0}", c.Make); Console.WriteLine("Car Model: {0}", c.Model); Console.WriteLine("Car Color: {0}", c.Color);
}

While we have not covered equality (next section) or inheritance (next chapter) with records, this first look at records does not seem like much of a benefit. The current Car example includes all of the plumbing code that we have come to expect. With one notable difference on the output: the ToString() method is fancied up for record types, as shown in this following sample output:
虽然我们还没有介绍记录的平等(下一节)或继承(下一章),但第一次看记录似乎并没有多大好处。当前的 Car 示例包括我们期望的所有管道代码。输出上有一个显着的区别:ToString() 方法适用于记录类型,如以下示例输出所示:

RECORDS ** My car:
CarRecord { Make = Honda, Model = Pilot, Color = Blue } Another variable for my car:
CarRecord { Make = Honda, Model = Pilot, Color = Blue }

Immutable Record Types with Positional Syntax

具有位置语法的不可变记录类型

Consider this updated (and much abbreviated) definition for the Car record:
考虑一下汽车记录的更新(和缩写)定义:

record CarRecord(string Make, string Model, string Color);

Referred to as a positional record type, the constructor defines the properties on the record, and all of the other plumbing code has been removed. There are three considerations when using this syntax: the first is that you cannot use object initialization of record types using the compact definition syntax, the second is that the record must be constructed with the properties in the correct position, and the third is that the casing of the properties in the constructor is directly translated to the casing of the properties on the record type.

构造函数称为位置记录类型,它定义记录上的属性,并且已删除所有其他管道代码。使用此语法时有三个注意事项:第一个是不能使用紧凑定义语法对记录类型使用对象初始化,第二个是必须使用位于正确位置的属性构造记录,第三个是构造函数中属性的大小写直接转换为记录类型上属性的大小写。
We can confirm that the Make, Model, and Color are all init-only properties on the Car record by looking at an abbreviated listing of the IL. Notice that there are backing fields for each of the parameters passed into the constructor, and each has the private and initonly modifiers.
我们可以通过查看 IL 的缩写列表来确认 Make、Model 和 Color 都是 Car 记录上的仅初始化属性。 请注意,传递给构造函数的每个参数都有支持字段,并且每个参数都有私有和 initonly 修饰符。

.class private auto ansi beforefieldinit FunWithRecords.CarRecord extends [System.Runtime]System.Object
implements class [System.Runtime]System.IEquatable`1
{
.field private initonly string 'k BackingField'
.field private initonly string 'k BackingField'
.field private initonly string 'k BackingField'
...
}

When using the positional syntax, record types provide a primary constructor that matches the positional parameters on the record declaration.
使用位置语法时,记录类型提供与记录声明上的位置参数匹配的主构造函数。

Deconstructing Mutable Record Types

解构可变记录类型

Record types using positional parameters also provide a Deconstruct() method with an out parameter for each positional parameter in the declaration. The following code creates a new record using the supplied constructor and then deconstructs the properties into separate variables:
使用位置参数的记录类型还提供一个 Deconstruct() 方法,该方法为声明中的每个位置参数提供一个 out 参数。下面的代码使用提供的构造函数创建新记录,然后将属性解构为单独的变量:

CarRecord myCarRecord = new CarRecord("Honda", "Pilot", "Blue"); myCarRecord.Deconstruct(out string make, out string model, out string color); Console.WriteLine($"Make: {make} Model: {model} Color: {color}");

Note that while the public properties on the record match the casing of the declaration, the out variables in the Deconstruct() method only have to match the position of the parameters. Changing the names of the variables in the Deconstruct() method still returns Make, Model, and Color, in that order:
请注意,虽然记录上的公共属性与声明的大小写匹配,但 Deconstruct() 方法中的 out 变量只需要与参数的位置匹配。在 Deconstruct() 方法中更改变量的名称仍按该顺序返回 Make、Model 和 Color:

myCarRecord.Deconstruct(out string a, out string b, out string c); Console.WriteLine($"Make: {a} Model: {b} Color: {c}");

The tuple syntax can also be used when deconstructing records. Note the following addition to the example:
解构记录时也可以使用元组语法。请注意示例中的以下补充:

var (make2, model2, color2) = myCarRecord;
Console.WriteLine($"Make: {make2} Model: {model2} Color: {color2}");

Mutable Record Types

可变记录类型
C# also supports mutable record types by using standard (not init-only) setters. The following is an example of this:
C# 还通过使用标准(不是仅初始化)资源库来支持可变记录类型。下面是一个示例:

record CarRecord
{
public string Make { get; set; } public string Model { get; set; } public string Color { get; set; }

public CarRecord() {}
public CarRecord(string make, string model, string color)
{
Make = make;
Model = model;
Color = color;
}
}

While this syntax is supported, the record types are intended to be used for immutable data models.
虽然支持此语法,但记录类型旨在用于不可变数据模型。

Value Equality with Record Types

记录类型的值相等

In the Car class example, the two Car instances were created with the same data. One might think that these two classes are equal, as the following line of code tests:
在 Car 类示例中,两个 Car 实例是使用相同的数据创建的。有人可能会认为这两个类是相等的,因为下面的代码行测试:

Console.WriteLine($"Cars are the same? {myCar.Equals(anotherMyCar)}");

However, they are not equal. Recall that record types are a specialized type of class, and classes are reference types. For two reference types to be equal, they have to point to the same object in memory. As a further test, check to see if the two Car objects point to the same object:
但是,它们并不相等。回想一下,记录类型是类的专用类型,类是引用类型。要使两个引用类型相等,它们必须指向内存中的同一对象。作为进一步的测试,请检查两个 Car 对象是否指向同一对象:

Console.WriteLine($"Cars are the same reference? {ReferenceEquals(myCar, anotherMyCar)}");

Running the program again produces this (abbreviated) result:
再次运行程序会产生以下(缩写)结果:

Cars are the same? False CarRecords are the same? False

Record types behave differently. Record types implicitly override Equals, ==, and !=, and two record types are considered equal if the hold the same values and are the same type, just as if the instances are value types. Consider the following code and the subsequent results:
记录类型的行为不同。记录类型隐式覆盖 Equals、== 和 !=,如果两个记录类型保存相同的值并且是相同的类型,则认为这两个记录类型相等,就像实例是值类型一样。请考虑以下代码和后续结果:

Console.WriteLine($"CarRecords are the same? {myCarRecord.Equals(anotherMyCarRecord)}"); Console.WriteLine($"CarRecords are the same reference? {ReferenceEquals(myCarRecord,another MyCarRecord)}");
Console.WriteLine($"CarRecords are the same? {myCarRecord == anotherMyCarRecord}"); Console.WriteLine($"CarRecords are not the same? {myCarRecord != anotherMyCarRecord}");

/ RECORDS **/ My car:
CarRecord { Make = Honda, Model = Pilot, Color = Blue } Another variable for my car:
CarRecord { Make = Honda, Model = Pilot, Color = Blue }

CarRecords are the same? True
CarRecords are the same reference? false CarRecords are the same? True
CarRecords are not the same? False

Notice that they are considered equal, even though the variables point to two different variables in memory.
请注意,即使变量指向内存中的两个不同变量,它们也被视为相等。

Copying Record Types Using with Expressions

复制记录类型 与表达式一起使用

With record types, assigning a record type instance to a new variable creates a pointer to the same reference, which is the same behavior as classes. The following code demonstrates this:
对于记录类型,将记录类型实例分配给新变量会创建一个指向同一引用的指针,这与类的行为相同。以下代码对此进行了演示:

CarRecord carRecordCopy = anotherMyCarRecord; Console.WriteLine("Car Record copy results");
Console.WriteLine($"CarRecords are the same? {carRecordCopy.Equals(anotherMyCarRecord)}"); Console.WriteLine($"CarRecords are the same? {ReferenceEquals(carRecordCopy, anotherMyCarRecord)}");

When executed, both tests return true, proving that they are the same in value and reference.
执行时,两个测试都返回 true,证明它们在值和引用上相同。

To create a true copy of a record with one or more properties modified (referred to as nondestructive mutation), C# 9.0 introduces with expressions. In the with construct, any properties that need to be updated are specified with their new values, and any properties not listed are shallow copied exactly. Examine the following example:
若要创建修改了一个或多个属性(称为非破坏性突变)的记录的真实副本,C# 9.0 引入了表达式。 在 with 构造中,需要更新的任何属性都使用其新值指定,并且未列出的任何属性都是完全浅拷贝的。检查以下示例:

CarRecord ourOtherCar = myCarRecord with {Model = "Odyssey"}; Console.WriteLine("My copied car:"); Console.WriteLine(ourOtherCar.ToString());

Console.WriteLine("Car Record copy using with expression results"); Console.WriteLine($"CarRecords are the same? {ourOtherCar.Equals(myCarRecord)}"); Console.WriteLine($"CarRecords are the same? {ReferenceEquals(ourOtherCar, myCarRecord)}");

The code creates a new instance of the CarRecord type, copying the Make and Color values of the
myCarRecord instance and setting Model to the string Odyssey. The results of this code is shown here:
该代码创建 CarRecord 类型的新实例,复制myCarRecord 实例并将模型设置为字符串 Odyssey。此代码的结果如下所示:

/ RECORDS **/ My copied car:
CarRecord { Make = Honda, Model = Odyssey, Color = Blue }

Car Record copy using with expression results CarRecords are the same? False
CarRecords are the same? False

Using with expressions, you can easily take complex record types into new record type instances with updated property values.
使用 with 表达式,您可以轻松地将复杂的记录类型放入具有更新属性值的新记录类型实例中。

Record Structs (New 10.0)

记录结构(新 10.0)

New in C# 10.0, record structs are the value type equivalent of record types. Record structs can also use positional parameters or standard property syntax and provide value equality, nondestructive mutation, and built-in display formatting. To start experimenting with records, create a new console application named FunWithRecordStructs. One major difference between record structs and records is that a record struct is mutable by default. To make a record struct immutable, you must use add the readonly modifier.
作为 C# 10.0 中的新增功能,记录结构是等效于记录类型的值类型。记录结构还可以使用位置参数或标准属性语法,并提供值相等、非破坏性突变和内置显示格式。若要开始试验记录,请创建一个名为 FunWithRecordStructs 的新控制台应用程序。记录结构和记录之间的一个主要区别是,默认情况下,记录结构是可变的。若要使记录结构不可变,必须使用添加只读修饰符。

Mutable Record Structs

可变记录结构

To create a record struct, let’s revisit our friend the Point struct. The following shows how to create two different record struct types, one using the positional syntax and the other using standard properties:
要创建一个记录结构,让我们重新访问我们的朋友 Point 结构。下面演示如何创建两种不同的记录结构类型,一种使用位置语法,另一种使用标准属性:

public record struct Point(double X, double Y, double Z); public record struct PointWithPropertySyntax()
{
public double X { get; set; } = default; public double Y { get; set; } = default; public double Z { get; set; } = default;

public PointWithPropertySyntax(double x, double y, double z) : this()
{
X = x; Y = y; Z = z;
}
};

The following code demonstrates the mutability of the two record struct types as well as the improved ToString() method:
下面的代码演示了两种记录结构类型的可变性以及改进的ToString() 方法:

Console.WriteLine(" Fun With Record Structs "); var rs = new Point(2, 4, 6); Console.WriteLine(rs.ToString());
rs.X = 8;
Console.WriteLine(rs.ToString());

var rs2 = new PointWithPropertySyntax(2, 4, 6); Console.WriteLine(rs2.ToString());
rs2.X = 8;
Console.WriteLine(rs2.ToString());

Immutable Record Structs

不可变的记录结构

The previous two record struct examples can be made immutable by adding the readonly keyword:
前两个记录结构示例可以通过添加只读关键字使它们不可变:

public readonly record struct ReadOnlyPoint(double X, double Y, double Z); public readonly record struct ReadOnlyPointWithPropertySyntax()
{
public double X { get; init; } = default; public double Y { get; init; } = default; public double Z { get; init; } = default;
public ReadOnlyPointWithPropertySyntax(double x, double y, double z) : this()
{
X = x; Y = y; Z = z;
}
};

You can confirm that they are now immutable (enforced by the compiler) with the following code:
您可以使用以下代码确认它们现在是不可变的(由编译器强制执行):

var rors = new ReadOnlyPoint(2, 4, 6);
//Compiler Error:
//rors.X = 8;

var rors2 = new ReadOnlyPointWithPropertySyntax(2, 4, 6);
//Compiler Error:
//rors2.X = 8;

Value equality and copying using with expressions work the same as record types.
值相等和与表达式一起使用的复制与记录类型的工作方式相同。

Deconstructing Record Structs

解构记录结构

Like record types, record structs that use positional syntax also provide a Deconstruct() method. The behavior is the same as mutable and immutable record structs. The following code creates a new record using the supplied constructor and then deconstructs the properties into separate variables:
与记录类型一样,使用位置语法的记录结构也提供 Deconstruct() 方法。该行为与可变和不可变记录结构相同。下面的代码使用提供的构造函数创建新记录,然后将属性解构为单独的变量:

Console.WriteLine("Deconstruction: "); var (x1, y1, z1) = rs;
Console.WriteLine($"X: {x1} Y: {y1} Z: {z1}"); var (x2, y2, z2) = rors; Console.WriteLine($"X: {x2} Y: {y2} Z: {z2}");
rs.Deconstruct(out double x3,out double y3,out double z3); Console.WriteLine($"X: {x3} Y: {y3} Z: {z3}"); rors.Deconstruct(out double x4,out double y4,out double z4); Console.WriteLine($"X: {x4} Y: {y4} Z: {z4}");

Summary
总结

The point of this chapter was to introduce you to the role of the C# class type and the new C# 9.0 record type. As you have seen, classes can take any number of constructors that enable the object user to establish the state of the object upon creation. This chapter also illustrated several class design techniques (and related keywords). The this keyword can be used to obtain access to the current object. The static keyword allows you to define fields and members that are bound at the class (not object) level. The const keyword, readonly modifier, and init-only setters allow you to define a point of data that can never change after the initial assignment or object construction. Record types are a special type of class that are immutable (by default) and behave like value types when comparing a record type with another instance of the same record type. Record structs are value types that are mutable (by default) and provide the same equality and nondestructive mutation capabilities as record types.
本章的重点是向您介绍 C# 类类型和新的 C# 9.0 记录类型的作用。如您所见,类可以采用任意数量的构造函数,这些构造函数使对象用户能够在创建时建立对象的状态。本章还说明了几种类设计技术(和相关关键字)。this 关键字可用于获取对当前对象的访问权限。静态关键字允许您定义绑定在类(而非对象)级别的字段和成员。const 关键字、只读修饰符和仅初始化资源库允许您定义在初始赋值或对象构造后永远不会更改的数据点。记录类型是不可变的特殊类型的类(默认情况下)并且在将记录类型与同一记录类型的另一个实例进行比较时的行为类似于值类型。记录结构是可变的值类型(默认情况下),并提供与记录类型相同的相等和非破坏性突变功能。

The bulk of this chapter dug into the details of the first pillar of OOP: encapsulation. You learned about the access modifiers of C# and the role of type properties, object initialization syntax, and partial classes.
本章的大部分内容深入探讨了 OOP 的第一个支柱:封装。您了解了 C# 的访问修饰符以及类型属性、对象初始化语法和分部类的作用。

With this behind you, you are now able to turn to the next chapter where you will learn to build a family of related classes using inheritance and polymorphism.
有了这个,你现在可以进入下一章,你将学习使用继承和多态性构建一个相关类的家族。

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章中,您将开始深入研究面向对象开发的细节。

Pro C#10 CHAPTER 3 Core C# Programming Constructs, Part 1

PART II

Core C# Programming

CHAPTER 3 Core C# Programming Constructs, Part 1

核心 C# 编程构造,第 1 部分

This chapter begins your formal investigation of the C# programming language by presenting a number of bite-sized, stand-alone topics you must be comfortable with as you explore the .NET Core Framework. The first order of business is to understand how to build your program’s application object and to examine the composition of an executable program’s entry point: the Main() method as well as a new C# 9.0 feature,top-level statements. Next, you will investigate the fundamental C# data types (and their equivalent types in the System namespace) including an examination of the System.String and System.Text.StringBuilder classes.
本章通过介绍一些在探索 .NET Core Framework 时必须熟悉的小型独立主题,开始对 C# 编程语言进行正式研究。首要任务是了解如何构建程序的应用程序对象并检查可执行程序入口点的组成:Main() 方法以及新的 C# 9.0 功能,顶级语句。接下来,您将研究基本的 C# 数据类型(及其在 System 命名空间中的等效类型),包括检查 System.String 和 System.Text.StringBuilder 类。

After you know the details of the fundamental .NET Core data types, you will then examine a number of data type conversion techniques, including narrowing operations, widening operations, and the use of the checked and unchecked keywords.
了解基本 .NET Core 数据类型的详细信息后,您将检查许多数据类型转换技术,包括缩小操作、扩大操作以及选中和未选中关键字的使用。

This chapter will also examine the role of the C# var keyword, which allows you to implicitly definea local variable. As you will see later in this book, implicit typing is extremely helpful, if not occasionally mandatory, when working with the LINQ technology set. You will wrap up this chapter by quickly examining the C# keywords and operators that allow you to control the flow of an application using various looping and decision constructs.
本章还将研究 C# var 关键字的作用,该关键字允许您隐式定义局部变量。正如您将在本书后面看到的那样,在使用 LINQ 技术集时,隐式键入非常有用(如果不是偶尔是必需的)。通过快速检查 C# 关键字和运算符来结束本章,这些关键字和运算符允许您使用各种循环和决策构造来控制应用程序的流。

Breaking Down a Simple C# Program (Updated C# 10)

分解简单的 C# 程序(更新的 C# 10)

C# demands that all program logic be contained within a type definition (recall from Chapter 1 that type is a general term referring to a member of the set {class, interface, structure, enumeration, delegate}). Unlike many other languages, in C# it is not possible to create global functions or global points of data. Rather, all data members and all methods must be contained within a type definition. To get the ball rolling, create a new empty solution named Chapter3_AllProject.sln that contains a C# console application named SimpleCSharpApp.
C# 要求所有程序逻辑都包含在类型定义中(回想一下第 1 章,类型是一个通用术语,指的是集合 {类、接口、结构、枚举、委托} 的成员)。 与许多其他语言不同,在 C# 中无法创建全局函数或全局数据点。相反,所有数据成员和所有方法都必须包含在类型定义中。要让球滚动,请创建一个名为 Chapter3_AllProject.sln 的新空解决方案,其中包含一个名为 SimpleCSharpApp 的 C# 控制台应用程序。

From Visual Studio, select the Blank Solution template on the “Create a new project” screen. When the solution opens, right-click the solution in Solution Explorer and select Add ➤ New Project. Select “C# console app” from the templates, name it SimpleCSharpApp, and click Next. Select .NET 6.0 for the framework and then click Create.
从 Visual Studio 中,在“创建新项目”屏幕上选择“空白解决方案”模板。当解决方案打开时,在“解决方案资源管理器”中右键单击该解决方案,然后选择“添加”➤“新建项目”。从模板中选择“C# 控制台应用”,将其命名为 SimpleCSharpApp,然后单击“下一步”。为框架选择“.NET 6.0”,然后单击“创建”。

To create a solution and console application and add that console application to the solution, from the command line (or the Visual Studio Code terminal window), execute the following:
若要创建解决方案和控制台应用程序并将该控制台应用程序添加到解决方案中,请从命令行(或 Visual Studio 代码终端窗口)执行以下命令:

dotnet new sln -n Chapter3_AllProjects
dotnet new console -lang c# -n SimpleCSharpApp -o .\SimpleCSharpApp -f net6.0 
dotnet sln .\Chapter3_AllProjects.sln add .\SimpleCSharpApp

In the created project, you will see one file (named Program.cs) with one line of code.
在创建的项目中,您将看到一个包含一行代码的文件(名为 Program.cs)。

Console.WriteLine("Hello, World!");

If you are new to C#, this line seems pretty straightforward. It writes the message “Hello, World!” to the standard console output window. Prior to C# 10, there was a lot more code required to achieve the same effect. When creating the same program in versions of C# prior to C# 10, you were required to write the following:
如果你不熟悉 C#,这一行似乎很简单。它将消息“Hello, World!”写入标准控制台输出窗口。在 C# 10 之前,需要更多代码才能达到相同的效果。在 C# 10 之前的 C# 版本中创建同一程序时,需要编写以下内容:

using System;
namespace SimpleCSharpApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

The Console class is contained in the System namespace, and with the implicit global namespaces provided by .NET 6/C# 10, the using System; statement is no longer needed. The next line creates a custom namespace (covered in Chapter 16) to wrap the Program class. Both the namespace and the Program class can be removed due to the top-level statement functionality introduced in C# 9 (covered shortly). That brings us back to the single line of code to write the message to the Console.
控制台类包含在 System 命名空间中,并使用 .NET 6/C# 10 提供的隐式全局命名空间,using System; 不再需要语句。下一行创建一个自定义命名空间(在第 16 章中介绍)来包装 Program 类。由于 C# 9 中引入的顶级语句功能(稍后将介绍),可以删除命名空间和 Program 类。这使我们回到将消息写入控制台的单行代码。

For now, to cover some important variations of the entry point into C# applications, we will use the older (more verbose) style of code instead of the streamlined C# 10 version. Given this, update the Main() method of your Program class with the following code statements:
现在,为了介绍 C# 应用程序入口点的一些重要变体,我们将使用较旧(更详细)的代码样式,而不是简化的 C# 10 版本。鉴于此,使用以下代码语句更新 Program 类的 Main() 方法:

class Program
{
    static void Main(string[] args)
    {
        // Display a simple message to the user.
        // 向用户显示一条简单的消息。
        Console.WriteLine("***** My First C# App *****");
        Console.WriteLine("Hello World!");
        Console.WriteLine();

        // Wait for Enter key to be pressed before shutting down.
        //等待按回车键后再关闭。
        Console.ReadLine();
    }
}

■ Note C# is a case-sensitive programming language. therefore, Main is not the same as main, and Readline is not the same as ReadLine. Be aware that all C# keywords are lowercase (e.g., public, lock, class, dynamic), while namespaces, types, and member names begin (by convention) with an initial capital letter and the first letter of any embedded words is capitalized (e.g., Console.WriteLine, System.Windows. MessageBox, System.Data.SqlClient). as a rule of thumb, whenever you receive a compiler error regarding “undefined symbols,” be sure to check your spelling and casing first!
注意 C# 是一种区分大小写的编程语言。因此,Main 与 main 不同,Readline 与 ReadLine 也不相同。请注意,所有 C# 关键字都是小写的(例如,public、lock、class、dynamic),而命名空间、类型和成员名称(按照惯例)以首字母大写字母开头任何嵌入单词的第一个字母都是大写的(例如,Console.WriteLine,System.Windows。MessageBox, System.Data.SqlClient)。根据经验,每当您收到有关“未定义符号”的编译器错误时,请务必先检查拼写和大小写!

The previous code contains a definition for a class type that supports a single method named Main(). By default, the C# project templates that don’t use top-level statements name the class containing the Main() method Program; however, you are free to change this if you so choose. Prior to C# 9.0, every executable C# application (console program, Windows desktop program, or Windows service) must contain a class defining a Main() method, which is used to signify the entry point of the application.
前面的代码包含支持名为 Main() 的单个方法的类类型的定义。默认情况下,不使用顶级语句的 C# 项目模板命名包含 Main() 方法程序的类;但是,如果您愿意,您可以自由更改此设置。在 C# 9.0 之前,每个可执行文件C# 应用程序(控制台程序、Windows 桌面程序或 Windows 服务)必须包含一个定义 Main() 方法的类,该方法用于表示应用程序的入口点。

Formally speaking, the class that defines the Main() method is termed the application object. It is possible for a single executable application to have more than one application object (which can be useful when performing unit tests), but then the compiler must know which Main() method should be used as the entry point. This can be done via the element in the project file or via the Startup Object drop-down list box, located on the Application tab of the Visual Studio project properties window.
从形式上讲,定义 Main() 方法的类称为应用程序对象。是的单个可执行应用程序可能具有多个应用程序对象(这在执行单元测试时很有用),但是编译器必须知道应使用哪个 Main() 方法作为入口点。这可以通过项目文件中的元素或通过位于Visual Studio项目属性窗口的“应用程序”选项卡上的“启动对象”下拉列表框来完成。

Note that the signature of Main() is adorned with the static keyword, which will be examined in detail in Chapter 5. For the time being, simply understand that static members are scoped to the class level (rather than the object level) and can thus be invoked without the need to first create a new class instance.
请注意,Main() 的签名装饰有静态关键字,这将在第 5 章中详细研究。目前,只需理解静态成员的作用域为类级别(而不是对象级别),因此无需先创建新的类实例即可调用。

In addition to the static keyword, this Main() method has a single parameter, which happens to be an array of strings (string[] args). Although you are not currently bothering to process this array, this parameter may contain any number of incoming command-line arguments (you will see how to access them momentarily). Finally, this Main() method has been set up with a void return value, meaning you do not explicitly define a return value using the return keyword before exiting the method scope.
除了 static 关键字之外,这个 Main() 方法还有一个参数,它恰好是一个字符串数组(string[] args)。虽然您目前没有费心处理此数组,但这参数可能包含任意数量的传入命令行参数(您将立即了解如何访问它们)。最后,这个 Main() 方法已经设置了一个 void 返回值,这意味着在退出方法范围之前,您不会使用 return 关键字显式定义返回值。

The logic of the Program class is within Main(). Here, you make use of the Console class, which is defined within the System namespace. Among its set of members is the static WriteLine(), which, as you might assume, sends a text string and carriage return to the standard output. You also make a call to Console.ReadLine() to ensure the command prompt launched by the Visual Studio IDE remains visible. When running .NET Core Console apps with Visual Studio (in either Debug or Release mode), the console window remains visible by default. This behavior can be changed by enabling the setting “Automatically close the console when debugging stops” found under Tools ➤ Options ➤ Debugging. The Console.ReadLine() method is there to keep the window open if the program is executed from Windows Explorer by double- clicking the product .exe file. You will learn more about the System.Console class shortly.
Program 类的逻辑在 Main() 中。在这里,您将使用在 System 命名空间中定义的 Console 类。在其成员集中是静态 WriteLine(),正如您可能假设的那样,它将文本字符串和回车符发送到标准输出。您还可以调用控制台。ReadLine() 来确保 Visual Studio IDE 启动的命令提示符仍然可见。使用 Visual Studio 运行 .NET Core 控制台应用(在调试或发布模式下)时,控制台窗口默认保持可见。可以通过启用“工具”➤“选项”➤“调试”下的“调试停止时自动关闭控制台”设置来更改此行为。如果通过双击产品
.exe 文件从 Windows 资源管理器执行程序,则 Console.ReadLine() 方法可以保持窗口打开。您将很快了解有关 System.Console 类的更多信息。v

Using Variations of the Main() Method (Updated 7.1)

使用 main() 方法的变体(更新 7.1)

By default, the .NET console project template will generate a Main() method that has a void return value and an array of string types as the single input parameter. This is not the only possible form of Main(), however. It is permissible to construct your application’s entry point using any of the following signatures (assuming it is contained within a C# class or structure definition):
默认情况下,.NET 控制台项目模板将生成一个 Main() 方法,该方法具有 void 返回值和字符串类型数组作为单个输入参数。然而,这并不是 Main() 的唯一可能形式。允许使用以下任何签名构造应用程序的入口点(假设它包含在 C# 类或结构定义中):

// int return type, array of strings as the parameter.
// int 返回类型,字符串数组作为参数。
static int Main(string[] args)
{
    // Must return a value before exiting!
    // 退出前必须返回一个值!return 0;
    return 0;
}

// No return type, no parameters.
// 没有返回类型,没有参数。
static void Main()
{
}

// int return type, no parameters.
static int Main()
{
    // Must return a value before exiting!
    return 0;
}

With the release of C# 7.1, the Main() method can be asynchronous. Async programming is covered in Chapter 15, but for now realize there are four additional signatures.
随着 C# 7.1 的发布,Main() 方法可以是异步的。异步编程在第15章中介绍,但现在意识到还有四个额外的签名。

static Task Main() 
static Task<int> Main()
static Task Main(string[]) 
static Task<int> Main(string[])

■ Note the Main() method may also be defined as public as opposed to private. note that private is assumed if you do not supply a specific access modifier. access modifiers are covered in detail in Chapter 5.
注意 Main() 方法也可以定义为公共的,而不是私有的。 请注意,如果未提供特定的访问修饰符,则假定为私有。 访问修饰符在第 5 章中有详细介绍。

Obviously, your choice of how to construct Main() will be based on three questions. First, do you want to return a value to the system when Main() has completed and your program terminates? If so, you need to return an int data type rather than void. Second, do you need to process any user-supplied, command- line parameters? If so, they will be stored in the array of strings. Lastly, do you need to call asynchronous code from the Main() method? We’ll examine the first two options in more detail after introducing top-level statements. The async options will be covered in Chapter 15.
显然,你选择如何构造 Main() 将基于三个问题。首先,是否要在 Main() 完成并且程序终止时向系统返回一个值?如果是这样,则需要返回 int 数据类型而不是 void。其次,是否需要处理任何用户提供的命令行参数?如果是这样,它们将存储在字符串s 数组中。最后,是否需要从 Main() 方法调用异步代码?在引入顶级语句后,我们将更详细地研究前两个选项。异步选项将在第 15 章中介绍。

Using Top-Level Statements (New 9.0)

使用顶级语句(新 9.0)

While it is true that prior to C# 9.0, all C# .NET Core applications must have a Main() method, C# 9.0 introduced top-level statements, which eliminate the need for much of the ceremony around the C# application’s entry point. Both the class (Program) and Main() methods can be removed. To see this in action, update the Program.cs class to match the following:
虽然在 C# 9.0 之前,所有 C# .NET Core 应用程序都必须具有 Main() 方法,但 C# 9.0 引入了顶级语句,这消除了围绕 C# 应用程序入口点进行大部分仪式的需要。类 (Program) 和 Main() 方法都可以删除。若要查看此操作的实际效果,请更新 Program.cs 类以匹配以下内容:

// Display a simple message to the user.
// 向用户显示一条简单的消息。
Console.WriteLine("***** My First C# App *****");
Console.WriteLine("Hello World!");
Console.WriteLine();

// Wait for Enter key to be pressed before shutting down.
// 等待按回车键后再关闭。
Console.ReadLine();

You will see that when you run the program, you get the same result! There are some rules around using top-level statements:
您将看到,当您运行该程序时,您会得到相同的结果!使用顶级语句有一些规则:

  • Only one file in the application can use top-level statements.
    应用程序中只有一个文件可以使用顶级语句。
  • When using top-level statements, the program cannot have a declared entry point.
    使用顶级语句时,程序不能具有声明的入口点。
  • The top-level statements cannot be enclosed in a namespace.
    顶级语句不能包含在命名空间中。
  • Top-level statements still access a string array of args.
    顶级语句仍访问参数的字符串数组。
  • Top-level statements return an application code (see the next section) by using a return.
    顶级语句使用返回应用程序代码(请参阅下一节)。
  • Functions that would have been declared in the Program class become local functions for the top-level statements. (Local functions are covered in Chapter 4.)
    本应在 Program 类中声明的函数将成为顶级语句的本地函数。(本章介绍了本地函数 4.)
  • The top-level statements compile to a class named Program, allowing for the addition of a partial Program class to hold regular methods. Partial classes are covered in Chapter 5.
    顶级语句编译为名为 Program 的类,允许添加分部 Program 类来保存常规方法。第5章介绍了部分类。
  • Additional types can be declared after all top-level statements. Any types declared before the end of the top-level statements will result in a compilation error.
    可以在所有顶级语句之后声明其他类型。在顶级语句末尾声明的任何类型都将导致编译错误。

Behind the scenes, the compiler fills in the blanks. Examining the generated IL for the updated code, you will see the following TypeDef for the entry point into the application:
在幕后,编译器填补空白。检查生成的 IL 以获取更新的代码,您将看到应用程序的入口点的以下 TypeDef:

// TypeDef #1 (02000002)
// -------------------------------------------------------
// TypDefName: <Program>$ (02000002)
// Flags : [NotPublic] [AutoLayout] [Class] [Abstract] [Sealed] [AnsiClass] [BeforeFieldInit] (00100180)
// Extends : 0100000D [TypeRef] System.Object
// Method #1 (06000001) [ENTRYPOINT]
// -------------------------------------------------------
// MethodName: <Main>$ (06000001)
Compare that to the TypeDef for the entry point from Chapter 1:
// TypeDef #1 (02000002)
// -------------------------------------------------------
// TypDefName: CalculatorExamples.Program (02000002)
// Flags : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit] (00100000)
// Extends : 0100000C [TypeRef] System.Object
// Method #1 (06000001) [ENTRYPOINT]
// -------------------------------------------------------
// MethodName: Main (06000001)

Notice for the example from Chapter 1, the TypDefName value is shown as the namespace (CalculatorExamples) plus the class name (Program), and the MethodName value is Main. In the updated example using top-level statements, the compiler has filled in the values of $ for the TypDefName and

$ for the method name.
请注意,对于第 1 章中的示例,TypDefName 值显示为命名空间(计算器示例)加上类名(程序),MethodName 值显示为 Main。在使用顶级语句的更新示例中,编译器为TypDefName填充了$的值,为方法名称填写了
$的值。

Specifying an Application Error Code (Updated 9.0)

指定应用程序错误代码(9.0 更新)

While a vast majority of your Main() methods (or top-level statements) will return void as the return value, the ability to return an int (or Task) keeps C# consistent with other C-based languages. By convention, returning the value 0 indicates the program has terminated successfully, while another value (such as -1) represents an error condition (be aware that the value 0 is automatically returned, even if you construct a Main() method prototyped to return void).
虽然绝大多数 Main() 方法(或顶级语句)将返回 void 作为返回值,但返回 int(或 Task)的能力使 C# 与其他基于 C 的语言保持一致。按照惯例,返回值 0 表示程序已成功终止,而另一个值(如 -1)表示错误条件(请注意,值 0 会自动返回,即使您构造了一个原型化为返回 void 的 Main() 方法)。

When using top-level statements (and therefore no Main() method), if the executing code returns an integer, that is the return code. If nothing is explicitly returned, it still returns 0, as with explicitly using a Main() method.
当使用顶级语句(因此没有 Main() 方法)时,如果执行代码返回一个整数,那就是返回代码。如果未显式返回任何内容,它仍返回 0,就像显式使用 Main() 方法一样。

On the Windows operating system, an application’s return value is stored within a system environment variable named %ERRORLEVEL%. If you were to create an application that programmatically launches another executable (a topic examined in Chapter 18), you can obtain the value of %ERRORLEVEL% using the ExitCode property of the launched process.
在 Windows 操作系统上,应用程序的返回值存储在名为 %ERRORLEVEL% 的系统环境变量中。如果要创建一个以编程方式启动另一个可执行文件的应用程序(第 18 章中介绍的主题),则可以使用启动的进程的 ExitCode 属性获取 %ERRORLEVEL% 的值。

Given that an application’s return value is passed to the system at the time the application terminates, it is obviously not possible for an application to obtain and display its final error code while running.However, to illustrate how to view this error level upon program termination, begin by updating the top-level statements, as follows:
假设应用程序的返回值在应用程序终止时传递给系统,则应用程序显然不可能在运行时获取并显示其最终错误代码。但是,为了说明如何在程序终止时查看此错误级别,请首先更新顶级语句,如下所示:

// Note we are explicitly returning an int, rather than void.
// 请注意,我们显式返回一个 int,而不是 void。
// Display a message and wait for Enter key to be pressed.
// 显示一条消息并等待按下 Enter 键。
Console.WriteLine("***** My First C# App *****");
Console.WriteLine("Hello World!");
Console.WriteLine();
Console.ReadLine();

// Return an arbitrary error code. 
// 返回任意错误代码。返回 -1;
return -1;

If the program is still using a Main() method as the entry point, change the method signature to return int instead of void, as follows:
如果程序仍在使用 Main() 方法作为入口点,请将方法签名更改为返回int 而不是 void,如下所示:

static int Main()
{
...
}

Now let’s capture the return value of the program with the help of a batch file. Using Windows Explorer, navigate to the folder containing your project file (e.g., C:\SimpleCSharpApp) and add a new text file (named SimpleCSharpApp.cmd) to that folder. Update the contents of the folder to the following (if you have not authored .cmd files before, do not concern yourself with the details):
现在让我们借助批处理文件捕获程序的返回值。使用 Windows 资源管理器,导航到包含项目文件的文件夹(例如 C:\SimpleCSharpApp),并将新的文本文件(名为 SimpleCSharpApp.cmd)添加到该文件夹。 将文件夹的内容更新为以下内容(如果您以前没有创作过
.cmd 文件,请不要关心详细信息):

@echo off
rem A batch file for SimpleCSharpApp.exe
rem which captures the app's return value.
dotnet run
@if "%ERRORLEVEL%" == "0" goto success

:fail
    echo This application has failed!
    echo return value = %ERRORLEVEL%
    goto end
:success
    echo This application has succeeded!
    echo return value = %ERRORLEVEL%
    goto end
:end
echo All Done.

At this point, open a command prompt (or use the Visual Studio Code terminal) and navigate to the folder containing your new .cmd file. Execute the file by typing its name and pressing the Enter key. You should find the output shown next, given that your top-level statements (or Main() method) return -1. Had the top-level statements (or Main() method) returned 0, you would see the message “This application has succeeded!” print to the console.
此时,打开命令提示符(或使用 Visual Studio 代码终端)并导航到包含新
.cmd 文件的文件夹。通过键入文件名称并按 Enter 键来执行文件。您应该找到接下来显示的输出,因为您的顶级语句(或 Main() 方法)返回 -1。如果顶级语句(或 Main() 方法)返回 0,您将看到消息“此应用程序已成功!

***** My First C# App ***** 

Hello World!

This application has failed! Return value = -1
All Done.

The PowerShell equivalent of the preceding .cmd file is as follows:
前面的
.cmd 文件的 PowerShell 等效项如下所示:

dotnet run
if ($LastExitCode -eq 0) {
    Write-Host "This application has succeeded!"
} else
{
    Write-Host "This application has failed!"
}
Write-Host "All Done."

To run this, type PowerShell into the Visual Studio Code terminal and then execute the script by typing this:
若要运行此命令,请在 Visual Studio Code 终端中键入 PowerShell,然后通过键入以下命令来执行脚本:

.\SimpleCSharpApp.ps1

You will see the following in the terminal window:
您将在终端窗口中看到以下内容:

***** My First C# App ***** 
Hello World!

This application has failed! All Done.

■ Note if you receive a security policy error when running the powershell script, you can set the policy to allow unsigned local scripts by executing the following command in powershell:set-executionpolicy -executionpolicy remotesigned -scope currentuser
注意 如果在运行 powershell 脚本时收到安全策略错误,可以通过在 powershell 中执行以下命令将策略设置为允许未签名的本地脚本:set-executionpolicy -executionpolicy remotesigned -scope currentuser

A vast majority (if not all) of your C# applications will use void as the return value from Main(), which, as you recall, implicitly returns the error code of zero. To this end, the Main() methods used in this text (beyond the current example) will return void.
绝大多数(如果不是全部)C# 应用程序将使用 void 作为 Main() 的返回值,您还记得,它隐式返回错误代码零。为此,本文中使用的 Main() 方法(当前示例之外)将返回 void。

Processing Command-Line Arguments (Updated 9.0)

处理命令行参数(9.0 更新)

Now that you better understand the return value of the Main() method or top-level statements, let’s examine the incoming array of string data. Assume that you now want to update your application to process any possible command-line parameters. One way to do this is using a C# for loop. (Note that C#’s iteration constructs will be examined in some detail near the end of this chapter.)
现在您已经更好地了解了 Main() 方法或顶级语句的返回值,让我们检查一下传入的字符串数据数组。假设您现在想要更新应用程序以处理任何可能的命令行参数。执行此操作的一种方法是使用 C# for 循环。(请注意,在本章末尾将详细检查 C# 的迭代构造)。

// Display a message and wait for Enter key to be pressed.
// 显示一条消息并等待按下 Enter 键。
Console.WriteLine("***** My First C# App *****");
Console.WriteLine("Hello World!");
Console.WriteLine();
// Process any incoming args.
// 处理任何传入的参数。
for (int i = 0; i < args.Length; i++)
{
    Console.WriteLine("Arg: {0}", args[i]);
}
Console.ReadLine();

// Return an arbitrary error code.
// 返回任意错误代码。
return 0;

■ Note this example is using top-level statements, which doesn’t utilize a Main() method. updating the Main() method to accept the args parameter is covered shortly.
请注意,此示例使用的是不使用 Main() 方法的顶级语句。更新稍后将介绍接受 args 参数的 main() 方法。

Once again, examining the generated IL for the program using top-level statements, notice that the

$ method accepts a string array named args, as shown here (abbreviated for space):
再次使用顶级语句检查为程序生成的 IL,请注意
$ 方法接受一个名为 args 的字符串数组,如下所示(空格缩写):

.class private abstract auto ansi sealed beforefieldinit '<Program>$' extends [System.Runtime]System.Object
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()=
    ( 01 00 00 00 )
    .method private hidebysig static int32 '<Main>$'(string[] args) cil managed
    {
        .entrypoint
    ...
    } // end of method '<Program>$'::'<Main>$'
} // end of class '<Program>$'

If the program is still using a Main() method as the entry point, make sure the method signature accepts a string array named args, as follows:
如果程序仍在使用 Main() 方法作为入口点,请确保方法签名接受名为 args 的字符串数组,如下所示:

static int Main(string[] args)
{
...
}

Here, you are checking to see whether the array of strings contains some number of items using the Length property of System.Array. As you will see in Chapter 4, all C# arrays actually alias the System.Array class and, therefore, share a common set of members. As you loop over each item in the array, its value is printed to the console window. Supplying the arguments at the command line is equally simple, as shown here:
在这里,您将使用 System.Array 的 Length 属性检查字符串s 的数组是否包含一定数量的项。正如您将在第 4 章中看到的,所有 C# 数组实际上都为系统设置了别名。数组类,因此共享一组公共成员。循环遍历数组中的每个项时,其值将打印到控制台窗口。在命令行中提供参数同样简单,如下所示:

// 在项目目录输入 dotnet run /arg1 -arg2
C:\SimpleCSharpApp>dotnet run /arg1 -arg2
// 输出结果
***** My First C# App ***** 
Hello World!
Arg: /arg1 
Arg: -arg2

As an alternative to the standard for loop, you may iterate over an incoming string array using the C# foreach keyword. Here is some sample usage (but again, you will see specifics of looping constructs later in this chapter):
作为标准 for 循环的替代方法,可以使用 C# foreach 关键字循环访问传入的字符串数组。下面是一些示例用法(但同样,您将在本章后面看到循环构造的细节):

// Notice you have no need to check the size of the array when using "foreach".
// 请注意,使用 “foreach” 时无需检查数组的大小。
// Process any incoming args using foreach.
// 使用 foreach 处理任何传入的参数。
foreach (string arg in args)
{
    Console.WriteLine("Arg: {0}", arg);
}
Console.ReadLine();
return 0;

Finally, you are also able to access command-line arguments using the static GetCommandLineArgs()method of the System.Environment type. The return value of this method is an array of strings. The first entry holds the name of the application itself, while the remaining elements in the array contain the individual command-line arguments.
最后,您还可以使用静态 GetCommandLineArgs() 访问命令行参数系统环境类型的方法。此方法的返回值是字符串s 的数组。第一个条目保存应用程序本身的名称,而数组中的其余元素包含各个命令行参数。

// Get arguments using System.Environment.
// 使用 System.Environment 获取参数。
string[] theArgs = Environment.GetCommandLineArgs();
foreach(string arg in theArgs)
{
Console.WriteLine("Arg: {0}", arg);
}
Console.ReadLine();
return 0;

■ Note the GetCommandLineArgs method does not receive the arguments for the application through the Main() method and does not depend on the string[] args parameter.
注意 GetCommandLineArgs 方法不会通过main() 方法,不依赖于 string[] args 参数。

Of course, it is up to you to determine which command-line arguments your program will respond to (if any) and how they must be formatted (such as with a - or / prefix). Here, I simply passed in a series of options that were printed directly to the command prompt. Assume, however, you were creating a newvideo game and programmed your application to process an option named -godmode. If the user starts your application with the flag, you know he is, in fact, a cheater, and you can take an appropriate course of action.
当然,由您决定程序将响应哪些命令行参数(如果有)以及如何格式化它们(例如使用 - 或 / 前缀)。在这里,我只是传入了一系列直接打印到命令提示符的选项。但是,假设您正在创建一个新的视频游戏,并对应用程序进行了编程,以处理名为 -godmode 的选项。如果用户使用标志启动应用程序,您就知道他实际上是一个作弊者,您可以采取适当的操作过程。

Specifying Command-Line Arguments with Visual Studio 2022

使用 Visual Studio 2022 指定命令行参数

In the real world, an end user has the option of supplying command-line arguments when starting a program. However, during the development cycle, you might want to specify possible command-line flags for testing purposes. To do so with Visual Studio, right-click the project name in Solution Explorer, select Properties, and then navigate to the Debug tab on the left side. From there, open the new Launch Profile UI, specify the values using the Command Line Arguments text box (see Figure 3-1), and save your changes.
在现实世界中,最终用户可以选择在启动程序时提供命令行参数。但是,在开发周期中,您可能希望指定可能的命令行标志以进行测试。若要使用 Visual Studio 执行此操作,请在“解决方案资源管理器”中右键单击项目名称,选择“属性”,然后导航到左侧的“调试”选项卡。在此处,打开新的启动配置文件 UI,使用命令行参数文本框指定值(请参阅图 3-1),然后保存更改。

Alt text

Figure 3-1. Setting application arguments in Visual Studio
图 3-1。 在 Visual Studio 中设置应用程序参数

After you have established such command-line arguments, they will automatically be passed to the Main() method when debugging or running your application within the Visual Studio IDE.
建立此类命令行参数后,它们将自动传递给在 Visual Studio IDE 中调试或运行应用程序时的 Main() 方法。

Additional Members of the System.Environment Class (Updated 10.0)

System.Environment 类的其他成员(10.0 更新)

The Environment class exposes a number of extremely helpful methods beyond GetCommandLineArgs(). Specifically, this class allows you to obtain a number of details regarding the operating system currently hosting your .NET 6 application using various static members. To illustrate the usefulness of System.Environment, update your code to call a local method named ShowEnvironmentDetails().
环境类公开了许多除了 GetCommandLineArgs() 之外非常有用的方法。具体而言,此类允许您使用各种静态成员获取有关当前承载 .NET 6 应用程序的操作系统的许多详细信息。为了说明系统的有用性。环境中,更新代码以调用名为 ShowEnvironmentDetails() 的本地方法。

// Local method within the Top-level statements. 
// 顶级语句中的本地方法。 

ShowEnvironmentDetails();

Console.ReadLine(); 
return -1;

Implement this method after your top-level statements to call various members of the Environment type:
在顶级语句之后实现此方法,以调用环境类型:

static void ShowEnvironmentDetails()
{
    // Print out the drives on this machine,
    // 打印出此机器上的驱动器
    // and other interesting details.
    // 以及其他有趣的细节。
    foreach (string drive in Environment.GetLogicalDrives())
    {
        Console.WriteLine("Drive: {0}", drive);
    }
    Console.WriteLine("OS: {0}", Environment.OSVersion);
    Console.WriteLine("Number of processors: {0}",Environment.ProcessorCount);
    Console.WriteLine(".NET Core Version: {0}",Environment.Version);
}

The following output shows a possible test run of invoking this method:
以下输出显示了调用此方法的可能测试运行:

***** My First C# App ***** Hello World!

Drive: C:\
OS: Microsoft Windows NT 10.0.19042.0 Number of processors: 16
.NET Core Version: 6.0.0

The Environment type defines members other than those shown in the previous example. Table 3-1 documents some additional properties of interest; however, be sure to check out the online documentation for full details.
环境类型定义上一个示例中所示的成员以外的成员。表 3-1 记录了一些感兴趣的其他属性;但是,请务必查看在线文档以获取完整详细信息。

Table 3-1. Select Properties of System.Environment

Property Meaning in Life
ExitCode
退出代码
Gets or sets the exit code for the application
获取或设置应用程序的退出代码
Is64BitOperatingSystem
Is64位操作系统
Returns a bool to represent whether the host machine is running a 64-bit OS
返回一个 bool 以表示主机是否正在运行 64 位操作系统
MachineName
计算机名称
Gets the name of the current machine
获取当前计算机的名称
NewLine
换行符
Gets the newline symbol for the current environment
获取当前环境的换行符
ProcessId (new in 10.0)
进程 ID(10.0 版)
Gets the unique identifier of the current process
获取当前进程的唯一标识符
ProcessPath (new in 10.0)
进程路径(10.0 版)
Returns the path of the executable that started the currently executing process; returns null when the path is not available
返回启动当前正在执行的进程的可执行文件的路径;当路径不可用时返回 null
SystemDirectory
系统目录
Returns the full path to the system directory
返回系统目录的完整路径
UserName
用户名
Returns the name of the user that started this application
返回启动此应用程序的用户的名称
Version版本 Returns a Version object that represents the version of the .NET Core platform
返回一个 Version 对象,该对象表示 .NET Core 平台的版本

Using the System.Console Class

返回一个 Version 对象,该对象表示 .NET Core 平台的版本

Almost all the example applications created over the course of the initial chapters of this book make extensive use of the System.Console class. While it is true that a console user interface (CUI) may not be as enticing as a graphical user interface (GUI) or web application, restricting the early examples to console programs will allow you to keep focused on the syntax of C# and the core aspects of the .NET 6 platform, rather than dealing with the complexities of building desktop GUIs or websites.
在本书的前几章中创建的几乎所有示例应用程序都广泛使用了 System.Console 类。虽然控制台用户界面 (CUI) 确实可能不如图形用户界面 (GUI) 或 Web 应用程序那么吸引人,但将早期示例限制为控制台程序将使您能够专注于 C# 的语法和 .NET 6 平台的核心方面,而不是处理构建桌面 GUI 或网站的复杂性。

■ Note access to the Console class is now implicitly provided by the global using statements provided by .net 6, negating the need to add in the using System; statement that was required in previous versions of C#/.net.
注意 对控制台类的访问现在由 提供的全局 using 语句隐式提供.net 6,无需添加使用系统; 以前版本的 C#/.net 中需要的语句。

As its name implies, the Console class encapsulates input, output, and error-stream manipulations for console-based applications. Table 3-2 lists some (but definitely not all) members of interest. As you can see, the Console class does provide some members that can spice up a simple command-line application, such as the ability to change background and foreground colors and issue beep noises (in a variety of frequencies!).
顾名思义,Console 类封装了基于控制台的应用程序的输入、输出和错误流操作。表 3-2 列出了一些(但绝对不是全部)感兴趣的成员。如您所见,Console 类确实提供了一些可以为简单的命令行应用程序增添趣味的成员,例如更改背景和前景色以及发出蜂鸣声(以各种频率!

Table 3-2. Select Members of System.Console
表 3-2. 选择系统控制台的成员

Member Meaning in Life
Beep() This method forces the console to emit a beep of a specified frequency and duration.
此方法强制主机发出指定频率和持续时间的蜂鸣声。
BackgroundColor These properties set the background/foreground colors for the current output.
这些属性设置当前输出的背景色/前景色。
ForegroundColor They may be assigned any member of the ConsoleColor enumeration.
可以为它们分配控制台颜色枚举的任何成员。
BufferHeight
BufferWidth
These properties control the height/width of the console’s buffer area.
这些属性控制控制台缓冲区的高度/宽度。
Title This property gets or sets the title of the current console.
此属性获取或设置当前控制台的标题。
WindowHeight
WindowWidth
WindowTop
WindowLeft
These properties control the dimensions of the console in relation to the established buffer.
这些属性控制控制台相对于已建立缓冲区的尺寸。
Clear() This method clears the established buffer and console display area.
此方法清除已建立的缓冲区和控制台显示区域。

Performing Basic Input and Output (I/O) with the Console Class

使用控制台类执行基本输入和输出 (I/O)

In addition to the members in Table 3-2, the Console type defines a set of methods to capture input and output, all of which are static and are, therefore, called by prefixing the name of the class (Console) to the method name. As you have seen, WriteLine() pumps a text string (including a carriage return) to the output stream. The Write() method pumps text to the output stream without a carriage return. ReadLine() allows you to receive information from the input stream up until the Enter key is pressed, while Read() is used to capture a single character from the input stream. 除了表 3-2 中的成员之外,控制台类型还定义了一组用于捕获输入和输出的方法,所有这些方法都是静态的,因此通过在方法名称前面加上类的名称 (Console) 来调用。如您所见,WriteLine() 将文本字符串(包括回车符)泵送到输出流中。Write() 方法将文本泵送到输出流,而不带回车符。ReadLine() 允许您从输入流接收信息,直到按下 Enter 键,而 Read() 用于从输入流中捕获单个字符。

To illustrate simple I/O using the Console class, create a new Console Application project named BasicConsoleIO and add it to your solution with these CLI commands:
若要说明使用控制台类的简单 I/O,请创建一个名为 BasicConsoleIO 的新控制台应用程序项目,并使用以下 CLI 命令将其添加到解决方案中:

 

dotnet new console -lang c# -n BasicConsoleIO -o .\BasicConsoleIO -f net6.0 
dotnet sln .\Chapter3_AllProjects.sln add .\BasicConsoleIO

Replace the Program.cs code with the following:
将程序.cs代码替换为以下内容:

Console.WriteLine("***** Basic Console I/O *****");
GetUserData();
Console.ReadLine();
static void GetUserData()
{
}

■ Note Visual studio and Visual studio Code both support a number of “code snippets” that will insert code once activated. the cw code snippet is quite useful during the early chapters of this text, in that it will automatically expand to Console.WriteLine()! to test this for yourself, type in cw somewhere within your code and hit the tab key. note: in Visual studio Code, you hit the tab key once; in Visual studio, you must hit the tab key twice.
注意 Visual Studio 和 Visual Studio Code 都支持许多“代码片段”,一旦激活,这些代码片段就会插入代码。CW 代码片段在本文的前几章中非常有用,因为它将自动展开到 Console.WriteLine()!若要自己对此进行测试,请在代码中的某处键入 CW,然后按 Tab 键。注意:在Visual Studio Code中,您按一次Tab键;在 Visual Studio 中,必须按两次 Tab 键。

Implement this method after the top-level statements with logic that prompts the user for some bits of information and echoes each item to the standard output stream. For example, you could ask the user for a name and age (which will be treated as a text value for simplicity, rather than the expected numerical value), as follows: 在顶级语句之后实现此方法,其逻辑提示用户输入一些信息位并将每个项目回显到标准输出流。例如,您可以要求用户提供姓名和年龄(为简单起见,将被视为文本值,而不是预期的数值),如下所示:

Console.WriteLine("***** Basic Console I/O *****");
GetUserData();
Console.ReadLine();

static void GetUserData()
{
    // Get name and age.
    // 获取姓名和年龄。
    Console.Write("Please enter your name: ");
    string userName = Console.ReadLine();
    Console.Write("Please enter your age: ");
    string userAge = Console.ReadLine();

    // Change echo color, just for fun.
    // 更改回声颜色,只是为了好玩。
    ConsoleColor prevColor = Console.ForegroundColor;
    Console.ForegroundColor = ConsoleColor.Yellow;

    // Echo to the console.
    // 回显到控制台。
    Console.WriteLine("Hello {0}! You are {1} years old.",userName, userAge);

    // Restore previous color. 
    //恢复以前的颜色。
    Console.ForegroundColor = prevColor;
}

Not surprisingly, when you run this application, the input data is printed to the console (using a custom color to boot!).
毫不奇怪,当您运行此应用程序时,输入数据将打印到控制台(使用自定义颜色启动!

Formatting Console Output

格式化控制台输出

During these first few chapters, you might have noticed numerous occurrences of tokens such as {0} and {1} embedded within various string literals. The .NET 6 platform supports a style of string formatting slightly akin to the printf() statement of C. Simply put, when you are defining a string literal that contains segments of data whose value is not known until runtime, you are able to specify a placeholder within the string literal using this curly-bracket syntax. At runtime, the values passed into Console.WriteLine() are substituted for each placeholder.
在前几章中,您可能已经注意到许多标记的出现,例如 {0} 和{1}嵌入在各种字符串文本中。.NET 6 平台支持一种字符串格式样式,略微类似于 C 的 printf() 语句。简而言之,当您定义包含其值在运行时之前未知的数据段的字符串文本时,您可以使用此大括号语法在字符串文本中指定占位符。在运行时,传递给 Console.WriteLine() 的值将替换为每个占位符。

The first parameter to WriteLine() represents a string literal that contains optional placeholders designated by {0}, {1}, {2}, and so forth. Be aware that the first ordinal number of a curly-bracket placeholder always begins with 0. The remaining parameters to WriteLine() are simply the values to be inserted into the respective placeholders.
WriteLine() 的第一个参数表示一个字符串文本,其中包含由 {0}、{1}、{2}等指定的可选占位符。请注意,大括号占位符的第一个序号始终以 0 开头。WriteLine() 的其余参数只是要插入到相应占位符中的值。

■ Note if you have more uniquely numbered curly-bracket placeholders than fill arguments, you will receive a format exception at runtime. however, if you have more fill arguments than placeholders, the unused fill arguments are ignored.
注意 如果唯一编号的大括号占位符多于填充参数,则在运行时会收到格式异常。 但是,如果填充参数多于占位符,则忽略未使用的填充参数。

It is permissible for a given placeholder to repeat within a given string. For example, if you are a Beatles fan and want to build the string "9, Number 9, Number 9", you would write this:
允许给定占位符在给定字符串中重复。例如,如果你是披头士乐队的粉丝,想要构建字符串“9,数字9,数字9”,你可以这样写:

// John says... 
// 约翰 说...
Console.WriteLine("{0}, Number {0}, Number {0}", 9);

Also, know that it is possible to position each placeholder in any location within a string literal, and it need not follow an increasing sequence. For example, consider the following code snippet: 另外,请注意,可以将每个占位符放置在字符串文本中的任何位置,并且不需要遵循递增顺序。例如,请考虑以下代码片段:

// Prints: 20, 10, 30
// 打印: 20, 10, 30
Console.WriteLine("{1}, {0}, {2}", 10, 20, 30);

Strings can also be formatted using string interpolation, which is covered later in this chapter.
还可以使用字符串内插来格式化字符串,本章稍后将对此进行介绍。

Formatting Numerical Data

格式化数值数据

If you require more elaborate formatting for numerical data, each placeholder can optionally contain various format characters. Table 3-3 shows the most common formatting options.
如果需要对数值数据进行更精细的格式设置,则每个占位符都可以选择包含各种格式字符。表 3-3 显示了最常见的格式设置选项。

Table 3-3. .NET Core Numerical Format Characters
表 3-3. .NET 核心数字格式字符

String FormatCharacter Meaning in Life
C or c Used to format currency. By default, the flag will prefix the local cultural symbol (a dollar sign [$] for US English).
用于格式化货币。默认情况下,该标志将作为当地文化符号的前缀(美元符号 [$] 表示美国英语)。
D or d Used to format decimal numbers. This flag may also specify the minimum number of digits used to pad the value.
用于设置十进制数字的格式。此标志还可以指定用于填充值的最小位数。
E or e Used for exponential notation. Casing controls whether the exponential constant is uppercase (E) or lowercase (e).
用于指数表示法。大小写控制指数常量是大写 (E) 还是小写 (e)。
F or f Used for fixed-point formatting. This flag may also specify the minimum number of digits used to pad the value.
用于定点格式。此标志还可以指定用于填充值的最小位数。
G or g Stands for general. This character can be used to format a number to fixed or exponential format.
代表 一般。此字符可用于将数字格式设置为固定或指数格式。
N or n Used for basic numerical formatting (with commas).
用于基本数字格式(带逗号)。
X or x Used for hexadecimal formatting. If you use an uppercase X, your hex format will also contain uppercase characters.
用于十六进制格式。如果使用大写 X,则十六进制格式也将包含大写字符。

These format characters are suffixed to a given placeholder value using the colon token (e.g.,{0:C}, {1:d}, {2:X}). To illustrate, update the top-level statements to call a new helper function named FormatNumericalData(). Implement this method in your Program.cs file to format a fixed numerical value in a variety of ways.
这些格式字符使用冒号标记后缀为给定的占位符值(例如,{0:C} , {1:d}, {2:X}).为了说明这一点,请更新顶级语句以调用名为 FormatNumericalData() 的新帮助程序函数。在 Program.cs 文件中实现此方法,以多种方式格式化固定数值。

FormatNumericalData();

// Now make use of some format tags.
// 现在使用一些格式标签。
static void FormatNumericalData()
{
    Console.WriteLine("The value 99999 in various formats:"); 
    Console.WriteLine("c format: {0:c}", 99999);
    Console.WriteLine("d9 format: {0:d9}", 99999); 
    Console.WriteLine("f3 format: {0:f3}", 99999);
    Console.WriteLine("n format: {0:n}", 99999);

    // Notice that upper- or lowercasing for hex 
    // 请注意十六进制的大写或小写
    // determines if letters are upper- or lowercase.
    // 确定字母是大写还是小写。
    Console.WriteLine("E format: {0:E}", 99999); 
    Console.WriteLine("e format: {0:e}", 99999); 
    Console.WriteLine("X format: {0:X}", 99999); 
    Console.WriteLine("x format: {0:x}", 99999);
}

The following output shows the result of calling the FormatNumericalData() method:
以下输出显示了调用 FormatNumericalData() 方法的结果:

The value 99999 in various formats:

c format: $99,999.00 d9 format: 000099999
f3 format: 99999.000
n format: 99,999.00
E format: 9.999900E+004
e format: 9.999900e+004 X format: 1869F
x format: 1869f

You will see additional formatting examples where required throughout this text; however, if you are interested in digging into string formatting further, look up the topic “Formatting Types” within the .NET Core documentation.
您将在本文中根据需要看到其他格式示例;但是,如果您有兴趣进一步了解字符串格式设置,请在 .NET Core 文档中查找主题“格式设置类型”。

Formatting Numerical Data Beyond Console Applications

设置控制台应用程序之外的数字数据格式

On a final note, be aware that the use of the string formatting characters is not limited to console programs. This same formatting syntax can be used when calling the static string.Format() method. This can be helpful when you need to compose textual data at runtime for use in any application type (e.g., desktop GUI app, ASP.NET web app, etc.).
最后,请注意,字符串格式字符的使用不仅限于控制台程序。调用静态字符串时,可以使用相同的格式语法。格式() 方法。当您需要在运行时撰写文本数据以用于任何应用程序类型(例如,桌面 GUI 应用程序、ASP.NET Web 应用程序等)时,这会很有帮助。

The string.Format() method returns a new string object, which is formatted according to the provided flags. The following code formats a string in hex:
The string.Format()方法返回一个新的字符串对象,该对象根据提供的标志进行格式化。以下代码以十六进制格式设置字符串的格式:

// Using string.Format() to format a string literal. 
// 使用string.Format()格式化字符串文本。
string userMessage = string.Format("100000 in hex is {0:x}", 100000);

Working with System Data Types and Corresponding C# Keywords

使用系统数据类型和相应的 C# 关键字

Like any programming language, C# defines keywords for fundamental data types, which are used to represent local variables, class data member variables, method return values, and parameters. Unlike other programming languages, however, these keywords are much more than simple compiler-recognized

tokens. Rather, the C# data type keywords are actually shorthand notations for full-blown types in the System namespace. Table 3-4 lists each system data type, its range, the corresponding C# keyword, and the type’s compliance with the Common Language Specification (CLS). All of the system types are in the System namespace, left off the chart for readability. 与任何编程语言一样,C# 为基本数据类型定义关键字,这些关键字用于表示局部变量、类数据成员变量、方法返回值和参数。然而,与其他编程语言不同,这些关键字不仅仅是简单的编译器识别的

令 牌。相反,C# 数据类型关键字实际上是 System 命名空间中成熟类型的简写表示法。表 3-4 列出了每种系统数据类型、其范围、相应的 C# 关键字以及类型是否符合公共语言规范 (CLS)。所有系统类型都位于 System 命名空间中,为了便于阅读,图表中未显示。

Table 3-4. The Intrinsic Data Types of C#

C# Shorthand CLS Compliant? System Type Range Meaning in Life
bool  Yes  Boolean  true or false  Represents truth or falsity
代表真假
sbyte  No  SByte  –128 to 127  Signed 8-bit number
有符号的 8 位数字
byte  Yes  Byte  0 to 255  Unsigned 8-bit number
无符号 8 位数字
short  Yes  Int16  –32,768 to 32,767  Signed 16-bit number
有符号的 16 位数字
ushort  No  UInt16  0 to 65,535  Unsigned 16-bit number
无符号 16 位数字
int  Yes  Int32  –2,147,483,648 to 2,147,483,647  Signed 32-bit number
有符号的 32 位数字
uint  No  UInt32  0 to 4,294,967,295  Unsigned 32-bit number
无符号 32 位数字
long  Yes  Int64  –9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 Signed 64-bit to number
有符号的 64 位数字
ulong  No  UInt64  0 to 18,446,744,073,709,551,615  Unsigned 64-bit number
无符号 64 位数字
char  Yes  Char  U+0000 to U+ffff  Single 16-bit Unicode character
单个 16 位 Unicode 字符
float  Yes  Single  –3.4 1038 to +3.4 1038  32-bit floating-point number
32 位浮点数
double  Yes  Double  ±5.0 10–324 to ±1.7 10308  64-bit floating-point number
64 位浮点数
decimal  Yes  Decimal  (–7.9 x 1028 to 7.9 x 1028)/(100 to 28)  128-bit signed number
128 位有符号号码
string  Yes  String  Limited by system memory  Represents a set of Unicode charactersv
object  Yes  Object  Can store any data type in an object variable  The base class of all types in the .NET universe
.NET 领域中所有类型的基类

■ Note recall from Chapter 1 that CLs-compliant .net Core code can be used by any other .net programming language. if you expose non-CLs-compliant data from your programs, other .net languages might not be able to make use of it.
请注意第 1 章中的回顾,符合 CL 的 .net Core 代码可以由任何其他 .net 编程语言使用。 如果从程序中公开不符合 CL 的数据,则其他 .NET 语言可能无法使用它。

Understanding Variable Declaration and Initialization

了解变量声明和初始化

When you are declaring a local variable (e.g., a variable within a member scope), you do so by specifying the data type, followed by the variable’s name. To begin, create a new Console Application project named BasicDataTypes and add it into the solution using these commands:
声明局部变量(例如,成员范围内的变量)时,可以通过指定数据类型,后跟变量的名称来实现。首先,创建一个名为 BasicDataTypes 的新控制台应用程序项目,并使用以下命令将其添加到解决方案中:

dotnet new console -lang c# -n BasicDataTypes -o .\BasicDataTypes -f net6.0 
dotnet sln .\Chapter3_AllProjects.sln add .\BasicDataTypes

Update the code to the following:
将代码更新为以下内容:

using System.Numerics;
Console.WriteLine("***** Fun with Basic Data Types *****");

Now, add the following static local function and call it from the top-level statements:
现在,添加以下静态本地函数并从顶级语句调用它:

using System.Numerics;
Console.WriteLine("***** Fun with Basic Data Types *****");

static void LocalVarDeclarations()
{
    Console.WriteLine("=> Data Declarations:");
    // Local variables are declared as so:
    // 局部变量声明如下:
    // dataType varName;
    // 数据类型变量名称;
    int myInt; 
    string myString; Console.WriteLine();
}

Be aware that it is a compiler error to make use of a local variable before assigning an initial value. Given this, it is good practice to assign an initial value to your local data points at the time of declaration. You may do so on a single line or by separating the declaration and assignment into two code statements.
请注意,在分配初始值之前使用局部变量是编译器错误。鉴于此,最好在声明时为本地数据点分配初始值。可以在一行上执行此操作,也可以通过将声明和赋值分隔为两个代码语句来执行此操作。

static void LocalVarDeclarations()
{
    Console.WriteLine("=> Data Declarations:");
    // Local variables are declared and initialized as follows: 
    // 局部变量的声明和初始化如下:
    // dataType varName = initialValue;
    // 数据类型变量名称;
    int myInt = 0;

    // You can also declare and assign on two lines. 
    //  您还可以在两行上声明和赋值。
    string myString;
    myString = "This is my character data";
    Console.WriteLine();
}

It is also permissible to declare multiple variables of the same underlying type on a single line of code, as in the following three bool variables:
还允许在一行代码上声明相同基础类型的多个变量,如以下三个布尔变量所示:

static void LocalVarDeclarations()
{
    Console.WriteLine("=> Data Declarations:");
    int myInt = 0;
    string myString;
    myString = "This is my character data";
    // Declare 3 bools on a single line.
    // 在一行上声明 3 个布尔值。
    bool b1 = true, b2 = false, b3 = b1;
    Console.WriteLine();
}

Since the C# bool keyword is simply a shorthand notation for the System.Boolean structure, it is also possible to allocate any data type using its full name (of course, the same point holds true for any C# data type keyword). Here is the final implementation of LocalVarDeclarations(), which illustrates various ways to declare a local variable:
由于 C# bool 关键字只是 System.Boolean 结构的简写表示法,因此也可以使用其全名分配任何数据类型(当然,对于任何 C# 数据类型关键字也是如此)。下面是 LocalVarDeclarations() 的最终实现,它说明了声明局部变量的各种方法:

static void LocalVarDeclarations()
{
    Console.WriteLine("=> Data Declarations:");
    // Local variables are declared and initialized as follows:
    // 局部变量的声明和初始化如下:
    // dataType varName = initialValue;
    // 数据类型变量名称 = 初始值;
    int myInt = 0;
    string myString;
    myString = "This is my character data";

    // Declare 3 bools on a single line.
    //在一行上声明 3 个布尔值。
    bool b1 = true, b2 = false, b3 = b1;

    // Use System.Boolean data type to declare a bool.
    // 使用 System.Boolean 数据类型声明布尔值。
    System.Boolean b4 = false;
    Console.WriteLine("Your data: {0}, {1}, {2}, {3}, {4}, {5}",
    myInt, myString, b1, b2, b3, b4);
    Console.WriteLine();
}

The default Literal (New 7.1)

默认文本(新 7.1)

The default literal assigns a variable the default value for its data type. This works for standard data types as well as custom classes (Chapter 5) and generic types (Chapter 10). Create a new method named DefaultDeclarations() and add the following code:
默认文本为变量分配其数据类型的默认值。这适用于标准数据类型以及自定义类(第 5 章)和泛型类型(第 10 章)。创建一个名为 DefaultDeclarations() 的新方法并添加以下代码:

static void DefaultDeclarations()
{
    Console.WriteLine("=> Default Declarations:");
    int myInt = default;
    Console.WriteLine(myInt);
}

Using Intrinsic Data Types and the new Operator (Updated 9.0)

使用内部数据类型和新运算符(9.0 更新)

All intrinsic data types support what is known as a default constructor (see Chapter 5). This feature allows you to create a variable using the new keyword, which automatically sets the variable to its default value:
所有内部数据类型都支持所谓的默认构造函数(请参阅第 5 章)。此功能允许您使用 new 关键字创建变量,该关键字会自动将变量设置为其默认值:

  • bool variables are set to false.
    布尔变量设置为 false。
  • Numeric data is set to 0 (or 0.0 in the case of floating-point data types).
    数值数据设置为 0(如果是浮点数据类型,则设置为 0.0)。
  • char variables are set to a single empty character.
    字符变量设置为单个空字符。
  • BigInteger variables are set to 0.
    BigInteger 变量设置为 0。
  • DateTime variables are set to 1/1/0001 12:00:00 AM.
    日期时间变量设置为 1/1/0001 12:00:00 AM。
  • Object references (including strings) are set to null.
    对象引用(包括字符串s)设置为 null。

■ Note the BigInteger data type mentioned in the previous list will be explained in just a bit.
请注意,上一个列表中提到的 BigInteger 数据类型将在稍作说明。

Although it is more cumbersome to use the new keyword when creating a basic data type variable, the following is syntactically well-formed C# code:
尽管在创建基本数据类型变量时使用 new 关键字比较麻烦,但以下是语法格式良好的 C# 代码:

static void NewingDataTypes()
{
    Console.WriteLine("=> Using new to create variables:");
    bool b = new bool(); // Set to false.
    int i = new int(); // Set to 0.
    double d = new double(); // Set to 0.
    DateTime dt = new DateTime(); // Set to 1/1/0001 12:00:00 AM
    Console.WriteLine("{0}, {1}, {2}, {3}", b, i, d, dt);
    Console.WriteLine();
}

C# 9.0 added a shortcut for creating variable instances. This shortcut is simply using the keyword new() without the data type. The updated version of NewingDataTypes is shown here:
C# 9.0 添加了用于创建变量实例的快捷方式。此快捷方式只是使用关键字 new()没有数据类型.NewingDataTypes 的更新版本如下所示:

static void NewingDataTypesWith9()
{
    Console.WriteLine("=> Using new to create variables:");
    bool b = new();             // Set to false.
    int i = new();              // Set to 0.
    double d = new();           // Set to 0.
    DateTime dt = new();        // Set to 1/1/0001 12:00:00 AM
    Console.WriteLine("{0}, {1}, {2}, {3}", b, i, d, dt);
    Console.WriteLine();
}

Understanding the Data Type Class Hierarchy

了解数据类型类层次结构

It is interesting to note that even the primitive .NET data types are arranged in a class hierarchy. If you are new to the world of inheritance, you will discover the full details in Chapter 6. Until then, just understand that types at the top of a class hierarchy provide some default behaviors that are granted to the derived types. The relationship between these core system types can be understood as shown in Figure 3-2.
有趣的是,即使是基元 .NET 数据类型也排列在类层次结构中。如果你是继承世界的新手,你将在第6章中发现全部细节。在此之前,只需了解类层次结构顶部的类型提供一些授予派生类型的默认行为。这些核心系统类型之间的关系可以理解为如图3-2所示。

Alt text

Figure 3-2. The class hierarchy of system types
图 3-2。 系统类型的类层次结构

Notice that each type ultimately derives from System.Object, which defines a set of methods (e.g., ToString(), Equals(), GetHashCode()) common to all types in the .NET Core base class libraries (these methods are fully detailed in Chapter 6).
请注意,每种类型最终都派生自 System.Object,它定义了一组 .NET Core 基类库中所有类型通用的方法(例如,ToString()、Equals()、GetHashCode())(这些方法在第 6 章中有详细说明)。

Also note that many numerical data types derive from a class named System.ValueType. Descendants of ValueType are automatically allocated on the stack and, therefore, have a predictable lifetime and are quite efficient. On the other hand, types that do not have System.ValueType in their inheritance chain (such as System.Type, System.String, System.Array, System.Exception, and System.Delegate) are not allocated on the stack but on the garbage-collected heap. (You can find more information on this distinction in Chapter 4.)
另请注意,许多数值数据类型派生自名为 System.ValueType 的类。ValueType 的后代在堆栈上自动分配,因此具有可预测的生存期并且非常高效。另一方面,在其继承链中没有 System.ValueType 的类型(如 System.Type、System.String、System.Array、System.Exception 和 System.Delegate)不是在堆栈上分配,但在垃圾收集堆上分配。(您可以在第 4 章中找到有关此区别的更多信息。

Without getting too hung up on the details of System.Object and System.ValueType, just understand that because a C# keyword (such as int) is simply shorthand notation for the corresponding system type (in this case, System.Int32), the following is perfectly legal syntax, given that System.Int32 (the C# int) eventually derives from System.Object and, therefore, can invoke any of its public members, as illustrated by this additional helper function:
无需过多关注System.Object和System.ValueType的细节,只需了解因为C#关键字(如int)只是相应系统类型(在本例中为System.Int32)的简写表示法,因此以下是完全合法的语法,因为System.Int32(C# int)最终派生自System.Object 因此,可以调用其任何公共成员,如以下附加帮助程序函数所示:


static void ObjectFunctionality()
{
    Console.WriteLine("=> System.Object Functionality:");
    // A C# int is really a shorthand for System.Int32,
    //  C# int 实际上是 System.Int32 的简写
    // which inherits the following members from System.Object.
    // 它从 System.Object 继承以下成员。
    Console.WriteLine("12.GetHashCode() = {0}", 12.GetHashCode());
    Console.WriteLine("12.Equals(23) = {0}", 12.Equals(23));
    Console.WriteLine("12.ToString() = {0}", 12.ToString());
    Console.WriteLine("12.GetType() = {0}", 12.GetType());
    Console.WriteLine();
}

If you were to call this method from within the top-level statements, you would find the output shown here:
如果要从顶级语句中调用此方法,则会找到如下所示的输出:

=> System.Object Functionality:
12.GetHashCode() = 12
12.Equals(23) = False
12.ToString() = 12
12.GetType() = System.Int32

Understanding the Members of Numerical Data Types

了解数值数据类型的成员

To continue experimenting with the intrinsic C# data types, understand that the numerical types of .NET Core support MaxValue and MinValue properties that provide information regarding the range a given type can store. In addition to the MinValue/MaxValue properties, a given numerical system type may define further useful members. For example, the System.Double type allows you to obtain the values for epsilon and infinity (which might be of interest to those of you with a flair for mathematics). To illustrate, consider the following helper function:
若要继续试验内部 C# 数据类型,请了解 .NET Core 的数值类型支持 MaxValue 和 MinValue 属性,这些属性提供有关给定类型可以存储的范围的信息。除了 MinValue/MaxValue 属性之外,给定的数值系统类型还可以定义更多有用的成员。例如,System.Double 类型允许您获取 epsilon 和无穷大的值(那些具有数学天赋的人可能会对此感兴趣)。为了说明这一点,请考虑以下帮助程序函数:

static void DataTypeFunctionality()
{
    Console.WriteLine("=> Data type Functionality:");
    Console.WriteLine("Max of int: {0}", int.MaxValue);
    Console.WriteLine("Min of int: {0}", int.MinValue);
    Console.WriteLine("Max of double: {0}", double.MaxValue);
    Console.WriteLine("Min of double: {0}", double.MinValue);
    Console.WriteLine("double.Epsilon: {0}", double.Epsilon);
    Console.WriteLine("double.PositiveInfinity: {0}",
    double.PositiveInfinity);
    Console.WriteLine("double.NegativeInfinity: {0}",
    double.NegativeInfinity);
    Console.WriteLine();
}

When you define a literal whole number (such as 500), the runtime will default the data type to an int. Likewise, literal floating-point data (such as 55.333) will default to a double. To set the underlying data type to a long, use suffix l or L (4L). To declare a float variable, use the suffix f or F to the raw numerical value (5.3F), and use the suffix m or M to a floating-point number to declare a decimal (300.5M). This becomes more important when declaring variables implicitly, which is covered later in this chapter.
定义文本整数(如 500)时,运行时会将数据类型默认为 int。同样,文本浮点数据(如 55.333)将默认为双精度。若要将基础数据类型设置为长整型,请使用后缀 l 或 L (4L)。若要声明浮点变量,请使用后缀 f 或 F 到原始数值 (5.3F),并使用后缀 m 或 M 到浮点数来声明十进制 (300.5M)。在隐式声明变量时,这一点变得更加重要,本章稍后将对此进行介绍。

Understanding the Members of System.Boolean

了解系统布尔值的成员

Next, consider the System.Boolean data type. The only valid assignment a C# bool can take is from the set {true || false}. Given this point, it should be clear that System.Boolean does not support a MinValue/MaxValue property set but rather TrueString/FalseString (which yields the string "True" or "False", respectively). Here is an example:
接下来,考虑 System.Boolean 数据类型。C# bool 可以接受的唯一有效赋值是从集合 {true || false} 中获取的。鉴于这一点,应该很清楚System.Boolean不支持设置了 MinValue/MaxValue 属性,但设置了 TrueString/FalseString(生成字符串“True”或分别是“假”)。下面是一个示例:

Console.WriteLine("bool.FalseString: {0}", bool.FalseString);

Console.WriteLine("bool.TrueString: {0}", bool.TrueString);

Understanding the Members of System.Char

了解 System.Char 的成员

C# textual data is represented by the string and char keywords, which are simple shorthand notations for System.String and System.Char, both of which are Unicode under the hood. As you might already know, a string represents a contiguous set of characters (e.g., "Hello"), while the char can represent a single slot in a string (e.g., 'H').
C# 文本数据由字符串和 char 关键字表示,它们是 System.String 和 System.Char 的简单速记符号,两者都是 Unicode 底层。您可能已经知道,字符串表示一组连续的字符(例如,“Hello”),而字符可以表示字符串中的单个插槽(例如,“H”)。

The System.Char type provides you with a great deal of functionality beyond the ability to hold a single point of character data. Using the static methods of System.Char, you are able to determine whether a given character is numerical, alphabetical, a point of punctuation, or whatnot. Consider the following method:
System.Char 类型为您提供了大量功能,超出了保存单点字符数据的能力。使用 System.Char 的静态方法,您可以确定给定字符是数字、字母、标点符号还是其他字符。请考虑以下方法:


static void CharFunctionality()
{
    Console.WriteLine("=> char type Functionality:");
    char myChar = 'a';
    Console.WriteLine("char.IsDigit('a'): {0}", char.IsDigit(myChar));
    Console.WriteLine("char.IsLetter('a'): {0}", char.IsLetter(myChar));
    Console.WriteLine("char.IsWhiteSpace('Hello There', 5): {0}",char.IsWhiteSpace("Hello There", 5));
    Console.WriteLine("char.IsWhiteSpace('Hello There', 6): {0}",char.IsWhiteSpace("Hello There", 6));
    Console.WriteLine("char.IsPunctuation('?'): {0}", char.IsPunctuation('?'));
    Console.WriteLine();
}

As illustrated in the previous method, many members of System.Char have two calling conventions: a single character or a string with a numerical index that specifies the position of the character to test.
如前面的方法所示,System.Char 的许多成员具有两种调用约定:单个字符或具有指定要测试的字符位置的数字索引的字符串。

Parsing Values from String Data

从字符串数据解析值

The .NET data types provide the ability to generate a variable of their underlying type given a textual equivalent (e.g., parsing). This technique can be extremely helpful when you want to convert some user input data (such as a selection from a GUI-based, drop-down list box) into a numerical value. Consider the following parsing logic within a method named ParseFromStrings():
.NET 数据类型提供了在给定文本等效项(例如,分析)的情况下生成其基础类型的变量的功能。当您想要将某些用户输入数据(例如从基于 GUI 的下拉列表框中选择的内容)转换为数值时,此技术非常有用。在名为 ParseFromStrings() 的方法中考虑以下解析逻辑:


static void ParseFromStrings()
{
    Console.WriteLine("=> Data type parsing:");
    bool b = bool.Parse("True");
    Console.WriteLine("Value of b: {0}", b);
    double d = double.Parse("99.884");
    Console.WriteLine("Value of d: {0}", d);
    int i = int.Parse("8");
    Console.WriteLine("Value of i: {0}", i);
    char c = Char.Parse("w");
    Console.WriteLine("Value of c: {0}", c);
    Console.WriteLine();
}

Using TryParse to Parse Values from String Data

使用 TryParse 从字符串数据中解析值

One issue with the preceding code is that an exception will be thrown if the string cannot be cleanly converted to the correct data type. For example, the following will fail at runtime:
上述代码的一个问题是,如果字符串无法完全转换为正确的数据类型,则会引发异常。例如,以下内容将在运行时失败:

bool b = bool.Parse("Hello");

One solution is to wrap each call to Parse() in a try-catch block (exception handling is covered in detail in Chapter 7), which can add a lot of code, or use a TryParse() statement. The TryParse() statement takes an out parameter (the out modifier is covered in detail in Chapter 4) and returns a bool if the parsing was successful. Create a new method named ParseFromStringWithTryParse() and add the following code:
一种解决方案是对 Parse() 的每个调用包装在一个 try-catch 块中(第 7 章详细介绍了异常处理),这可以添加大量代码,或使用 TryParse() 语句。 TryParse() 语句采用一个 out 参数(第 4 章详细介绍了 out 修饰符),如果解析成功,则返回一个布尔值。创建一个名为 ParseFromStringWithTryParse() 的新方法,并添加以下代码:


static void ParseFromStringsWithTryParse()
{
    Console.WriteLine("=> Data type parsing with TryParse:");
    if (bool.TryParse("True", out bool b))
    {
        Console.WriteLine("Value of b: {0}", b);
    }
    else
    {
        Console.WriteLine("Default value of b: {0}", b);
    }
    string value = "Hello";
    if (double.TryParse(value, out double d))
    {
        Console.WriteLine("Value of d: {0}", d);
    }
    else
    {
        Console.WriteLine("Failed to convert the input ({0}) to a double and the variable was assigned the default {1}", value, d);
    }
    Console.WriteLine();
}

If you are new to programming and do not know how if/else statements work, they are covered later in this chapter in detail. The important item to note from the preceding example is that if a string can be converted to the requested data type, the TryParse() method returns true and assigns the parsed value to the variable passed into the method. If the value cannot be parsed, the variable is assigned its default value, and the TryParse() method returns false.
如果您不熟悉编程并且不知道 if/else 语句的工作原理,本章稍后将详细介绍它们。前面示例中需要注意的重要事项是,如果字符串可以转换为请求的数据类型,则 TryParse() 方法返回 true 并将解析的值分配给传递给该方法的变量。如果无法解析该值,则为变量分配其默认值,并且 TryParse() 方法返回 false。

Using System.DateTime and System.TimeSpan (Updated 10.0)

使用 System.DateTime 和 System.TimeSpan(更新的 10.0)

The System namespace defines a few useful data types for which there are no C# keywords, such as the DateTime and TimeSpan structures.
System 命名空间定义了一些没有 C# 关键字的有用数据类型,例如日期时间和时间跨度结构。

The DateTime type contains data that represents a specific date (month, day, year) and time value, both of which may be formatted in a variety of ways using the supplied members. The TimeSpan structure allows you to easily define and transform units of time using various members.
DateTime 类型包含表示特定日期(月、日、年)和时间值的数据,这两者都可以使用提供的成员以多种方式设置格式。TimeSpan 结构允许您使用各种成员轻松定义和转换时间单位。


static void UseDatesAndTimes()
{
    Console.WriteLine("=> Dates and Times:");
    // This constructor takes (year, month, day).
    // 此构造函数需要(年、月、日)。
    DateTime dt = new DateTime(2015, 10, 17);

    // What day of the month is this?
    // 这是每月的哪一天?
    Console.WriteLine("The day of {0} is {1}", dt.Date, dt.DayOfWeek);

    // Month is now December.
    // 月份现在是 12 月。
    dt = dt.AddMonths(2);
    Console.WriteLine("Daylight savings: {0}", dt.IsDaylightSavingTime());

    // This constructor takes (hours, minutes, seconds).
    // 此构造函数需要(小时、分钟、秒)。
    TimeSpan ts = new TimeSpan(4, 30, 0);
    Console.WriteLine(ts);

    // Subtract 15 minutes from the current TimeSpan and
    // 从当前时间跨度中减去 15 分钟,然后
    // print the result.
    // 打印结果。
    Console.WriteLine(ts.Subtract(new TimeSpan(0, 15, 0)));
}

The DateOnly and TimeOnly structs were added in .NET 6/C# 10, and each represents half of the DateTime type. The DateOnly struct aligns with the SQL Server Date type, and the TimeOnly struct aligns with the SQL Server Time type. The following code shows the new types in action:
DateOnly 和 TimeOnly 结构是在 .NET 6/C# 10 中添加的,每个结构都表示 DateTime 类型的一半。DateOnly 结构与 SQL Server Date 类型对齐,TimeOnly 结构与 SQL Server Time 类型对齐。以下代码显示了操作中的新类型:

static void UseDatesAndTimes()
{
    Console.WriteLine("=> Dates and Times:");
    ...
    DateOnly d = new DateOnly(2021, 07, 21);
    Console.WriteLine(d);
    TimeOnly t = new TimeOnly(13, 30, 0, 0);
    Console.WriteLine(t);
}

Working with the System.Numerics Namespace

使用 System.Numerics 命名空间

The System.Numerics namespace defines a structure named BigInteger. As its name implies, the BigInteger data type can be used when you need to represent humongous numerical values, which are not constrained by a fixed upper or lower limit.
命名空间定义了一个名为 BigInteger 的结构。顾名思义,当您需要表示不受固定上限或下限约束的庞大数值时,可以使用 BigInteger 数据类型。

■ Note the System.Numerics namespace defines a second structure named Complex, which allows you to model mathematically complex numerical data (e.g., imaginary units, real data, hyperbolic tangents). Consult the .net Core documentation if you are interested.
请注意,System.Numerics 命名空间定义了名为 Complex 的第二个结构,它允许您对数学上复杂的数值数据(例如,虚数单位、实数数据、双曲正切线)进行建模。如果您有兴趣,请参阅 .net Core 文档。

While many of your .NET Core applications might never need to make use of the BigInteger structure, if you do find the need to define a massive numerical value, your first step is to add the following using directive to the file:
虽然许多 .NET Core 应用程序可能永远不需要使用 BigInteger 结构,但如果您发现需要定义大量数值,则第一步是将以下 using 指令添加到文件中:

// BigInteger lives here! BigInteger住在这里!
using System.Numerics;

At this point, you can create a BigInteger variable using the new operator. Within the constructor, you can specify a numerical value, including floating-point data. However, C# implicitly types non-floating-point numbers as an int and floating-point numbers as a double. How, then, can you set BigInteger to a massive value while not overflowing the default data types used for raw numerical values?
此时,您可以使用 new 运算符创建 BigInteger 变量。在构造函数中,可以指定数值,包括浮点数据。但是,C# 将非浮点数隐式键入为 int,将浮点数键入为双精度数。那么,如何将 BigInteger 设置为大量值,同时不溢出用于原始数值的默认数据类型?

The simplest approach is to establish the massive numerical value as a text literal, which can be converted into a BigInteger variable via the static Parse() method. If required, you can also pass in a byte array directly to the constructor of the BigInteger class.
最简单的方法是将大量数值建立为文本文字,可以通过静态 Parse() 方法将其转换为 BigInteger 变量。如果需要,还可以将字节数组直接传递给 BigInteger 类的构造函数。

■ Note after you assign a value to a BigInteger variable, you cannot change it, as the data is immutable. however, the BigInteger class defines a number of members that will return new BigInteger objects based on your data modifications (such as the static Multiply() method used in the following code sample).
注意 将值分配给 BigInteger 变量后,无法更改它,因为数据是不可变的。但是,BigInteger 类定义了许多成员,这些成员将根据您的数据修改(如以下代码示例中使用的静态 Multiply() 方法)返回新的 BigInteger 对象。

In any case, after you have defined a BigInteger variable, you will find this class defines similar members as other intrinsic C# data types (e.g., float, int). In addition, the BigInteger class defines several static members that allow you to apply basic mathematical expressions (such as adding and multiplying) to BigInteger variables. Here is an example of working with the BigInteger class:
在任何情况下,在定义 BigInteger 变量后,您会发现此类定义的成员与其他内部 C# 数据类型(例如,float、int)类似。此外,BigInteger 类定义了几个静态成员,这些成员允许您将基本数学表达式(如加法和乘法)应用于 BigInteger 变量。下面是使用 BigInteger 类的示例:

using System.Numerics;

static void UseBigInteger()
{
    Console.WriteLine("=> Use BigInteger:");
    BigInteger biggy =
    BigInteger.Parse("9999999999999999999999999999999999999999999999");
    Console.WriteLine("Value of biggy is {0}", biggy);
    Console.WriteLine("Is biggy an even value?: {0}", biggy.IsEven);
    Console.WriteLine("Is biggy a power of two?: {0}", biggy.IsPowerOfTwo);
    BigInteger reallyBig = BigInteger.Multiply(biggy,
    BigInteger.Parse("8888888888888888888888888888888888888888888"));
    Console.WriteLine("Value of reallyBig is {0}", reallyBig);
}

It is also important to note that the BigInteger data type responds to C#’s intrinsic mathematical operators, such as +, -, and . Therefore, rather than calling BigInteger.Multiply() to multiply two huge numbers, you could author the following code:
同样重要的是要注意,BigInteger 数据类型响应 C# 的内部数学运算符,如 +、- 和
。因此,与其调用 BigInteger.Multiply() 来乘以两个大数字,不如编写以下代码:

 
BigInteger reallyBig2 = biggy * reallyBig;

At this point, I hope you understand that the C# keywords representing basic data types have a corresponding type in the .NET Core base class libraries, each of which exposes a fixed functionality. While I have not detailed each member of these data types, you are in a great position to dig into the details as you see fit. Be sure to consult the .NET Core documentation for full details regarding the various .NET data types—you will likely be surprised at the amount of built-in functionality.
此时,我希望您了解表示基本数据类型的 C# 关键字在 .NET Core 基类库中具有相应的类型,每个基类库都公开一个固定的功能。虽然我没有详细说明这些数据类型的每个成员,但您可以根据需要深入了解详细信息。请务必查阅 .NET Core 文档,了解有关各种 .NET 数据类型的完整详细信息 - 您可能会对内置功能的数量感到惊讶。

Using Digit Separators (New 7.0)

使用数字分隔符(新版 7.0)

Sometimes when assigning large numbers to a numeric variable, there are more digits than the eye can keep track of. C# 7.0 introduced the underscore () as a digit separator (for integer, long, decimal, double data, or hex types). C# 7.2 allows for hex values (and the new binary literal, covered next, to start with an underscore, after the opening declaration). Here is an example of using the new digit separator:
有时,当为数值变量分配大数字时,数字多于肉眼可以跟踪的数字。C# 7.0 引入了下划线 (
) 作为数字分隔符(用于整数、长整型、十进制、双精度数据或十六进制类型)。C# 7.2 允许十六进制值(以及下面介绍的新二进制文本,在开始声明之后以下划线开头)。下面是使用新数字分隔符的示例:

static void DigitSeparators()
{
    Console.WriteLine("=> Use Digit Separators:");
    Console.Write("Integer:");
    Console.WriteLine(123_456);
    Console.Write("Long:");
    Console.WriteLine(123_456_789L);
    Console.Write("Float:");
    Console.WriteLine(123_456.1234F);
    Console.Write("Double:");
    Console.WriteLine(123_456.12);
    Console.Write("Decimal:");
    Console.WriteLine(123_456.12M);
    //Updated in 7.2, Hex can begin with _
    Console.Write("Hex:");
    Console.WriteLine(0x_00_00_FF);
}

Using Binary Literals (New 7.0/7.2)

使用二进制文本(新版 7.0/7.2)

C# 7.0 introduces a new literal for binary values, for example, for creating bit masks. The new digit separator works with binary literals, and C# 7.2 allows for binary and hex numbers to start with an underscore. Now, binary numbers can be written as you would expect. Here is an example:
C# 7.0 为二进制值引入了新的文本,例如,用于创建位掩码。新的数字分隔符适用于二进制文本,C# 7.2 允许二进制和十六进制数字以下划线开头。现在,二进制数可以按您的期望编写。下面是一个示例:

0b_0001_0000

Here is a method that shows using the new literals with the digit separator:
下面是一个显示使用带有数字分隔符的新文本的方法:

 
static void BinaryLiterals()
{
    //Updated in 7.2, Binary can begin with _
    Console.WriteLine("=> Use Binary Literals:");
    Console.WriteLine("Sixteen: {0}", 0b_0001_0000);
    Console.WriteLine("Thirty Two: {0}", 0b_0010_0000);
    Console.WriteLine("Sixty Four: {0}", 0b_0100_0000);
}

Working with String Data

使用字符串数据

System.String provides a number of methods you would expect from such a utility class, including methods that return the length of the character data, find substrings within the current string, and convert to and from uppercase/lowercase. Table 3-5 lists some (but by no means all) of the interesting members.
System.String 提供了许多期望从此类实用程序类获得的方法,包括返回字符数据长度、查找当前字符串中的子字符串以及与大写/小写相互转换的方法。表 3-5 列出了一些(但绝不是全部)有趣的成员。
Table 3-5. Select Members of System.String
表 3-5. 选择系统字符串的成员

String Member Meaning in Life
Length This property returns the length of the current string.
此属性返回当前字符串的长度。
Compare() This static method compares two strings.
此静态方法比较两个字符串。
Contains() This method determines whether a string contains a specific substring.
此方法确定字符串是否包含特定的子字符串。
Equals() This method tests whether two string objects contain identical character data.
此方法测试两个字符串对象是否包含相同的字符数据。
Format() This static method formats a string using other primitives (e.g., numerical data, other strings) and the {0} notation examined earlier in this chapter.
此静态方法使用其他基元(例如,数值数据、其他字符串)和本章前面研究的{0}表示法来格式化字符串。
Insert() This method inserts a string within a given string.
此方法在给定字符串中插入字符串。
PadLeft() \ PadRight() These methods are used to pad a string with some characters.
这些方法用于用一些字符填充字符串。
Remove() \ Replace() These methods are used to receive a copy of a string with modifications (characters removed or replaced).
这些方法用于接收经过修改(删除或替换字符)的字符串副本。
Split() This method returns a String array containing the substrings in this instance that are delimited by elements of a specified char array or string array.
此方法返回一个 String 数组,其中包含此实例中由指定 char 数组或字符串数组的元素分隔的子字符串。
Trim() This method removes all occurrences of a set of specified characters from the beginning and end of the current string.
此方法从当前字符串的开头和结尾删除一组指定字符的所有匹配项。
ToUpper() \ ToLower() These methods create a copy of the current string in uppercase or lowercase format, respectively.
这些方法分别以大写或小写格式创建当前字符串的副本。

Performing Basic String Manipulation

执行基本字符串操作

Working with the members of System.String is as you would expect. Simply declare a string variable and make use of the provided functionality via the dot operator. Be aware that a few of the members of System. String are static members and are, therefore, called at the class (rather than the object) level.
与 System.String 的成员一起工作是您所期望的。只需声明一个字符串变量,并通过点运算符使用提供的功能。请注意,系统的一些成员。字符串是静态成员,因此在类(而不是对象)级别调用。

Assume you have created a new Console Application project named FunWithStrings and added it to your solution. Clear out the existing code and add the following:
假设您已经创建了一个名为 FunWithString 的新控制台应用程序项目,并将其添加到您的解决方案中。清除现有代码并添加以下内容:

using System.Runtime.CompilerServices;
using System.Text;
// BasicStringFunctionality();
static void BasicStringFunctionality()
{
    Console.WriteLine("=> Basic String functionality:");
    string firstName = "Freddy";
    Console.WriteLine("Value of firstName: {0}", firstName);
    Console.WriteLine("firstName has {0} characters.", firstName.Length);
    Console.WriteLine("firstName in uppercase: {0}", firstName.ToUpper());
    Console.WriteLine("firstName in lowercase: {0}", firstName.ToLower());
    Console.WriteLine("firstName contains the letter y?: {0}",
    firstName.Contains("y"));
    Console.WriteLine("New first name: {0}", firstName.Replace("dy", ""));
    Console.WriteLine();
}

There is not too much to say here, as this method simply invokes various members, such as ToUpper() and Contains(), on a local string variable to yield various formats and transformations. Here is the initial output:
这里没有太多要说的,因为此方法只是调用各种成员,例如 ToUpper()和 Contains(),在局部字符串变量上生成各种格式和转换。以下是初始输出:

***** Fun with Strings *****
=> Basic String functionality:
Value of firstName: Freddy
firstName has 6 characters.
firstName in uppercase: FREDDY
firstName in lowercase: freddy
firstName contains the letter y?: True
firstName after replace: Fred

While this output might not seem too surprising, the output seen via calling the Replace() method is a bit misleading. In reality, the firstName variable has not changed at all; rather, you receive a new string in a modified format. You will revisit the immutable nature of strings in just a few moments.
虽然此输出可能看起来不太令人惊讶,但通过调用 Replace() 方法看到的输出有点误导。实际上,firstName 变量根本没有改变;相反,您会收到一个修改格式的新字符串。您将在短短几分钟内重新审视字符串的不可变性质。

Performing String Concatenation

执行字符串串联

String variables can be connected to build larger strings via the C# + (as well as +=) operator. As you might know, this technique is formally termed string concatenation. Consider the following new helper function:
字符串变量可以通过 C# +(以及 +=)运算符连接以生成更大的字符串。您可能知道,这种技术正式称为字符串串联。请考虑以下新的帮助程序函数:

static void StringConcatenation()
{
    Console.WriteLine("=> String concatenation:");
    string s1 = "Programming the ";
    string s2 = "PsychoDrill (PTP)";
    string s3 = s1 + s2;
    Console.WriteLine(s3);
    Console.WriteLine();
}

You might be interested to know that the C# + symbol is processed by the compiler to emit a call to the static String.Concat() method. Given this, it is possible to perform string concatenation by calling String. Concat() directly as shown in the following modified version of the method (although you really have not gained anything by doing so—in fact, you have incurred additional keystrokes!):
您可能有兴趣知道 C# + 符号由编译器处理以发出对静态 String.Concat() 方法的调用。鉴于此,可以通过调用 String 来执行字符串连接。Concat() 直接如以下方法的修改版本所示(尽管您这样做实际上没有任何收获 - 事实上,您已经产生了额外的击键!

static void StringConcatenation()
{
    Console.WriteLine("=> String concatenation:");
    string s1 = "Programming the ";
    string s2 = "PsychoDrill (PTP)";
    string s3 = String.Concat(s1, s2);
    Console.WriteLine(s3);
    Console.WriteLine();
}

Using Escape Characters

使用转义字符

As in other C-based languages, C# string literals may contain various escape characters, which qualify how the character data should be printed to the output stream. Each escape character begins with a backslash, followed by a specific token. In case you are a bit rusty on the meanings behind these escape characters, Table 3-6 lists the more common options.
与其他基于 C 的语言一样,C# 字符串文本可能包含各种转义字符,这些字符限定了应如何将字符数据打印到输出流。每个转义字符都以反斜杠开头,后跟一个特定的标记。如果您对这些转义字符背后的含义有点生疏,
表 3-6 列出了更常见的选项。

Table 3-6. String Literal Escape Characters
表 3-6. 字符串文本转义字符

Character Meaning in Life
\' Inserts a single quote into a string literal.
在字符串文本中插入单引号。
\" Inserts a double quote into a string literal.
在字符串文本中插入双引号。
\\ Inserts a backslash into a string literal. This can be quite helpful when defining file or network paths.
在字符串文本中插入反斜杠。这在定义文件或网络路径时非常有用。
\a Triggers a system alert (beep). For console programs, this can be an audio clue to the user.
触发系统警报(蜂鸣音)。对于控制台程序,这可以是用户的音频线索。
\n Inserts a line feed (Unix-based systems).
插入换行符(基于 Unix 的系统)。
\r\n Inserts a line feed (non-Unix-based platforms).
插入换行符(非基于 Unix 的平台)。
\r Inserts a carriage return.
插入回车符。
\t Inserts a horizontal tab into the string literal.
在字符串文本中插入水平制表符。

For example, to print a string that contains a tab between each word, you can make use of the \t escape character. Or assume you want to create a string literal that contains quotation marks, another that defines a directory path, and a final string literal that inserts three blank lines after printing the character data. To do so without compiler errors, you would need to make use of the \", \, and \n escape characters. The following method demonstrates this:
例如,若要打印每个单词之间包含一个制表符的字符串,可以使用 \t 转义字符。或者假设您要创建一个包含引号的字符串文本,另一个定义目录路径和在打印字符数据后插入三个空行的最终字符串文本。若要在不出错的情况下执行此操作,需要使用 \“、\ 和 \n 转义字符。 以下方法对此进行了演示:

static void EscapeChars()
{
    Console.WriteLine("=> Escape characters:");
    string strWithTabs = "Model\tColor\tSpeed\tPet Name ";
    Console.WriteLine(strWithTabs);
    Console.WriteLine("Everyone loves \"Hello World\" ");
    Console.WriteLine("C:\\MyApp\\bin\\Debug ");
    // Adds a total of 4 blank lines (3 escaped, 1 from WriteLine).
    // 总共添加 4 个空白行(3 个转义,1 个来自 WriteLine)。
    Console.WriteLine("All finished.\n\n\n ");
    Console.WriteLine();
}

Notice from Table 3-6 that there is a difference when creating a new line based on the operating system the code is executing on. The NewLine property of the static Environment type adds in the proper escape code(s) for adding blank lines into your text. Consider the following addition to the EscapeChars() method:
请注意,从表 3-6 中可以看出,根据执行代码的操作系统创建新行时存在差异。静态环境类型的 NewLine 属性添加了正确的转义代码,用于在文本中添加空白行。考虑对 EscapeChars() 方法的以下补充:

static void EscapeChars()
{
    // omitted for brevity
    // 为简洁起见省略
    // Adds a 4 more blank lines.
    // 再添加 4 个空行。
    Console.WriteLine("All finished for real this time.{0}{0}{0}", Environment.NewLine);
}

Performing String Interpolation

执行字符串插值

The curly-bracket syntax illustrated within this chapter ({0}, {1}, etc.) has existed within the .NET platform since version 1.0. Starting with the release of C# 6, C# programmers can use an alternative syntax to build string literals that contain placeholders for variables. Formally, this is called string interpolation. While the output of the operation is identical to traditional string formatting syntax, this new approach allows you to directly embed the variables themselves, rather than tacking them on as a comma-delimited list.
本章中说明的大括号语法({0}、{1}等)自 1.0 版起就存在于 .NET 平台中。从 C# 6 的发布开始,C# 程序员可以使用替代语法来生成包含变量占位符的字符串文本。从形式上讲,这称为字符串插值。虽然操作的输出与传统的字符串格式语法相同,但这种新方法允许您直接嵌入变量本身,而不是将它们作为逗号分隔的列表。

Consider the following additional method of your Program class (StringInterpolation()), which builds a string variable using each approach:
请考虑 Program 类的以下附加方法 (StringInterpolation()),该方法使用每种方法生成一个字符串变量:

static void StringInterpolation()
{
    Console.WriteLine("=> String interpolation:\a");
    // Some local variables we will plug into our larger string
    // 一些局部变量我们将插入到较大的字符串 int age = 4 中;
    int age = 4;
    string name = "Soren";
    // Using curly-bracket syntax.
    // 使用大括号语法。
    string greeting = string.Format("Hello {0} you are {1} years old.", name, age);
    Console.WriteLine(greeting);
    // Using string interpolation
    // 使用字符串内插
    string greeting2 = $"Hello {name} you are {age} years old.";
    Console.WriteLine(greeting2);
}

In the greeting2 variable, notice how the string you are constructing begins with a dollar sign ($)prefix.Next, notice that the curly brackets still are used to mark a variable placeholder; however, rather than using a numerical tag, you are able to place the variable directly into the scope. The assumed advantage is that this new formatting syntax is a bit easier to read in a linear (left-to-right) fashion, given that you are not required to “jump to the end” to see the list of values to plug in at runtime.
在 greeting2 变量中,请注意您构造的字符串如何以美元符号 ($) 前缀开头。接下来,请注意,大括号仍用于标记变量占位符;但是,您可以直接将变量放入作用域中,而不是使用数字标记。假定的优点是,这种新的格式语法以线性(从左到右)方式更容易阅读,因为您不需要“跳到末尾”即可查看运行时要插入的值列表。

There is another interesting aspect of this new syntax: the curly brackets used in string interpolation are a valid scope. Therefore, you can use the dot operation on the variables to change their state. Consider updates to each assembled string variable.
这种新语法还有另一个有趣的方面:字符串插值中使用的大括号是一个有效的范围。因此,您可以对变量使用点运算来更改其状态。请考虑更新每个组合的字符串变量。

string greeting = string.Format("Hello {0} you are {1} years old.", name.ToUpper(), age);
string greeting2 = $"Hello {name.ToUpper()} you are {age} years old.";

Here, I have uppercased the name via a call to ToUpper(). Do note that in the string interpolation approach, you do not add a semicolon terminator when calling this method. Given this, you cannot use the curly-bracket scope as a fully blown method scope that contains numerous lines of executable code. Rather, you can invoke a single member on the object using the dot operator as well as define a simple general expression such as {age += 1}.
在这里,我通过调用 ToUpper() 将名称大写。请注意,在字符串内插方法中,调用此方法时不会添加分号终止符。鉴于此,您不能将大括号作用域用作包含大量可执行代码行的完全成熟的方法作用域。相反,您可以使用点运算符调用对象上的单个成员,也可以定义一个简单的通用表达式,例如 {age += 1}。

It is also worth noting that you can still use escape characters in the string literal within this new syntax.Thus, if you wanted to insert a tab, you can prefix a \t token as so:
还值得注意的是,您仍然可以在此新语法中的字符串文本中使用转义字符。因此,如果要插入制表符,可以在 \t 标记前面添加如下前缀:

string greeting = string.Format("\tHello {0} you are {1} years old.", name.ToUpper(), age); 
string greeting2 = $"\tHello {name.ToUpper()} you are {age} years old.";

Performance Improvements (Updated 10.0)

性能改进(10.0 更新)

When using string interpolation in versions prior to C# 10, under the hood the compiler is converting the interpolated string statement into a Format() call. For example, take a shortened version of the previous example and plug it into the Main() method of a .NET 5 (C# 9) console application (named CSharp9Strings).
在 C# 10 之前的版本中使用字符串内插时,编译器会将内插字符串语句转换为 Format() 调用。例如,采用上一示例的缩短版本,并将其插入 .NET 5 (C# 9) 控制台应用程序(名为 CSharp9Strings)的 Main() 方法中。

using System;
namespace CSharp9Strings
{
    class Program
    {
        static void Main(string[] args)
        {
            int age = 4;
            string name = "Soren";
            string greeting = string.Format("\tHello {0} you are {1} years old.", name.ToUpper(), age);
            string greetings = $"\tHello {name.ToUpper()} you are {age} years old.";
        }
    }
}

When the Main() method is examined with ILDasm, you can see that both the call to Format() and the string interpolation calls are implemented as the same Format() calls in the IL (lines in bold):
当使用 ILDasm 检查 Main() 方法时,您可以看到对 Format() 的调用和字符串内插调用都作为 IL 中的相同 Format() 调用实现(粗体行):

.method private hidebysig static void Main(string[] args) cil managed
{
    .maxstack 3
    .entrypoint
    .locals init (int32 V_0, string V_1, string V_2)
    IL_0000: nop
    IL_0001: ldc.i4.4
    IL_0002: stloc.0
    IL_0003: ldstr "Soren"
    IL_0008: stloc.1
    IL_0009: ldstr "\tHello {0} you are {1} years old."
    IL_000e: ldloc.1
    IL_000f: callvirt instance string [System.Runtime]System.String::ToUpper()
    IL_0014: ldloc.0
    IL_0015: box [System.Runtime]System.Int32
    IL_001a: call string [System.Runtime]System.String::Format(string, object, object)
    IL_001f: stloc.2
    IL_0020: ldstr "\tHello {0} you are {1} years old."
    IL_0025: ldloc.1
    IL_0026: callvirt instance string [System.Runtime]System.String::ToUpper()
    IL_002b: ldloc.0
    IL_002c: box [System.Runtime]System.Int32
    IL_0031: call string [System.Runtime]System.String::Format(string, object, object)
    IL_0036: stloc.3
    IL_0037: ret
} // end of method Program::Main

The problem with this is performance. When Format() is called at runtime, the method parses the format string to find the literals, format items, specifiers, and alignments, which the compiler already did at compile time. All items are passed in as System.Object, which means value types are boxed. If there are more than three parameters, an array is allocated. In addition to performance issues, Format() works only with reference types.
这样做的问题是性能。在运行时调用 Format() 时,该方法会解析格式字符串以查找文本、格式项、说明符和对齐方式,编译器在编译时已经这样做了。所有项都作为 System.Object 传入,这意味着值类型被装箱。如果有三个以上的参数,则分配一个数组。除了性能问题之外,Format() 仅适用于引用类型。

A major change in C# 10 is that all the work that can be done at compile time is retained in the IL using the DefaultInterpolatedStringHandler and its methods. This is the equivalent C# 10 code that interpolated strings are converted to:
C# 10 中的一个主要更改是,在编译时可以完成的所有工作都使用 DefaultInterpolatedStringHandler 及其方法保留在 IL 中。这是将内插字符串转换为的等效 C# 10 代码:

using System.Runtime.CompilerServices;

static void StringInterpolationWithDefaultInterpolatedStringHandler()
{
    Console.WriteLine("=> String interpolation under the covers:\a");
    int age = 4;
    string name = "Soren";
    var builder = new DefaultInterpolatedStringHandler(3, 2);
    builder.AppendLiteral("\tHello ");
    builder.AppendFormatted(name);
    builder.AppendLiteral(" you are ");
    builder.AppendFormatted(age);
    builder.AppendLiteral(" years old.");
    var greeting = builder.ToStringAndClear();
    Console.WriteLine(greeting);
}

The version of the DefaultInterpolatedStringHandler’s constructor used in this example takes two integers. The first is the number of literals, and the second is the number variables. This enables the instance to make an educated guess as to how much memory to allocate. Literals are added in with the AppendLiteral() method, and variables are added in with the AppendFormatted() method.
此示例中使用的 DefaultInterpolatedStringHandler 构造函数的版本采用两个整数。第一个是文本的数量,第二个是数字变量。这使实例能够对要分配的内存量进行有根据的猜测。文本是用 AppendLiteral() 方法添加的,变量是用 AppendFormatted() 方法添加的。

Bench testing has shown a significant performance improvement in string handling in C# 10 when your code contains string interpolation, which is good news. The really good news is that you don’t have to write all this extra code. The compiler takes care of it all when compiling string interpolation.
基准测试显示,当代码包含字符串插值时,C# 10 中的字符串处理性能显著提高,这是个好消息。真正的好消息是,您不必编写所有这些额外的代码。编译器在编译字符串插值时会处理所有问题。

Defining Verbatim Strings (Updated 8.0)

定义逐字字符串(8.0 更新)

When you prefix a string literal with the @ symbol, you have created what is termed a verbatim string. Using verbatim strings, you disable the processing of a literal’s escape characters and print out a string as is. This can be most useful when working with strings representing directory and network paths. Therefore, rather than making use of \ escape characters, you can simply write the following: 在字符串文本前面加上 @ 符号时,您已经创建了所谓的逐字字符串。使用逐字字符串,可以禁用文本转义字符的处理,并按原样打印出字符串。这在处理表示目录和网络路径的字符串s 时最有用。因此,与其使用 \ 转义字符,不如简单地编写以下内容:

Also note that verbatim strings can be used to preserve whitespace for strings that flow over multiple lines.
另请注意,逐字字符串可用于保留流经多行的字符串的空格。

// The following string is printed verbatim, 
// 以下字符串逐字打印,
// thus all escape characters are displayed. 
// 因此,将显示所有转义字符。Console.WriteLine(@"C:\MyApp\bin\Debug");
// Whitespace is preserved with verbatim strings. 
// 空格使用逐字字符串保留。
string myLongString = @"This is a very
very
very
long string";
Console.WriteLine(myLongString);

Using verbatim strings, you can also directly insert a double quote into a literal string by doubling the token。
使用逐字逐句字符串,您还可以通过加倍标记直接将双引号插入到文字字符串中

Console.WriteLine(@"Cerebus said ""Darrr! Pret-ty sun-sets""");

Verbatim strings can also be interpolated strings, by specifying both the interpolation operator ($) and the verbatim operator (@).
逐字字符串也可以是内插字符串,方法是同时指定插值运算符 ($) 和逐字运算符 (@)。

string interp = "interpolation";
string myLongString2 = $@"This is a very
    very
        long string with {interp}";

New in C# 8, the order does not matter. Using either $@ or @$ will work.
C# 8 中的新功能,顺序无关紧要。使用 $@ 或 @$ 都可以。

Working with Strings and Equality

使用字符串和相等

As will be fully explained in Chapter 4, a reference type is an object allocated on the garbage-collected managed heap. By default, when you perform a test for equality on reference types (via the C# == and != operators), you will be returned true if the references are pointing to the same object in memory. However, even though the string data type is indeed a reference type, the equality operators have been redefined to compare the values of string objects, not the object in memory to which they refer.
正如将在第4章中充分解释的那样,引用类型是在垃圾回收的托管堆上分配的对象。默认情况下,对引用类型执行相等性测试(通过 C# == 和 != 运算符)时,如果引用指向内存中的同一对象,则将返回 true。但是,即使字符串数据类型确实是引用类型,也已重新定义相等运算符以比较字符串对象的值,而不是它们引用的内存中的对象。

static void StringEquality()
{
    Console.WriteLine("=> String equality:");
    string s1 = "Hello!";
    string s2 = "Yo!";
    Console.WriteLine("s1 = {0}", s1);
    Console.WriteLine("s2 = {0}", s2);
    Console.WriteLine();

    // Test these strings for equality.
    // 测试这些字符串的相等性。
    Console.WriteLine("s1 == s2: {0}", s1 == s2);
    Console.WriteLine("s1 == Hello!: {0}", s1 == "Hello!");
    Console.WriteLine("s1 == HELLO!: {0}", s1 == "HELLO!");
    Console.WriteLine("s1 == hello!: {0}", s1 == "hello!");
    Console.WriteLine("s1.Equals(s2): {0}", s1.Equals(s2));
    Console.WriteLine("Yo!.Equals(s2): {0}", "Yo!".Equals(s2));
    Console.WriteLine();
}

The C# equality operators by default perform a case-sensitive, culture-insensitive, character-by- character equality test on string objects. Therefore, "Hello!" is not equal to "HELLO!", which is also different from "hello!". Also, keeping the connection between string and System.String in mind, notice that you are able to test for equality using the Equals() method of String as well as the baked-in equality operators. Finally, given that every string literal (such as "Yo!") is a valid System.String instance, you are able to access string-centric functionality from a fixed sequence of characters.
默认情况下,C# 相等运算符对字符串对象执行区分大小写、不区分区域性、逐字符相等性测试。因此,“你好! 不等于“你好!”,这也不同于“你好!”。此外,请记住字符串和 System.String 之间的联系,请注意,您可以使用 String 的 Equals() 方法以及内置的相等运算符来测试相等性。最后,假设每个字符串文本(如“Yo!”)都是有效的 System.String 实例,您可以从固定的字符序列访问以字符串为中心的功能。

Modifying String Comparison Behavior

修改字符串比较行为

As mentioned, the string equality operators (Compare(), Equals(), and ==) as well as the IndexOf() function are by default case sensitive and culture insensitive. This can cause a problem if your program does not care about case. One way to overcome this is to convert everything to uppercase or lowercase and then compare, like this:
如前所述,字符串相等运算符(Compare()、Equals() 和 ==)以及 IndexOf() 函数默认区分大小写,区域性不区分。如果您的程序不关心大小写,这可能会导致问题。克服此问题的一种方法是将所有内容转换为大写或小写,然后进行比较,如下所示:

if (firstString.ToUpper() == secondString.ToUpper())
{
    //Do something做点什么
}

This makes a copy of each string with all lowercase letters. It is probably not an issue in most cases but could be a performance hit with a significantly large string or even fail based on culture. A much better practice is to use the overloads of the methods listed earlier that take a value of the StringComparison enumeration to control exactly how the comparisons are done. Table 3-7 describes the StringComparison values.
这将创建包含所有小写字母的每个字符串的副本。在大多数情况下,这可能不是问题,但可能是字符串非常大的字符串对性能的影响,甚至基于区域性而失败。更好的做法是使用前面列出的方法的重载,这些方法的值为StringComparison 枚举,用于准确控制如何完成比较。表 3-7 介绍了字符串比较值。

Table 3-7. Values of the StringComparison Enumeration
表 3-7. 字符串比较枚举的值

C# Equality/Relational Operator Meaning in Life
CurrentCulture Compares strings using culture-sensitive sort rules and the current culture
使用区分区域性的排序规则和当前区域性比较字符串
CurrentCultureIgnoreCase Compares strings using culture-sensitive sort rules and the current culture and ignores the case of the strings being compared
使用区分区域性的排序规则和当前区域性比较字符串,并忽略要比较的字符串的大小写
InvariantCulture Compares strings using culture-sensitive sort rules and the invariant culture
使用区分区域性的排序规则和固定区域性比较字符串
InvariantCultureIgnoreCase Compares strings using culture-sensitive sort rules and the invariant culture and ignores the case of the strings being compared
使用区分区域性的排序规则和固定区域性比较字符串,并忽略要比较的字符串的大小写
Ordinal Compares strings using ordinal (binary) sort rules
使用序号(二进制)排序规则比较字符串
OrdinalIgnoreCare Compares strings using ordina
使用序号(二进制)排序规则比较字符串,并忽略要比较的字符串的大小写

To see the effect of using the StringComparison option, create a new method named StringEqualitySpecifyingCompareRules() and add the following code:
若要查看使用“字符串比较”选项的效果,请创建一个名为StringEqualSpecifyingCompareRules() 并添加以下代码:

static void StringEqualitySpecifyingCompareRules()
{
    Console.WriteLine("=> String equality (Case Insensitive:");
    string s1 = "Hello!";
    string s2 = "HELLO!";
    Console.WriteLine("s1 = {0}", s1);
    Console.WriteLine("s2 = {0}", s2);
    Console.WriteLine();
    // Check the results of changing the default compare rules.
    // 检查更改默认比较规则的结果。
    Console.WriteLine("Default rules: s1={0},s2={1}s1.Equals(s2): {2}", s1, s2, s1.Equals(s2));
    Console.WriteLine("Ignore case: s1.Equals(s2, StringComparison.OrdinalIgnoreCase): {0}",
    s1.Equals(s2, StringComparison.OrdinalIgnoreCase));
    Console.WriteLine("Ignore case, Invariant Culture: s1.Equals(s2, StringComparison.InvariantCultureIgnoreCase): {0}", s1.Equals(s2, StringComparison.InvariantCultureIgnoreCase));
    Console.WriteLine();
    Console.WriteLine("Default rules: s1={0},s2={1} s1.IndexOf(\"E\"): {2}", s1, s2, s1.IndexOf("E"));
    Console.WriteLine("Ignore case: s1.IndexOf(\"E\", StringComparison.OrdinalIgnoreCase): {0}", s1.IndexOf("E", StringComparison.OrdinalIgnoreCase));
    Console.WriteLine("Ignore case, Invariant Culture: s1.IndexOf(\"E\", StringComparison.InvariantCultureIgnoreCase): {0}", s1.IndexOf("E", StringComparison.InvariantCultureIgnoreCase));
    Console.WriteLine();
}

While the examples here are simple ones and use the same letters across most cultures, if your application needed to consider different culture sets, using the StringComparison options is a must.
虽然此处的示例很简单,并且在大多数区域性中使用相同的字母,但如果应用程序需要考虑不同的区域性集,则必须使用 StringCompare 选项。

Strings Are Immutable

字符串是不可变的

One of the interesting aspects of System.String is that after you assign a string object with its initial value, the character data cannot be changed. At first glance, this might seem like a flat-out lie, given that you are always reassigning strings to new values and because the System.String type defines a number of methods that appear to modify the character data in one way or another (such as uppercasing and lowercasing).However, if you look more closely at what is happening behind the scenes, you will notice the methods of thestring type are, in fact, returning you a new string object in a modified format.
System.String 的一个有趣的方面是,在为字符串对象分配其初始值后,无法更改字符数据。乍一看,这似乎是一个彻头彻尾的谎言,因为您总是将字符串重新分配给新值,并且因为 System.String 类型定义了许多似乎以某种方式修改字符数据的方法(例如大写和小写)。但是,如果您更仔细地观察幕后发生的事情,您会注意到实际上,字符串类型以修改后的格式返回一个新的字符串对象。

static void StringsAreImmutable()
{
    Console.WriteLine("=> Immutable Strings:\a");

    // Set initial string value.
    // 设置初始字符串值。
    string s1 = "This is my string.";
    Console.WriteLine("s1 = {0}", s1);

    // Uppercase s1?
    // 大写 s1?
    string upperString = s1.ToUpper();
    Console.WriteLine("upperString = {0}", upperString);

    // Nope! s1 is in the same format!
    //不!S1 的格式相同!
    Console.WriteLine("s1 = {0}", s1);
}

If you examine the relevant output that follows, you can verify that the original string object (s1) is not uppercased when calling ToUpper(). Rather, you are returned a copy of the string in a modified format.
如果检查下面的相关输出,则可以验证在调用 ToUpper() 时原始字符串对象 (s1) 是否不大写。相反,将返回修改格式的字符串副本。

s1 = This is my string.
upperString = THIS IS MY STRING.
s1 = This is my string.

The same law of immutability holds true when you use the C# assignment operator. To illustrate, implement the following StringsAreImmutable2() method:
使用 C# 赋值运算符时,相同的不变性定律也适用。为了说明这一点,请实现以下 StringsAreImmutable2() 方法:

static void StringsAreImmutable2()
{
    Console.WriteLine("=> Immutable Strings 2:\a");
    string s2 = "My other string";
    s2 = "New string value";
    Console.WriteLine(s2);
}

Now, compile your application and run ildasm.exe (see Chapter 1). The following output shows what you would find if you were to generate CIL code for the StringsAreImmutable2() method:
现在,编译您的应用程序并运行 ildasm.exe(请参阅第 1 章)。以下输出显示了如果要为 StringsAreImmutable2() 方法生成 CIL 代码时会发现的内容:

.method assembly hidebysig static void '<<Main>$>g__StringsAreImmutable2()|0,8'() cil managed
{
// Code size 32 (0x20)
.maxstack 1
.locals init (string V_0)
IL_0000: nop
...
IL_000c: ldstr "My other string"
IL_0011: stloc.0
IL_0012: ldstr "New string value"
IL_0017: stloc.0
IL_0018: ldloc.0
IL_0013: nop
...
IL_0014: ret
} // end of method Program::StringsAreImmutable2

Although you have yet to examine the low-level details of the CIL, note the two calls to the ldstr (load string) opcode. Simply put, the ldstr opcode of the CIL loads a new string object on the managed heap. The previous string object that contained the value "My other string" will eventually be garbage collected.
尽管您尚未检查 CIL 的低级详细信息,但请注意对 ldstr(加载字符串)操作码的两次调用。简单地说,CIL 的 ldstr 操作码在托管堆上加载一个新的字符串对象。包含值“我的其他字符串”的上一个字符串对象最终将被垃圾回收。

So, what exactly are you to gather from this insight? In a nutshell, the string class can be inefficient and result in bloated code if misused, especially when performing string concatenation or working with huge amounts of text data. If you need to represent basic character data such as a US Social Security number, first or last names, or simple bits of text used within your application, the string class is the perfect choice.
那么,你究竟要从这个见解中收集什么?简而言之,字符串类可能效率低下,如果使用不当会导致代码臃肿,尤其是在执行字符串连接或处理大量文本数据时。如果需要表示基本字符数据(如美国社会保险号、名字或姓氏或应用程序中使用的简单文本位),则字符串类是完美的选择。

However, if you are building an application that makes heavy use of frequently changing textual data (such as a word processing program), it would be a bad idea to represent the word processing data using string objects, as you will most certainly (and often indirectly) end up making unnecessary copies of string data. So, what is a programmer to do? Glad you asked.
但是,如果您正在构建大量使用频繁更改的文本数据(如文字处理程序)的应用程序,则使用字符串对象表示文字处理数据将是一个坏主意,因为您肯定会(通常是间接地)最终制作不必要的字符串数据副本。那么,程序员该怎么做呢?很高兴你问。

Using the System.Text.StringBuilder Type

使用 System.Text.StringBuilder 类型

Given that the string type can be inefficient when used with reckless abandon, the .NET Core base class libraries provide the System.Text namespace. Within this (relatively small) namespace lives a class named StringBuilder. Like the System.String class, the StringBuilder defines methods that allow you to replace or format segments, for example. When you want to use this type in your C# code files, your first step is to make sure the following namespace is imported into your code file (this should already be the case for a new Visual Studio project):
鉴于字符串类型在鲁莽放弃使用时可能效率低下,.NET Core 基类库提供了 System.Text 命名空间。在这个(相对较小的)命名空间中,有一个名为 StringBuilder 的类。例如,与 System.String 类一样,StringBuilder 定义了允许您替换或格式化段的方法。如果要在 C# 代码文件中使用此类型,第一步是确保将以下命名空间导入到代码文件中(对于新的 Visual Studio 项目,应该已经如此):

// StringBuilder lives here! 
// StringBuilder住在这里!
using System.Text;

What is unique about the StringBuilder is that when you call members of this type, you are directly modifying the object’s internal character data (making it more efficient), not obtaining a copy of the data in a modified format. When you create an instance of the StringBuilder, you can supply the object’s initial startup values via one of many constructors. If you are new to the topic of constructors, simply understand that constructors allow you to create an object with an initial state when you apply the new keyword.
StringBuilder 的独特之处在于,当您调用此类型的成员时,您直接修改对象的内部字符数据(使其更有效),而不是以修改的格式获取数据的副本。创建 StringBuilder 的实例时,可以通过多个构造函数之一提供对象的初始启动值。如果您不熟悉构造函数主题,只需了解构造函数允许您在应用 new 关键字时创建具有初始状态的对象。Consider the following usage of StringBuilder: 考虑以下 StringBuilder 用法:

using System.Text;
// FunWithStringBuilder();
static void FunWithStringBuilder()
{
    Console.WriteLine("=> Using the StringBuilder:");
    StringBuilder sb = new StringBuilder("**** Fantastic Games ****");
    sb.Append("\n");
    sb.AppendLine("Half Life");
    sb.AppendLine("Morrowind");
    sb.AppendLine("Deus Ex" + "2");
    sb.AppendLine("System Shock");
    Console.WriteLine(sb.ToString());
    sb.Replace("2", " Invisible War");
    Console.WriteLine(sb.ToString());
    Console.WriteLine("sb has {0} chars.", sb.Length);
    Console.WriteLine();
}

Here, I have constructed a StringBuilder set to the initial value " Fantastic Games ". As you can see, I am appending to the internal buffer and am able to replace or remove characters at will. By default, a StringBuilder is only able to initially hold a string of 16 characters or fewer (but will expand automatically if necessary); however, this default starting value can be changed via an additional constructor argument. 在这里,我构造了一个设置为初始值“ Fantastic Games ”的 StringBuilder。
如您所见,我正在追加到内部缓冲区,并且能够随意替换或删除字符。默认情况下,StringBuilder 最初只能容纳 16 个字符或更少的字符串(但如有必要会自动扩展);但是,可以通过其他构造函数参数更改此默认起始值。

 
// Make a StringBuilder with an initial size of 256. 
// 创建一个初始大小为 256 的 StringBuilder。
StringBuilder sb = new StringBuilder("**** Fantastic Games ****", 256);

If you append more characters than the specified limit, the StringBuilder object will copy its data into a new instance and grow the buffer by the specified limit.
如果追加的字符数超过指定的限制,则 StringBuilder 对象会将其数据复制到新实例中,并按指定的限制增加缓冲区。

Narrowing and Widening Data Type Conversions

缩小和扩大数据类型转换

Now that you understand how to work with intrinsic C# data types, let’s examine the related topic of data type conversion. Assume you have a new Console Application project named TypeConversions and added it to your solution. Update the code to match the following:
现在,你已了解如何使用内部 C# 数据类型,接下来让我们来看看数据类型转换的相关主题。假设您有一个名为 TypeConversions 的新控制台应用程序项目,并将其添加到您的解决方案中。更新代码以匹配以下内容:

Console.WriteLine("***** Fun with type conversions *****");
// Add two shorts and print the result.
// 添加两个shorts 类型数据并打印结果。
short numb1 = 9, numb2 = 10;
Console.WriteLine("{0} + {1} = {2}",
numb1, numb2, Add(numb1, numb2));
Console.ReadLine();
static int Add(int x, int y)
{
    return x + y;
}

Notice that the Add() method expects to be sent two int parameters. However, the calling code is, in fact, sending in two short variables. While this might seem like a complete and total mismatch of data types, the program compiles and executes without error, returning the expected result of 19.
请注意,Add() 方法期望发送两个 int 参数。但是,调用代码实际上发送了两个短变量。虽然这看起来像是数据类型的完全不匹配,但程序编译和执行时没有错误,返回预期结果 19。

The reason the compiler treats this code as syntactically sound is because there is no possibility for loss of data. Given that the maximum value of a short (32,767) is well within the maximum range of an int (2,147,483,647), the compiler implicitly widens each short to an int. Formally speaking, widening is the term used to define an implicit upward cast that does not result in a loss of data.
编译器将此代码视为语法合理的原因是没有数据丢失的可能性。鉴于 short 的最大值 (32,767) 完全在 int (2,147,483,647) 的最大范围内,编译器会将每个 short 隐式扩大到 int。从形式上讲,加宽是用于定义不会导致数据丢失的隐式向上转换的术语。

■ Note Look up “type Conversion tables” in the .net Core documentation if you want to see permissible widening (and narrowing, discussed next) conversions for each C# data type.
注意 如果要查看每种 C# 数据类型的允许加宽(和缩小,下文将讨论)转换,请在 .net Core 文档中查找“类型转换表”。

Although this implicit widening worked in your favor for the previous example, other times this “feature” can be the source of compile-time errors. For example, assume you have set values to numb1 and numb2 that (when added together) overflow the maximum value of a short. Also, assume you are storing the return value of the Add() method within a new local short variable, rather than directly printing the result to the console.
尽管这种隐式加宽在前面的示例中对您有利,但其他时候此“功能”可能是编译时错误的根源。例如,假设您已将值设置为 numb1 和 numb2,这些值(加在一起时)会溢出短线的最大值。 此外,假设您将 Add() 方法的返回值存储在新的局部短变量中,而不是直接将结果打印到控制台。

Console.WriteLine("***** Fun with type conversions *****");
// Compiler error below!
// 下面的编译器错误!
short numb1 = 30000, numb2 = 30000;
short answer = Add(numb1, numb2);
Console.WriteLine("{0} + {1} = {2}",
numb1, numb2, answer);
Console.ReadLine();

In this case, the compiler reports the following error:
在这种情况下,编译器将报告以下错误:

Cannot implicitly convert type 'int' to 'short'. An explicit conversion exists (are you missing a cast?)
不能将类型“int”隐式转换为“short”。存在显式转换(您是否缺少强制转换?

The problem is that although the Add() method is capable of returning an int with the value 60,000 (which fits within the range of a System.Int32), the value cannot be stored in a short, as it overflows the bounds of this data type. Formally speaking, the CoreCLR was unable to apply a narrowing operation. As you can guess, narrowing is the logical opposite of widening, in that a larger value is stored within a smaller data type variable.
问题在于,尽管 Add() 方法能够返回值为 60,000 的 int(这符合 System.Int32 的范围),但该值不能存储在短时间内,因为它会溢出此数据类型的边界。从形式上讲,CoreCLR无法应用缩小操作。您可以猜到,缩小在逻辑上与加宽相反,因为较大的值存储在较小的数据类型变量中。

It is important to point out that all narrowing conversions result in a compiler error, even when you can reason that the narrowing conversion should indeed succeed. For example, the following code also results in a compiler error:
请务必指出,所有缩小转换都会导致编译器错误,即使您可以推断缩小转换确实应该成功。例如,以下代码还会导致编译器错误:

// Another compiler error!
// 另一个编译器错误!
static void NarrowingAttempt()
{
    byte myByte = 0;
    int myInt = 200;
    myByte = myInt;
    Console.WriteLine("Value of myByte: {0}", myByte);
}

Here, the value contained within the int variable (myInt) is safely within the range of a byte; therefore, you would expect the narrowing operation to not result in a runtime error. However, given that C# is a language built with type safety in mind, you do indeed receive a compiler error.
在这里,int 变量 (myInt) 中包含的值安全地在一个字节的范围内;因此,您希望缩小操作不会导致运行时错误。但是,鉴于 C# 是一种在构建时考虑了类型安全的语言,您确实会收到编译器错误。

When you want to inform the compiler that you are willing to deal with a possible loss of data because of a narrowing operation, you must apply an explicit cast using the C# casting operator, (). Consider the following update to the Program.cs file:
如果要通知编译器您愿意处理由于缩小操作而可能丢失的数据,则必须使用 C# 强制转换运算符 () 应用显式强制转换。请考虑对 Program.cs 文件进行以下更新:

Console.WriteLine("***** Fun with type conversions *****");
short numb1 = 30000, numb2 = 30000;

// Explicitly cast the int into a short (and allow loss of data).
// 将 int 显式转换为短整型(并允许丢失数据)。
short answer = (short)Add(numb1, numb2);
Console.WriteLine("{0} + {1} = {2}",
numb1, numb2, answer);
NarrowingAttempt();
Console.ReadLine();
static int Add(int x, int y)
{
    return x + y;
}
static void NarrowingAttempt()
{
    byte myByte = 0;
    int myInt = 200;
    // Explicitly cast the int into a byte (no loss of data).
    // 将 int 显式转换为字节(不丢失数据)。
    myByte = (byte)myInt;
    Console.WriteLine("Value of myByte: {0}", myByte);
}

At this point, the code compiles; however, the result of the addition is completely incorrect.
此时,代码将编译;但是,添加的结果是完全不正确的。

***** Fun with type conversions ***** 
30000 + 30000 = -5536
Value of myByte: 200

As you have just witnessed, an explicit cast allows you to force the compiler to apply a narrowing conversion, even when doing so may result in a loss of data. In the case of the NarrowingAttempt() method, this was not a problem because the value 200 can fit snugly within the range of a byte. However, in the case of adding the two shorts within the code, the end result is completely unacceptable (30,000 + 30,000 =–5536?).
正如您刚才所目睹的,显式强制转换允许您强制编译器应用缩小转换,即使这样做可能会导致数据丢失。在 NarrowingTry() 方法的情况下,这不是问题,因为值 200 可以紧贴在字节的范围内。但是,在代码中添加两个短s 的情况下,最终结果是完全不可接受的(30,000 + 30,000 =–5536?).

If you are building an application where loss of data is always unacceptable, C# provides the checked and unchecked keywords to ensure data loss does not escape undetected.
如果正在构建的应用程序始终无法接受数据丢失,C# 将提供已检查的和未选中的关键字,以确保数据丢失不会逃脱而未被发现。

Using the checked Keyword

使用选中的关键字

Let’s begin by learning the role of the checked keyword. Assume you have a new method within Program that attempts to add two bytes, each of which has been assigned a value that is safely below the maximum (255). If you were to add the values of these types (casting the returned int to a byte), you would assume that the result would be the exact sum of each member.
让我们从学习选中关键字的作用开始。假设您在程序中有一个新方法,该方法尝试添加两个字节,每个字节都分配了一个安全低于最大值 (255) 的值。如果要添加这些类型的值(将返回的 int 强制转换为字节),则假定结果将是每个成员的确切总和。

static void ProcessBytes()
{
    byte b1 = 100;
    byte b2 = 250;
    byte sum = (byte)Add(b1, b2);
    // sum should hold the value 350. However, we find the value 94!
    // 总和应保持值 350。但是,我们发现值为 94!
    Console.WriteLine("sum = {0}", sum);
}

If you were to view the output of this application, you might be surprised to find that sum contains the value 94 (rather than the expected 350). The reason is simple. Given that a System.Byte can hold a value only between 0 and 255 (inclusive, for a grand total of 256 slots), sum now contains the overflow value (350 – 256 = 94). By default, if you take no corrective course of action, overflow/underflow conditions occur without error.
如果要查看此应用程序的输出,您可能会惊讶地发现 sum 包含值 94(而不是预期的 350)。原因很简单。假设 System.Byte 只能保存介于 0 和 255 之间的值(包括 256 到 <> 个插槽),则 sum 现在包含溢出值(350 – 256 = 94)。默认情况下,如果不采取纠正措施,则会发生溢出/下溢情况而不会出错。

To handle overflow or underflow conditions in your application, you have two options. Your first choice is to leverage your wits and programming skills to handle all overflow/underflow conditions manually. Of course, the problem with this technique is the simple fact that you are human, and even your best attempts might result in errors that have escaped your eyes.
要处理应用程序中的溢出或下溢情况,您有两个选项。您的首选是利用您的智慧和编程技能手动处理所有溢出/下溢情况。当然,这种技术的问题在于你是人类的简单事实,即使是你最好的尝试也可能导致你眼睛之外的错误。

Thankfully, C# provides the checked keyword. When you wrap a statement (or a block of statements) within the scope of the checked keyword, the C# compiler emits additional CIL instructions that test for overflow conditions that may result when adding, multiplying, subtracting, or dividing two numerical data types.
值得庆幸的是,C# 提供了 checked 关键字。将语句(或语句块)包装在 checked 关键字的范围内时,C# 编译器会发出其他 CIL 指令,用于测试在添加、乘法、减去或除以两种数值数据类型时可能导致的溢出情况。

If an overflow has occurred, you will receive a runtime exception: System.OverflowException.Chapter 7 will examine all the details of structured exception handling and the use of the try and catch keywords. Without getting too hung up on the specifics at this point, observe the following update:
如果发生溢出,您将收到运行时异常:System.OverflowException。第7章将研究结构化异常处理的所有细节以及try和catch的使用。关键字。在这一点上,不要太纠结于细节,请观察以下更新:

static void ProcessBytes()
{
    byte b1 = 100;
    byte b2 = 250;
    // This time, tell the compiler to add CIL code
    // 这一次,告诉编译器添加 CIL 代码
    // to throw an exception if overflow/underflow
    // 在溢出/下溢时引发异常
    // takes place.发生。
    try
    {
        byte sum = checked((byte)Add(b1, b2));
        Console.WriteLine("sum = {0}", sum);
    }
    catch (OverflowException ex)
    {
        Console.WriteLine(ex.Message);
    }
}

Notice that the return value of Add() has been wrapped within the scope of the checked keyword.Because the sum is greater than a byte, this triggers a runtime exception. Notice the error message printed out via the Message property.
请注意,Add() 的返回值已包装在 checked 关键字的范围内。由于总和大于一个字节,因此会触发运行时异常。请注意通过 Message 属性打印出的错误消息。

Arithmetic operation resulted in an overflow. 
算术运算导致溢出。

If you want to force overflow checking to occur over a block of code statements, you can do so by defining a “checked scope” as follows:
如果要强制对代码语句块进行溢出检查,可以通过定义“已检查范围”来实现,如下所示:

try
{
    checked
    {
        byte sum = (byte)Add(b1, b2);
        Console.WriteLine("sum = {0}", sum);
    }
}
catch (OverflowException ex)
{
    Console.WriteLine(ex.Message);
}

In either case, the code in question will be evaluated for possible overflow conditions automatically, which will trigger an overflow exception if encountered.
无论哪种情况,都会自动评估相关代码是否存在可能的溢出情况,如果遇到溢出情况,这将触发溢出异常。

Setting Project-wide Overflow Checking (Project File)

设置项目范围的溢出检查(项目文件)

If you are creating an application that should never allow silent overflow to occur, you might find yourself in the annoying position of wrapping numerous lines of code within the scope of the checked keyword. As an alternative, the C# compiler supports the /checked flag. When it’s enabled, all your arithmetic will be evaluated for overflow without the need to make use of the C# checked keyword. If overflow has been discovered, you will still receive a runtime exception. To set this for the entire project, enter the following into the project file:
如果您正在创建一个永远不允许发生静默溢出的应用程序,您可能会发现自己处于在 checked 关键字范围内包装大量代码的烦人位置。作为替代方法,C# 编译器支持 /checked 标志。启用后,将计算所有算术是否溢出,而无需使用 C# checked 关键字。如果发现溢出,您仍将收到运行时异常。要为整个项目设置此项,请在项目文件中输入以下内容:

<PropertyGroup>
    <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
</PropertyGroup>

Setting Project-wide Overflow Checking (Visual Studio)

设置项目范围的溢出检查(Visual Studio)

To enable the “Check for arithmetic overflow” flag, open the project’s property page. Note that Visual Studio 2022 has updated the project settings dialog pretty significantly from Visual Studio 2019. Most of the options in the dialog now have a descriptor along with the setting. Select General from the Build menu (on the left side of the dialog) and then select the “Check for arithmetic overflow/underflow” check box (see Figure 3-3). Enabling this setting can be helpful when you are creating a debug build. After all the overflow exceptions have been squashed out of the code base, you are free to disable the /checked flag for subsequent builds (which can increase the runtime performance of your application).
若要启用“检查算术溢出”标志,请打开项目的属性页。请注意,Visual Studio 2022 已从 Visual Studio 2019 中显着更新了项目设置对话框。对话框中的大多数选项现在都有一个描述符和设置。从“生成”菜单(位于对话框左侧)中选择“常规”,然后选中“检查算术溢出/下溢”复选框(请参阅图 3-3)。在创建调试版本时,启用此设置会很有帮助。从代码库中清除所有溢出异常后,您可以自由地禁用后续构建的 /checked 标志(这可以提高应用程序的运行时性能)。

■ Note the configuration selection is updated in Visual studio 2022. the gear icon shows up only when you hover your mouse pointer by the check box or to the left of the title. selecting the configuration is covered next.
请注意,配置选择在 Visual Studio 2022 中更新。 仅当您将鼠标指针悬停在复选框上或标题左侧时,齿轮图标才会显示。 接下来将介绍如何选择配置。

Alt text

Figure 3-3. Enabling project-wide overflow/underflow data checking
图 3-3。 启用项目范围的溢出/下溢数据检查

Selecting the Build Configuration

选择构建配置

To select all configurations or a specific configuration for a build option, hover with your mouse over the check box or to the left of the title. Click the gear that appears, and you will see the build configuration selector shown in Figure 3-4.
若要为生成选项选择所有配置或特定配置,请将鼠标悬停在复选框上或标题左侧。单击出现的齿轮,您将看到如图 3-4 所示的构建配置选择器。

Alt text

Figure 3-4. Selecting build configuration(s) for build options
图 3-4。 为构建选项选择构建配置

Using the unchecked Keyword

使用未选中的关键字

Now, assuming you have enabled this project-wide setting, what are you to do if you have a block of code where data loss is acceptable? Given that the /checked flag will evaluate all arithmetic logic, C# provides the unchecked keyword to disable the throwing of an overflow exception on a case-by-case basis. This keyword’s use is identical to that of the checked keyword, in that you can specify a single statement or a block of statements.
现在,假设您已启用此项目范围的设置,那么如果您有一个可以接受数据丢失的代码块,该怎么办?鉴于 /checked 标志将计算所有算术逻辑,C# 提供了 unchecked 关键字,以根据具体情况禁用溢出异常的引发。此关键字的用法与选中关键字的用法相同,因为您可以指定单个语句或语句块。

// Assuming /checked is enabled,
// 假设启用了 /check,
// this block will not trigger
// 此块不会触发
// a runtime exception.
// 运行时异常。
unchecked
{
    byte sum = (byte)(b1 + b2);
    Console.WriteLine("sum = {0} ", sum);
}

So, to summarize the C# checked and unchecked keywords, remember that the default behavior of the .NET Core runtime is to ignore arithmetic overflow/underflow. When you want to selectively handle discrete statements, use the checked keyword. If you want to trap overflow errors throughout your application, enable the /checked flag. Finally, the unchecked keyword can be used if you have a block of code where overflow is acceptable (and thus should not trigger a runtime exception).
因此,为了总结 C# 选中和 未选中的关键字,请记住.NET Core 运行时忽略算术溢出/下溢。如果要有选择地处理离散语句,请使用 checked 关键字。如果要在整个应用程序中捕获溢出错误,请启用 /checked 标志。最后,如果您有一个代码块,其中溢出是可以接受的(因此不应触发运行时异常),则可以使用 unchecked 关键字。

Understanding Implicitly Typed Local Variables

了解隐式类型局部变量

Up until this point in the chapter, when you have been defining local variables, you have explicitly specified the underlying data type of each variable being declared.
在本章的这一点之前,当您定义局部变量时,您已经显式指定了要声明的每个变量的基础数据类型。

static void DeclareExplicitVars()
{
    // Explicitly typed local variables
    // 显式类型的局部变量
    // are declared as follows:
    // 声明如下:
    // dataType variableName = initialValue;
    // 数据类型变量名称 = 初始值;
    int myInt = 0;
    bool myBool = true;
    string myString = "Time, marches on...";
}

While many would argue that it is generally a good practice to explicitly specify the data type of each variable, the C# language does provide for implicitly typing local variables using the var keyword. The var keyword can be used in place of specifying a specific data type (such as int, bool, or string). When you do so, the compiler will automatically infer the underlying data type based on the initial value used to initialize the local data point.
虽然许多人会争辩说,显式指定每个变量的数据类型通常是一种很好的做法,但 C# 语言确实提供了使用 var 关键字隐式键入局部变量的功能。var 关键字可用于代替指定特定数据类型(如 int、bool 或字符串)。执行此操作时,编译器将根据用于初始化本地数据点的初始值自动推断基础数据类型。

To illustrate the role of implicit typing, create a new Console Application project named ImplicitlyTypedLocalVars and add it to your solution. Update the code in Program.cs to the following:
为了说明隐式键入的作用,请创建一个名为 ImplicitlyTypedLocalVars 的新控制台应用程序项目,并将其添加到解决方案中。将程序.cs中的代码更新为以下内容:

Add the following function to demonstrate implicit declarations:
添加以下函数来演示隐式声明:

Console.WriteLine("***** Fun with Implicit Typing *****");
static void DeclareImplicitVars()
{
    // Implicitly typed local variables
    // are declared as follows:
    // var variableName = initialValue;
    var myInt = 0;
    var myBool = true;
    var myString = "Time, marches on...";
}

■ Note strictly speaking, var is not a C# keyword. it is permissible to declare variables, parameters, and fields named var without compile-time errors. however, when the var token is used as a data type, it is contextually treated as a keyword by the compiler.
请注意,严格来说,var 不是 C# 关键字。允许声明名为 var 的变量、参数和字段,而不会产生编译时错误。但是,当 var 令牌用作数据类型时,编译器会在上下文中将其视为关键字。

In this case, the compiler is able to infer, given the initially assigned value, that myInt is, in fact, a System.Int32, myBool is a System.Boolean, and myString is indeed of type System.String. You can verify this by printing the type name via reflection. As you will see in much more detail in Chapter 17, reflection is the act of determining the composition of a type at runtime. For example, using reflection, you can determine the data type of an implicitly typed local variable. Update your method with the following code statements:
在这种情况下,编译器能够推断出,给定初始分配的值,myInt 实际上是一个 System.Int32,myBool 是一个 System.Boolean,而 myString 确实是 System.String 类型。可以通过反射打印类型名称来验证这一点。正如您将在第 17 章中看到的更详细内容,反射是在运行时确定类型组合的行为。例如,使用反射,可以确定隐式类型化局部变量的数据类型。使用以下代码语句更新方法:

static void DeclareImplicitVars()
{
    // Implicitly typed local variables.
    // 隐式类型的局部变量。
    var myInt = 0;
    var myBool = true;
    var myString = "Time, marches on...";

    // Print out the underlying type.
    // 打印出基础类型。
    Console.WriteLine("myInt is a: {0}", myInt.GetType().Name);
    Console.WriteLine("myBool is a: {0}", myBool.GetType().Name);
    Console.WriteLine("myString is a: {0}", myString.GetType().Name);
}

■ Note Be aware that you can use this implicit typing for any type including arrays, generic types (see Chapter 10), and your own custom types. You will see other examples of implicit typing over the course of this book.
注意 请注意,可以将此隐式类型用于任何类型,包括数组、泛型类型(请参阅第 10 章)和您自己的自定义类型。在本书的过程中,您将看到隐式键入的其他示例。

If you were to call the DeclareImplicitVars() method from the top-level statements, you would find the output shown here:
如果要从顶级语句调用 DeclareImplicitVars() 方法,则会找到如下所示的输出:

***** Fun with Implicit Typing *****
myInt is a: Int32
myBool is a: Boolean
myString is a: String

Declaring Numerics Implicitly

隐式声明数字

As stated earlier, whole numbers default to integers, and floating-point numbers default to doubles. Create a new method named DeclareImplicitNumerics, and add the following code to demonstrate implicit declaration of numerics:
如前所述,整数默认为整数,浮点数默认为双精度。创建一个名为 DeclareImplicitNumerics 的新方法,并添加以下代码来演示数字的隐式声明:

static void DeclareImplicitNumerics()
{
    // Implicitly typed numeric variables.
    // 隐式类型化数值变量。
    var myUInt = 0u;
    var myInt = 0;
    var myLong = 0L;
    var myDouble = 0.5; 
    var myFloat = 0.5F;
    var myDecimal = 0.5M;
    // Print out the underlying type.
    // 打印出基础类型。
    Console.WriteLine("myUInt is a: {0}", myUInt.GetType().Name);
    Console.WriteLine("myInt is a: {0}", myInt.GetType().Name);
    Console.WriteLine("myLong is a: {0}", myLong.GetType().Name);
    Console.WriteLine("myDouble is a: {0}", myDouble.GetType().Name);
    Console.WriteLine("myFloat is a: {0}", myFloat.GetType().Name);
    Console.WriteLine("myDecimal is a: {0}", myDecimal.GetType().Name);
}

Understanding Restrictions on Implicitly Typed Variables

了解对隐式类型变量的限制

There are various restrictions regarding the use of the var keyword. First, implicit typing applies only to local variables in a method or property scope. It is illegal to use the var keyword to define return values, parameters, or field data of a custom type. For example, the following class definition will result in various compile-time errors:
关于 var 关键字的使用有各种限制。首先,隐式类型仅适用于方法或属性范围内的局部变量。使用 var 关键字定义自定义类型的返回值、参数或字段数据是非法的。例如,以下类定义将导致各种编译时错误:

class ThisWillNeverCompile
{
    // Error! var cannot be used as field data!
    // 错误!VaR 不能用作字段数据!
    private var myInt = 10;
    // Error! var cannot be used as a return value
    // 错误!var 不能用作返回值
    // or parameter type!
    // 或参数类型!
    public var MyMethod(var x, var y) { }
}

Also, local variables declared with the var keyword must be assigned an initial value at the exact time of declaration and cannot be assigned the initial value of null. This last restriction should make sense, given that the compiler cannot infer what sort of type in memory the variable would be pointing to based onlyon null.
此外,使用 var 关键字声明的局部变量必须在声明的确切时间分配一个初始值,并且不能为其分配初始值 null。最后一个限制应该是有意义的,因为编译器无法推断变量仅基于内存中的哪种类型。在空值上。

// Error! Must assign a value! 
// 错误!必须分配一个值!
var myData;

// Error! Must assign value at exact time of declaration! 
// 错误!必须在声明的确切时间分配值!
var myInt;
myInt = 0;

// Error! Can't assign null as initial value! 
// 错误!无法将 null 指定为初始值!
var myObj = null;

It is permissible, however, to assign an inferred local variable to null after its initial assignment (provided it is a reference type).
但是,允许在初始赋值后将推断的局部变量赋值为 null(前提是它是引用类型)。

// OK, if SportsCar is a reference type! 
// 好的,如果跑车是参考类型!

var myCar = new SportsCar();
myCar = null;

Furthermore, it is permissible to assign the value of an implicitly typed local variable to the value of other variables, implicitly typed or not.
此外,允许将隐式类型局部变量的值分配给其他变量的值,无论是否隐式类型。

// Also OK! 也行!
var myInt = 0;

var anotherInt = myInt;
string myString = "Wake up!"; 
var myData = myString;

Also, it is permissible to return an implicitly typed local variable to the caller, provided the method return type is the same underlying type as the var-defined data point.
此外,允许向调用方返回隐式类型的局部变量,前提是该方法返回类型与 var 定义的数据点的基础类型相同。

static int GetAnInt()
{
    var retVal = 9; return retVal;
}

Implicit Typed Data Is Strongly Typed Data

隐式类型化数据是强类型数据

Be aware that implicit typing of local variables results in strongly typed data. Therefore, use of the var keyword is not the same technique used with scripting languages (such as JavaScript or Perl) or the COM Variant data type, where a variable can hold values of different types over its lifetime in a program (often termed dynamic typing).
请注意,局部变量的隐式键入会导致强类型数据。因此,var 关键字的使用与脚本语言(如 JavaScript 或 Perl)或 COM Variant 数据类型使用的技术不同,其中变量可以在程序中的生存期内保存不同类型的值(通常称为动态类型)。

■ Note C# does allow for dynamic typing in C# using a keyword called—surprise, surprise—dynamic. You will learn about this aspect of the language in Chapter 17.
注意 C# 确实允许在 C# 中使用名为 - 惊喜,惊喜 - 动态的关键字进行动态键入。您将在第 17 章中了解该语言的这一方面。

Rather, type inference keeps the strongly typed aspect of the C# language and affects only the declaration of variables at compile time. After that, the data point is treated as if it were declared with that type; assigning a value of a different type into that variable will result in a compile-time error.
相反,类型推断保留了 C# 语言的强类型方面,并且仅影响编译时变量的声明。之后,数据点将被视为使用该类型声明;将不同类型的值分配给该变量将导致编译时错误。

static void ImplicitTypingIsStrongTyping()
{
    // The compiler knows "s" is a System.String.
    // 编译器知道“s”是一个System.String。
    var s = "This variable can only hold string data!";
    s = "This is fine...";

    // Can invoke any member of the underlying type.
    // 可以调用基础类型的任何成员。 
    string upper = s.ToUpper();

    // Error! Can't assign numerical data to a string!
    // 错误!无法为字符串分配数值数据!
    s = 44;
}

Understanding the Usefulness of Implicitly Typed Local Variables

了解隐式类型局部变量的有用性

Now that you have seen the syntax used to declare implicitly typed local variables, I am sure you are wondering when to make use of this construct. First, using var to declare local variables simply for the sake of doing so brings little to the table. Doing so can be confusing to others reading your code because it becomes harder to quickly determine the underlying data type and, therefore, more difficult to understand the overall functionality of the variable. So, if you know you need an int, declare an int!
现在您已经了解了用于声明隐式类型局部变量的语法,我相信您想知道何时使用此构造。首先,使用 var 来声明局部变量只是为了这样做,这几乎没有什么好处。这样做可能会让阅读代码的其他人感到困惑,因为快速确定基础数据类型变得更加困难,因此更难理解变量的整体功能。所以,如果你知道你需要一个int,声明一个int!

However, as you will see beginning in Chapter 13, the LINQ technology set makes use of query expressions that can yield dynamically created result sets based on the format of the query itself. In these cases, implicit typing is extremely helpful because you do not need to explicitly define the type that a query may return, which in some cases would be literally impossible to do. Without getting hung up on the following LINQ example code, see whether you can figure out the underlying data type of subset:
但是,正如您将在第 13 章开始看到的那样,LINQ 技术集利用了查询表达式,这些表达式可以根据查询本身的格式生成动态创建的结果集。在这些情况下,隐式类型非常有用,因为您不需要显式定义查询可能返回的类型,这在某些情况下实际上是不可能的。无需纠结于以下 LINQ 示例代码,请查看是否可以确定子集的基础数据类型:

static void LinqQueryOverInts()
{
    int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };
    // LINQ query!
    var subset = from i in numbers where i < 10 select i;
    Console.Write("Values in subset: ");
    foreach (var i in subset)
    {
        Console.Write("{0} ", i);
    }
    Console.WriteLine();
    // Hmm...what type is subset?
    // 嗯。。。子集是什么类型?
    Console.WriteLine("subset is a: {0}", subset.GetType().Name);
    Console.WriteLine("subset is defined in: {0}", subset.GetType().Namespace);
}

You might be assuming that the subset data type is an array of integers. That seems to be the case, but, in fact, it is a low-level LINQ data type that you would never know about unless you have been doing LINQ for a long time or you open the compiled image in ildasm.exe. The good news is that when you are using LINQ, you seldom (if ever) care about the underlying type of the query’s return value; you will simply assign the value to an implicitly typed local variable.
您可能假设子集数据类型是整数数组。情况似乎是这样,但实际上,它是一种低级 LINQ 数据类型,除非您已经执行 LINQ 很长时间或在 ildasm.exe 中打开编译后的映像,否则您永远不会知道它。好消息是,在使用 LINQ 时,您很少(如果有的话)关心查询返回值的基础类型;您只需将值分配给隐式类型的局部变量。

In fact, it could be argued that the only time you would make use of the var keyword is when defining data returned from a LINQ query. Remember, if you know you need an int, just declare an int! Overuse of implicit typing (via the var keyword) is considered by most developers to be poor style in production code.
事实上,可以说,唯一使用 var 关键字的时间是在定义从 LINQ 查询返回的数据时。记住,如果你知道你需要一个int,只需声明一个int!大多数开发人员认为过度使用隐式类型(通过 var 关键字)是生产代码中的不良风格。

Working with C# Iteration Constructs

使用 C# 迭代构造

All programming languages provide ways to repeat blocks of code until a terminating condition has been met. Regardless of which language you have used in the past, I would guess the C# iteration statements should not raise too many eyebrows and should require little explanation. C# provides the following four iteration constructs:
所有编程语言都提供了重复代码块的方法,直到满足终止条件。无论您过去使用哪种语言,我猜 C# 迭代语句都不应该引起太多的注意,并且应该不需要解释。C# 提供以下四个迭代构造:

  • for loop
  • foreach/in loop
  • while loop
  • do/while loop

Let’s quickly examine each looping construct in turn, using a new Console Application project named IterationsAndDecisions.
让我们使用名为 IterationsAndDecisions 的新控制台应用程序项目依次快速检查每个循环构造。

■ Note i will keep this section of the chapter short and to the point, as i am assuming you have experience using similar keywords (if, for, switch, etc.) in your current programming language. if you require more information, look up the topics “iteration statements (C# reference),” “Jump statements (C# reference),” and “selection statements (C# reference)” within the C# documentation.
请注意,我将保持本章的这一部分简短而中肯,因为我假设您有在当前编程语言中使用类似关键字(if,for,switch等)的经验。如果需要更多信息,请在 C# 文档中查找主题“迭代语句(C# 参考)”、“跳转语句(C# 参考)”和“选择语句(C# 参考)”。

Using the for Loop

使用 for 循环

When you need to iterate over a block of code a fixed number of times, the for statement provides a good deal of flexibility. In essence, you are able to specify how many times a block of code repeats itself, as well as the terminating condition. Without belaboring the point, here is a sample of the syntax:
当您需要对代码块进行固定次数的迭代时,for 语句提供了很大的灵活性。实质上,您可以指定代码块重复的次数以及终止条件。在不赘述这一点的情况下,下面是语法示例:

 
// A basic for loop.
// 一个基本的 for 循环。
static void ForLoopExample()
{
    // Note! "i" is only visible within the scope of the for loop.
    // 注意!“i”仅在 for 循环的范围内可见。
    for (int i = 0; i < 4; i++)
    {
        Console.WriteLine("Number is: {0} ", i);
    }
    // "i" is not visible here.
    //“i”在这里不可见。
}

All your old C, C++, and Java tricks still hold when building a C# for statement. You can create complex terminating conditions, build endless loops, loop in reverse (via the -- operator), and use the goto, continue, and break jump keywords.
在构建 C# for 语句时,所有旧的 C、C++ 和 Java 技巧仍然适用。您可以创建复杂的终止条件、构建无限循环、反向循环(通过 -- 运算符),并使用 goto、继续和中断跳转关键字。

Using the foreach Loop使用 foreach 循环

The C# foreach keyword allows you to iterate over all items in a container without the need to test for an upper limit. Unlike a for loop, however, the foreach loop will walk the container only in a linear (n+1) fashion (thus, you cannot go backward through the container, skip every third element, or whatnot).
C# foreach 关键字允许循环访问容器中的所有项,而无需测试上限。但是,与 for 循环不同的是,foreach 循环将仅以线性 (n+1) 方式遍历容器(因此,您不能向后浏览容器,跳过每隔三个元素,或者其他什么)。

However, when you simply need to walk a collection item by item, the foreach loop is the perfect choice. Here are two examples using foreach—one to traverse an array of strings and the other to traverse an array of integers. Notice that the data type before the in keyword represents the type of data in the container.
但是,当您只需要逐项浏览集合时,foreach 循环是完美的选择。下面是两个使用 foreach 的示例,一个用于遍历字符串数组,另一个用于遍历整数数组。请注意,in 关键字之前的数据类型表示容器中的数据类型。

 
// Iterate array items using foreach.
// 使用 foreach 迭代数组项。
static void ForEachLoopExample()
{
    string[] carTypes = { "Ford", "BMW", "Yugo", "Honda" };
    foreach (string c in carTypes)
    {
        Console.WriteLine(c);
    }
    int[] myInts = { 10, 20, 30, 40 };
    foreach (int i in myInts)
    {
        Console.WriteLine(i);
    }
}

The item after the in keyword can be a simple array (seen here) or, more specifically, any class implementing the IEnumerable interface. As you will see in Chapter 10, the .NET Core base class libraries ship with a number of collections that contain implementations of common abstract data types (ADTs). Any of these items (such as the generic List) can be used within a foreach loop.
in 关键字后面的项可以是一个简单的数组(见此处),或者更具体地说,是实现 IEnumerable 接口的任何类。正如您将在第 10 章中看到的,.NET Core 基类库附带了许多集合,这些集合包含常见抽象数据类型 (ADT) 的实现。这些项中的任何一项(如通用 List)都可以在 foreach 循环中使用。

Using Implicit Typing Within foreach Constructs

在 foreach 构造中使用隐式类型

It is also possible to use implicit typing within a foreach looping construct. As you would expect, the compiler will correctly infer the correct “type of type.” Recall the LINQ example method shown earlier in this chapter. Given that you do not know the exact underlying data type of the subset variable, you can iterate over the result set using implicit typing.
也可以在 foreach 循环构造中使用隐式类型。如您所料,编译器将正确推断正确的“类型类型”。回顾本章前面所示的 LINQ 示例方法。如果您不知道子集变量的确切基础数据类型,则可以使用隐式类型循环访问结果集。

 
static void LinqQueryOverInts()
{
    int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };
    // LINQ query!
    var subset = from i in numbers where i < 10 select i;
    Console.Write("Values in subset: ");
    foreach (var i in subset)
    {
        Console.Write("{0} ", i);
    }
}

Using the while and do/while Looping Constructs

使用 while 和 do/while 循环构造

The while looping construct is useful should you want to execute a block of statements until some terminating condition has been reached. Within the scope of a while loop, you will need to ensure this terminating event is indeed established; otherwise, you will be stuck in an endless loop. In the following example, the message "In while loop" will be continuously printed until the user terminates the loop by entering yes at the command prompt:
如果要执行语句块直到达到某个终止条件,则 while 循环构造很有用。在 while 循环的范围内,您需要确保确实建立了此终止事件;否则,您将陷入无休止的循环。在以下示例中,将连续打印消息“In while loop”,直到用户通过在命令提示符下输入 yes 来终止循环:

 
static void WhileLoopExample()
{
    string userIsDone = "";
    // Test on a lower-class copy of the string.
    // 在字符串的较低类副本上进行测试。
    while (userIsDone.ToLower() != "yes")
    {
        Console.WriteLine("In while loop");
        Console.Write("Are you done? [yes] [no]: ");
        userIsDone = Console.ReadLine();
    }
}

Closely related to the while loop is the do/while statement. Like a simple while loop, do/while is used when you need to perform some action an undetermined number of times. The difference is that do/while loops are guaranteed to execute the corresponding block of code at least once. In contrast, it is possible that a simple while loop may never execute if the terminating condition is false from the onset.
与 while 循环密切相关的是 do/while 语句。 就像一个简单的 while 循环一样,当您需要执行某些操作的次数不确定时,使用 do/while。 不同之处在于 do/while 循环保证至少执行一次相应的代码块。相反,如果终止条件从一开始就为 false,则简单的 while 循环可能永远不会执行。

 
static void DoWhileLoopExample()
{
    string userIsDone = "";
    do
    {
        Console.WriteLine("In do/while loop");
        Console.Write("Are you done? [yes] [no]: ");
        userIsDone = Console.ReadLine();
    } while (userIsDone.ToLower() != "yes"); // Note the semicolon!
}

A Quick Discussion About Scope

关于范围的快速讨论

Like all languages based on C (C#, Java, etc.), a scope is created using curly braces. You have already seen this in many of the examples so far, including namespaces, classes, and methods. The iteration and decision constructs also operate in a scope, as in the following example:
像所有基于 C(C#、Java 等)的语言一样,作用域是使用大括号创建的。到目前为止,您已经在许多示例中看到了这一点,包括命名空间、类和方法。迭代和决策构造也在作用域中运行,如以下示例所示:

 
for(int i = 0; i < 4; i++)
{
    Console.WriteLine("Number is: {0} ", i);
}

For these constructs (both in the previous section and the next section), it is permissible to not use curly braces. In other words, the following code is exactly the same as the previous example:
对于这些构造(在上一节和下一节中),允许不使用大括号。换句话说,以下代码与前面的示例完全相同:

 
for(int i = 0; i < 4; i++) 
    Console.WriteLine("Number is: {0} ", i);

While this is permissible, it is typically not a good idea. The problem is not the one-line statement, but the statement that goes from one line to more than one line. Without the braces, mistakes could be made when expanding the code within the iteration/decision constructs. For example, the following two examples are not the same:
虽然这是允许的,但这通常不是一个好主意。问题不在于单行语句,而在于从一行到多行的语句。如果没有大括号,在迭代/决策结构中扩展代码时可能会出错。例如,以下两个示例并不相同:

 
// 带括号
for (int i = 0; i < 4; i++)
{
    Console.WriteLine("Number is: {0} ", i);
    Console.WriteLine("Number plus 1 is: {0} ", i + 1);
}

// 不带括号
for (int i = 0; i < 4; i++)
    Console.WriteLine("Number is: {0} ", i);
    Console.WriteLine("Number plus 1 is: {0} ", i + 1);

If you are lucky (like in this example), the additional line of code generates a compilation error, since the variable i is defined only in the scope of the for loop. If you are unlucky, you are executing code that does not get flagged as a compiler error, but is a logic error, which is harder to find and debug.
如果幸运的话(如本例所示),额外的代码行会生成编译错误,因为变量 i 仅在 for 循环的范围内定义。如果运气不好,您正在执行的代码不会被标记为编译器错误,而是逻辑错误,这更难查找和调试。

Working with Decision Constructs and the Relational/ Equality Operators

使用决策构造和关系/相等运算符

Now that you can iterate over a block of statements, the next related concept is how to control the flow of program execution. C# defines two simple constructs to alter the flow of your program, based on various contingencies:
现在您可以迭代语句块,下一个相关概念是如何控制程序执行流。C# 定义了两个简单的构造,用于根据各种意外情况更改程序流:

  • The if/else statement
  • The switch statement

■ Note C# 7 extends the is expression and switch statements with a technique called pattern matching. the basics of how these extensions affect if/else and switch statements are shown here for completeness. these extensions will make more sense after reading Chapter 6, which covers base class/derived class rules, casting, and the standard is operator.
注意 C# 7 使用称为模式匹配的技术扩展了 is 表达式和开关语句。为了完整起见,此处显示了这些扩展如何影响 if/else 和 switch 语句的基础知识。 阅读第 6 章后,这些扩展将更有意义,该章涵盖了基类/派生类规则、强制转换和标准 IS 运算符。

Using the if/else Statement

使用 if/else 语句

First up is the if/else statement. Unlike in C and C++, the if/else statement in C# operates only on Boolean expressions, not ad hoc values such as –1 or 0.
首先是 if/else 语句。与 C 和 C++ 不同,C# 中的 if/else 语句仅对布尔表达式进行操作,而不对 –1 或 0 等即席值进行操作。

Using Equality and Relational Operators

使用相等运算符和关系运算符

C# if/else statements typically involve the use of the C# operators shown in Table 3-8 to obtain a literal Boolean value.
C# if/else 语句通常涉及使用表 3-8 中所示的 C# 运算符来获取文本布尔值。

Table 3-8. C# Relational and Equality Operators
表 3-8. C# 关系运算符和相等运算符

C# Equality/Relational Operator Example Usage Meaning in Life
== if(age == 30) Returns true only if each expression is the same
仅当每个表达式相同时才返回 true
!= if("Foo" != myStr) Returns true only if each expression is different
仅当每个表达式不同时才返回 true
< if(bonus < 2000) Returns true if expression A (bonus) is less than expression B (2000)
如果表达式 A(奖金)小于表达式 B (2000)
> if(bonus > 2000) Returns true if expression A (bonus) is greater than expression B (2000)
如果表达式 A(奖金)大于表达式 B (2000)
<= if(bonus <= 2000) Returns true if expression A (bonus) is less than or equal to expression B (2000)
如果表达式 A(奖金)小于或等于表达式 B (2000)
>= if(bonus >= 2000) Returns true if expression A (bonus) is greater than or equal to expression B (2000)
如果表达式 A(奖金)大于或等于表达式 B (2000)

Again, C and C++ programmers need to be aware that the old tricks of testing a condition for a value not equal to zero will not work in C#. Let’s say you want to see whether the string you are working with is longer than zero characters. You might be tempted to write this:
如果表达式 A(奖金)大于或等于表达式 B (2000)

static void IfElseExample()
{
    // This is illegal, given that Length returns an int, not a bool.
    // 这是非法的,因为 Length 返回的是 int,而不是布尔值。
    string stringData = "My textual data";
    if (stringData.Length)
    {
        Console.WriteLine("string is greater than 0 characters");
    }
    else
    {
        Console.WriteLine("string is not greater than 0 characters");
    }
    Console.WriteLine();
}

If you want to use the String.Length property to determine truth or falsity, you need to modify your conditional expression to resolve to a Boolean.
如果要使用 String.Length 属性来确定真假,则需要修改条件表达式以解析为布尔值。

// Legal, as this resolves to either true or false.
// 合法,因为这解析为真或假。
if(stringData.Length > 0)
{
    Console.WriteLine("string is greater than 0 characters");
}

Using if/else with Pattern Matching (New 7.0)

将 if/else 与模式匹配结合使用(新版 7.0)

New in C# 7.0, pattern matching is allowed in if/else statements. Pattern matching allows code to inspect an object for certain traits and properties and make decisions based on the (non)existence of those properties and traits. Do not worry if you are new to object-oriented programming; the previous sentence will be explained in great detail in later chapters. Just know (for now) that you can check the type of an object using the is keyword, assign that object to a variable if the pattern matches, and then use that variable.
C# 7.0 中的新增功能是 if/else 语句中允许模式匹配。模式匹配允许代码检查对象的某些特征和属性,并根据这些属性和特征的(不存在)做出决策。如果您不熟悉面向对象编程,请不要担心;前一句将在后面的章节中详细解释。只要知道(现在)您可以使用is关键字检查对象的类型,如果模式匹配,则将该对象分配给变量,然后使用该变量。

The IfElsePatternMatching method examines two object variables and determines if they are a string or an int and then prints the results to the console:
IfElsePatternMatch 方法检查两个对象变量并确定它们是否为字符串或 int,然后将结果打印到控制台:

static void IfElsePatternMatching()
{
    Console.WriteLine("===If Else Pattern Matching ===");
    object testItem1 = 123;
    object testItem2 = "Hello";
    if (testItem1 is string myStringValue1)
    {
        Console.WriteLine($"{myStringValue1} is a string");
    }
    if (testItem1 is int myValue1)
    {
        Console.WriteLine($"{myValue1} is an int");
    }
    if (testItem2 is string myStringValue2)
    {
        Console.WriteLine($"{myStringValue2} is a string");
    }
    if (testItem2 is int myValue2)
    {
        Console.WriteLine($"{myValue2} is an int");
    }
    Console.WriteLine();
}

Making Pattern Matching Improvements (New 9.0)

进行模式匹配改进(新 9.0)

C# 9.0 has introduced a host of improvements to pattern matching, as shown in Table 3-9.
C# 9.0 对模式匹配进行了大量改进,如表 3-9 所示。

Table 3-9. Pattern Matching Improvements
表 3-9. 模式匹配改进

Pattern Meaning in Life
Type patterns Checks if a variable is a type
检查变量是否为类型
Parenthesized patterns Enforces or emphasizes the precedence of pattern combinations
强制或强调模式组合的优先级
Conjuctive (and) patterns Requires both patterns to match
要求两种模式匹配
Disjunctive (or) patterns Requires either pattern to match
要求任一模式匹配
Negated (not) patterns Requires a pattern does not match
要求模式不匹配
Relational patterns Requires input to be less than, less than or equal, greater than, or greater than or equal
要求输入小于、小于或等于、大于或大于或等于
Pattern combinator Allows multiple patterns to be used together.允许多个模式一起使用。

The updated IfElsePatternMatchingUpdatedInCSharp9() shows these new patterns in action:
更新的 IfElsePatternMatchingUpdateInCSharp9() 显示了这些新模式的实际应用:

static void IfElsePatternMatchingUpdatedInCSharp9()
{
    Console.WriteLine("======= C# 9 If Else Pattern Matching Improvements =======");
    object testItem1 = 123;
    Type t = typeof(string);
    char c = 'f';
    // Type patterns
    // 键入模式
    if (t is Type)
    {
        Console.WriteLine($"{t} is a Type");
    }
    // Relational, Conjuctive, and Disjunctive patterns
    // 关系、共轭和析取模式(什么鬼?)
    if (c is >= 'a' and <= 'z' or >= 'A' and <= 'Z')
    {
        Console.WriteLine($"{c} is a character");
    };
    // Parenthesized patterns
    // 括号图案
    if (c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',')
    {
        Console.WriteLine($"{c} is a character or separator");
    };
    // Negative patterns
    // 负面模式
    if (testItem1 is not string)
    {
        Console.WriteLine($"{testItem1} is not a string");
    }
    if (testItem1 is not null)
    {
        Console.WriteLine($"{testItem1} is not null");
    }
    Console.WriteLine();
}

Using the Conditional Operator (Updated 7.2, 9.0)

使用条件运算符(7.2、9.0 更新)

The conditional operator (?:), also known as the ternary conditional operator, is a shorthand method of writing a simple if/else statement. The syntax works like this:
条件运算符 (?:),也称为三元条件运算符,是编写简单 if/else 语句的简写方法。语法的工作方式如下:

condition ? first_expression : second_expression;

The condition is the conditional test (the if part of the if/else statement). If the test passes, then the code immediately after the question mark (?) is executed. If the test does not evaluate to true, the code after the colon (the else part of the if/else statement) is executed. The previous code example can be written using the conditional operator like this:
条件是条件测试(if/else 语句的 if 部分)。如果测试通过,则紧跟在问号 (?) 之后的代码将执行。如果测试的计算结果未为 true,则执行冒号(if/else 语句的 else 部分)后面的代码。前面的代码示例可以使用条件运算符编写,如下所示:

static void ExecuteIfElseUsingConditionalOperator()
{
    string stringData = "My textual data";
    Console.WriteLine(stringData.Length > 0
    ? "string is greater than 0 characters"
    : "string is not greater than 0 characters");
    Console.WriteLine();
}

There are some restrictions to the conditional operator. First, both types of first_expression and second_expression must have implicit conversions to from one to another, or, new in C# 9.0, each must have an implicit conversion to a target type. Second, the conditional operator can be used only in assignment statements. The following code will result in the compiler error “Only assignment, call, increment, decrement, and new object expressions can be used as a statement”:
对条件运算符有一些限制。首先,两种类型的first_expression和second_expression都必须具有从一种到另一种的隐式转换,或者,在 C# 9.0 中,每种类型都必须具有到目标类型的隐式转换。其次,条件运算符只能在赋值语句中使用。以下代码将导致编译器错误“只有赋值、调用、递增、递减和新对象表达式可以用作语句”:

stringData.Length > 0
? Console.WriteLine("string is greater than 0 characters")
: Console.WriteLine("string is not greater than 0 characters");

New in C# 7.2, the conditional operator can be used to return a reference to the result of the condition.Take the following example, which uses two forms of the conditional operator by ref:
条件运算符是 C# 7.2 中的新增功能,可用于返回对条件结果的引用。以以下示例为例,该示例通过 ref 使用两种形式的条件运算符:

static void ConditionalRefExample()
{
    var smallArray = new int[] { 1, 2, 3, 4, 5 };
    var largeArray = new int[] { 10, 20, 30, 40, 50 };
    int index = 7;
    ref int refValue = ref ((index < 5)
    ? ref smallArray[index]
    : ref largeArray[index - 5]);
    refValue = 0; 
    index = 2;
    ((index < 5)
    ? ref smallArray[index]
    : ref largeArray[index - 5]) = 100;
    Console.WriteLine(string.Join(" ", smallArray));
    Console.WriteLine(string.Join(" ", largeArray));
}

If you are not familiar with the ref keyword, do not worry too much at this point, as it will be covered in depth in the next chapter. To sum up, the first example returns a reference to the array location checked with the condition and assigns the refValue variable to that reference. Think of the reference conceptually as a point to the location in the array and not the actual value of the position of the array. This allows for changing of the array’s value in that position directly by changing the value assigned to the variable. The result of setting the value of the refValue variable to zero changes the second array’s values to 10,20,0,40,50. The second example updates the first array’s second value to 100, resulting in 1,2,100,4,5.
如果您不熟悉 ref 关键字,此时不要太担心,因为它将在下一章中深入介绍。总而言之,第一个示例返回对使用该条件检查的数组位置的引用,并将 refValue 变量分配给该引用。从概念上讲,将引用视为数组中位置的点,而不是数组位置的实际值。这允许通过更改分配给变量的值来直接更改该位置的数组值。这将 refValue 变量的值设置为零的结果会将第二个数组的值更改为 10,20,0,40,50。第二个示例将第一个数组的第二个值更新为 100,结果为 1,2,100,4,5。

Using Logical Operators

使用逻辑运算符

An if statement may be composed of complex expressions as well and can contain else statements to perform more complex testing. The syntax is identical to C (and C++) and Java. To build complex expressions, C# offers an expected set of logical operators, as shown in Table 3-10.
if 语句也可以由复杂的表达式组成,并且可以包含 else 语句来执行更复杂的测试。语法与C(和C++)和Java相同。为了生成复杂的表达式,C# 提供了一组预期的逻辑运算符,如表 3-10 所示。

Table 3-10. C# Logical Operators
表 3-10. C# 逻辑运算符

Operator Example Meaning in Life
&& if(age == 30 && name == "Fred") AND operator. Returns true if all expressions are true.
和运算符。如果所有表达式都为 true,则返回 true。
|| if(age == 30 || name == "Fred") OR operator. Returns true if at least one expression is true.
OR 运算符。如果至少有一个表达式为 true,则返回 true。
! if(!myBool) NOT operator. Returns true if false, or false if true.
不是运算符。如果为假,则返回 true,如果为真,则返回 false。

■ Note the && and || operators both “short-circuit” when necessary. this means that after a complex expression has been determined to be false, the remaining subexpressions will not be checked. if you require all expressions to be tested regardless, you can use the related & and | operators.
请注意 && 和 || 必要时,操作员都会“短路”。这意味着在确定复杂表达式为 false 后,将不检查其余子表达式。如果你要求无论如何都要测试所有表达式,你可以使用相关的 &和 | 运营商。

Using the switch Statement

使用开关语句

The other simple selection construct offered by C# is the switch statement. As in other C-based languages, the switch statement allows you to handle program flow based on a predefined set of choices. For example, the following logic prints a specific string message based on one of two possible selections (the default case handles an invalid selection):
C# 提供的另一个简单选择构造是 switch 语句。与其他基于 C 的语言一样,switch 语句允许您基于一组预定义的选择来处理程序流。例如,以下逻辑根据两个可能的选择之一打印特定的字符串消息(默认情况处理无效选择):

// Switch on a numerical value.
// 数值开关。
static void SwitchExample()
{
    Console.WriteLine("1 [C#], 2 [VB]");
    Console.Write("Please pick your language preference: ");
    string langChoice = Console.ReadLine();
    int n = int.Parse(langChoice);
    switch (n)
    {
        case 1:
            Console.WriteLine("Good choice, C# is a fine language.");
            break;
        case 2:
            Console.WriteLine("VB: OOP, multithreading, and more!");
            break;
        default:
            Console.WriteLine("Well...good luck with that!");
            break;
    }
}

■ Note C# demands that each case (including default) that contains executable statements have a terminating return, break, or goto to avoid falling through to the next statement.
注意 C# 要求包含可执行语句的每个事例(包括默认值)都具有终止回车、中断或 goto,以避免落入下一条语句。

One nice feature of the C# switch statement is that you can evaluate string data in addition to numeric data. In fact, all versions of C# can evaluate char, string, bool, int, long, and enum data types. As you will see in the next section, C# 7 adds additional capabilities. Here is an updated switch statement that evaluates a string variable:
C# switch 语句的一个很好的功能是,除了数值数据之外,还可以计算字符串数据。事实上,所有版本的 C# 都可以计算字符、字符串、布尔值、整数、长整型和枚举数据类型。正如您将在下一节中看到的,C# 7 添加了其他功能。下面是一个更新的 switch 语句,用于计算字符串变量:

static void SwitchOnStringExample()
{
    Console.WriteLine("C# or VB");
    Console.Write("Please pick your language preference: ");
    string langChoice = Console.ReadLine();
    switch (langChoice.ToUpper())
    {
        case "C#":
            Console.WriteLine("Good choice, C# is a fine language.");
            break;
        case "VB":
            Console.WriteLine("VB: OOP, multithreading and more!");
            break;
        default:
            Console.WriteLine("Well...good luck with that!");
            break;
    }
}

It is also possible to switch on an enumeration data type. As you will see in Chapter 4, the C# enum keyword allows you to define a custom set of name-value pairs. To whet your appetite, consider the following final helper function, which performs a switch test on the System.DayOfWeek enum. You will notice some syntax I have not yet examined, but focus on the issue of switching over the enum itself; the missing pieces will be filled in over the chapters to come.
也可以打开枚举数据类型。正如您将在第 4 章中看到的,C# 枚举关键字允许您定义一组自定义的名称/值对。为了激发您的胃口,请考虑以下最后一个帮助程序函数,该函数在 System.DayOfWeek 枚举上执行开关测试。你会注意到一些我还没有检查的语法,但重点是切换枚举本身的问题;缺失的部分将在以后的章节中填补。

static void SwitchOnEnumExample()
{
    Console.Write("Enter your favorite day of the week: ");
    DayOfWeek favDay;
    try
    {
        favDay = (DayOfWeek)Enum.Parse(typeof(DayOfWeek), Console.ReadLine());
    }
    catch (Exception)
    {
        Console.WriteLine("Bad input!");
        return;
    }
    switch (favDay)
    {
        case DayOfWeek.Sunday:
            Console.WriteLine("Football!!");
            break;
        case DayOfWeek.Monday:
            Console.WriteLine("Another day, another dollar");
            break;
        case DayOfWeek.Tuesday:
            Console.WriteLine("At least it is not Monday");
            break;
        case DayOfWeek.Wednesday:
            Console.WriteLine("A fine day.");
            break;
        case DayOfWeek.Thursday:
            Console.WriteLine("Almost Friday...");
            break;
        case DayOfWeek.Friday:
            Console.WriteLine("Yes, Friday rules!");
            break;
        case DayOfWeek.Saturday:
            Console.WriteLine("Great day indeed.");
            break;
    }
    Console.WriteLine();
}

Falling through from one case statement to another case statement is not allowed, but what if multiple case statements should produce the same result? Fortunately, they can be combined, as the following code snippet demonstrates:
不允许从一个案例陈述跌到另一个案例陈述,但如果多个案例陈述应该产生相同的结果怎么办?幸运的是,它们可以组合在一起,如以下代码片段所示:

 
case DayOfWeek.Saturday:
case DayOfWeek.Sunday:
    Console.WriteLine("It’s the weekend!"); 
    break;

If any code were included between the case statements, the compiler would throw an error. As long as they are consecutive statements, as shown earlier, case statements can be combined to share common code.
如果在 case 语句之间包含任何代码,编译器将引发错误。只要它们是连续语句,如前所述,就可以组合 case 语句以共享公共代码。

In addition to the return and break statements shown in the previous code samples, the switch statement also supports using a goto to exit a case condition and execute another case statement. While this is supported, it is pretty universally thought of as an anti-pattern and not generally used. Here is an example of using the goto statement in a switch block:
除了前面的代码示例中显示的返回和中断语句外,switch 语句还支持使用 goto 退出案例条件并执行另一个案例语句。虽然这得到了支持,但它被普遍认为是一种反模式,通常不使用。下面是在开关块中使用 goto 语句的示例:

 
static void SwitchWithGoto()
{
    var foo = 5;
    switch (foo)
    {
        case 1:
            // do something
            // 做点什么
            goto case 2;
        case 2:
            // do something else
            // 做点别的什么
            break;
        case 3:
            // yet another action
            // 有一个动作
            goto default;
        default:
            // default action
            // 默认动作
            break;
    }
}

Performing switch Statement Pattern Matching (New 7.0, Updated 9.0)

执行交换机语句模式匹配(新版 7.0,更新的 9.0)

Prior to C# 7, match expressions in switch statements were limited to comparing a variable to constant values, sometimes referred to as the constant pattern. In C# 7, switch statements can also employ the type pattern, where case statements can evaluate the type of the variable being checked and case expressions are no longer limited to constant values. The rule that each case statement must be terminated with a return or break still applies; however, goto statements are not supported using the type pattern.
在 C# 7 之前,switch 语句中的匹配表达式仅限于将变量与常量值进行比较,有时称为常量模式。在 C# 7 中,switch 语句还可以采用类型模式,其中 case 语句可以计算要检查的变量的类型,并且 case 表达式不再局限于常量值。每个 case 语句必须以返回或中断终止的规则仍然适用;但是,不支持使用 GoTo 语句使用类型模式。

■ Note if you are new to object-oriented programming, this section might be a little confusing. it will all come together in Chapter 6, when you revisit the new pattern matching features of C# 7 in the context of classes and base classes. For now, just understand that there is a powerful new way to write switch statements.
注意 如果您不熟悉面向对象编程,本节可能会有点令人困惑。 在第 6 章中,当您在类和基类的上下文中重新访问 C# 7 的新模式匹配功能时,所有这些内容将汇集在一起。现在,只要了解有一种强大的新方法来编写 switch 语句。

Add another method named ExecutePatternMatchingSwitch() and add the following code:
添加另一个名为 ExecutePatternMatchingSwitch() 的方法,并添加以下代码:

 
static void ExecutePatternMatchingSwitch()
{
    Console.WriteLine("1 [Integer (5)], 2 [String (\"Hi\")], 3 [Decimal (2.5)]");
    Console.Write("Please choose an option: ");
    string userChoice = Console.ReadLine();
    object choice;
    // This is a standard constant pattern switch statement to set up the example
    // 这是一个标准的常量模式开关语句,用于设置示例开关
    switch (userChoice)
    {
        case "1":
            choice = 5;
            break;
        case "2":
            choice = "Hi";
            break;
        case "3":
            choice = 2.5M;
            break;
        default:
            choice = 5;
            break;
    }

    //This is new the pattern matching switch statement
    // 这是新的模式匹配开关语句
    switch (choice)
    {
        case int i:
            Console.WriteLine("Your choice is an integer.");
            break;
        case string s:
            Console.WriteLine("Your choice is a string.");
            break;
        case decimal d:
            Console.WriteLine("Your choice is a decimal.");
            break;
        default:
            Console.WriteLine("Your choice is something else");
            break;
    }
    Console.WriteLine();
}

The first switch statement is using the standard constant pattern and is included merely to set up this (trivial) example. In the second switch statement, the variable is typed as object and, based on the input from the user, can be parsed into an int, string, or decimal data type. Based on the type of the variable, different case statements are matched. In addition to checking the data type, a variable is assigned in each of the case statements (except for the default case). Update the code to the following to use the values in the variables:
第一个 switch 语句使用标准常量模式,包含它只是为了设置这个(微不足道的)示例。在第二个 switch 语句中,变量被类型化为对象,并且根据用户的输入,可以解析为 int、string 或十进制数据类型。根据变量的类型,匹配不同的 case 语句。除了检查数据类型外,还会在每个 case 语句中分配一个变量(默认大小写除外)。将代码更新为以下内容以使用变量中的值:

//This is new the pattern matching switch statement
// 这是新的模式匹配开关语句
switch (choice)
{
    case int i:
        Console.WriteLine("Your choice is an integer {0}.", i);
        break;
    case string s:
        Console.WriteLine("Your choice is a string. {0}", s);
        break;
    case decimal d:
        Console.WriteLine("Your choice is a decimal. {0}", d);
        break;
    default:
        Console.WriteLine("Your choice is something else");
        break;
}

In addition to evaluating on the type of the match expression, when clauses can be added to the case statements to evaluate conditions on the variable. In this example, in addition to checking the type, the value of the converted type is also checked for a match:
除了计算匹配表达式的类型之外,何时可以将子句添加到 case 语句中以计算变量的条件。在此示例中,除了检查类型外,还检查转换后类型的值是否匹配:

 
static void ExecutePatternMatchingSwitchWithWhen()
{
    Console.WriteLine("1 [C#], 2 [VB]");
    Console.Write("Please pick your language preference: ");
    object langChoice = Console.ReadLine();
    var choice = int.TryParse(langChoice.ToString(), out int c) ? c : langChoice;
    switch (choice)
    {
        case int i when i == 2:
        case string s when s.Equals("VB", StringComparison.OrdinalIgnoreCase):
            Console.WriteLine("VB: OOP, multithreading, and more!");
            break;
        case int i when i == 1:
        case string s when s.Equals("C#", StringComparison.OrdinalIgnoreCase):
            Console.WriteLine("Good choice, C# is a fine language.");
            break;
        default:
            Console.WriteLine("Well...good luck with that!");
            break;
    }
    Console.WriteLine();
}

This adds a new dimension to the switch statement as the order of the case statements is now significant. With the constant pattern, each case statement had to be unique. With the type pattern, this is no longer the case. For example, the following code will match every integer in the first case statement and will never execute the second or the third (in fact, the following code will fail to compile):
这为 switch 语句添加了一个新的维度,因为 case 语句的顺序现在很重要。在常量模式下,每个案例语句都必须是唯一的。对于类型模式,情况不再如此。例如,下面的代码将匹配第一个 case 语句中的每个整数,并且永远不会执行第二个或第三个(实际上,下面的代码将无法编译):

 
switch (choice)
{
    case int i:
        //do something
        break;
    case int i when i == 0:
        //do something
        break;
    case int i when i == -1:
        // do something
        break;
}

With the initial release of C# 7, there was a small glitch with pattern matching when using generic types.This has been resolved with C# 7.1. Generic types will be covered in Chapter 10. 在 C# 7 的初始版本中,使用泛型类型时模式匹配出现了一个小故障。此问题已在 C# 7.1 中得到解决。泛型类型将在第 10 章中介绍。

■ Note all of the pattern matching improvement in C# 9.0 previously demonstrated are also available for use in switch statements.
请注意,前面演示的 C# 9.0 中的所有模式匹配改进也可用于 switch 语句。

Using switch Expressions (New 8.0)

使用开关表达式(新版 8.0)

New in C# 8 are switch expressions, allowing the assignment of a variable in a concise statement. Consider the C# 7 version of this method that takes in a color and returns the hex value for the color name:
C# 8 中的新功能是开关表达式,允许在简洁语句中赋值变量。请考虑此方法的 C# 7 版本,该方法采用颜色并返回颜色名称的十六进制值:

 
static string FromRainbowClassic(string colorBand)
{
    switch (colorBand)
    {
        case "Red":
            return "#FF0000";
        case "Orange":
            return "#FF7F00";
        case "Yellow":
            return "#FFFF00";
        case "Green":
            return "#00FF00";
        case "Blue":
            return "#0000FF";
        case "Indigo":
            return "#4B0082";
        case "Violet":
            return "#9400D3";
        default:
            return "#FFFFFF";
    };
}

With the new switch expressions in C# 8, the previous method can be written as follows, which is much more concise:
使用 C# 8 中的新开关表达式,可以按如下方式编写前面的方法,这要简洁得多:

static string FromRainbow(string colorBand)
{
    return colorBand switch
    {
        "Red" => "#FF0000",
        "Orange" => "#FF7F00",
        "Yellow" => "#FFFF00",
        "Green" => "#00FF00",
        "Blue" => "#0000FF",
        "Indigo" => "#4B0082",
        "Violet" => "#9400D3",
        _ => "#FFFFFF",
    };
}

There is a lot to unpack in that example, from the lambda (=>) statements to the discard (). These will all be covered in later chapters, as will this example, in further detail.
在该示例中,从 lambda (=>) 语句到丢弃 (
),有很多东西需要解压缩。这些都将在后面的章节中介绍,这个例子也将更详细地介绍。

There is one more example before finishing the topic of switch expressions, and it involved tuples.Tuples are covered in detail in Chapter 4, so for now think of a tuple as a simple construct holding more than one value and defined with parentheses, like this tuple that holds a string and an int:
在完成开关表达式的主题之前,还有一个示例,它涉及元组。元组在第 4 章中有详细介绍,所以现在将元组视为包含多个值并用括号定义的简单构造,就像这个包含字符串和整数的元组一样:

(string, int)

In the following example, the two values passed into the RockPaperScissors method are converted to a tuple, and then the switch expression evaluates the two values in a single expression. This pattern allows for comparing more than one value during a switch statement.
在下面的示例中,传递给 RockPaperScissors 方法的两个值将转换为元组,然后 switch 表达式在单个表达式中计算这两个值。此模式允许在 switch 语句期间比较多个值。

// Switch expression with Tuples
// 使用元组切换表达式
Console.WriteLine(RockPaperScissors("paper", "rock"));
Console.WriteLine(RockPaperScissors("scissors", "rock"));

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.",
    };
}

To call this method, add the following lines of code to the top-level statements:
若要调用此方法,请将以下代码行添加到顶级语句中:

Console.WriteLine(RockPaperScissors("paper", "rock"));
Console.WriteLine(RockPaperScissors("scissors", "rock"));

This example will be revisited in Chapter 4 when tuples are introduced.
在第 4 章介绍元组时,将重新讨论此示例。

■ Note property patterns were also introduced in C# 8.0 and are covered in Chapter 4.
注意属性模式也在 C# 8.0 中引入,并在第 4 章中介绍。

Summary总结

The goal of this chapter was to expose you to numerous core aspects of the C# programming language.You examined the commonplace constructs in any application you may be interested in building. After examining the role of an application object, you learned that every C# executable program must have a type defining a Main() method, either explicitly or through the use of top-level statements. This method serves as the program’s entry point.
本章的目标是让您了解C#编程语言的许多核心方面。您研究了您可能感兴趣构建的任何应用程序中的常见构造。在研究了应用程序对象的角色后,您了解到每个C#可执行程序都必须有一个类型来定义Main()方法,无论是显式的还是通过使用顶级语句。此方法用作程序的入口点。

Next, you dove into the details of the built-in data types of C# and came to understand that each data type keyword (e.g., int) is really a shorthand notation for a full-blown type in the System namespace (System.Int32, in this case). Given this, each C# data type has a number of built-in members. Along the same vein, you also learned about the role of widening and narrowing, as well as the role of the checked andunchecked keywords.
接下来,深入了解C#的内置数据类型的细节,并了解到每个数据类型关键字(例如,int)实际上都是System命名空间(本例中为System.Int32)中一个完整类型的简写符号。鉴于此,每个C#数据类型都有许多内置成员。同样,您还了解了加宽和变窄的作用,以及选中和未选中关键字的作用。

The chapter wrapped up by covering the role of implicit typing using the var keyword. As discussed, the most useful place for implicit typing is when working with the LINQ programming model. Finally, you quickly examined the various iteration and decision constructs supported by C#.
本章最后介绍了使用var关键字进行隐式键入的作用。如前所述,隐式类型最有用的地方是在使用LINQ编程模型时。最后,您快速检查了C#支持的各种迭代和决策结构。

Now that you understand some of the basic nuts and bolts, the next chapter (Chapter 4) will complete your examination of core language features. After that, you will be well prepared to examine the object- oriented features of C# beginning in Chapter 5.
现在您已经了解了一些基本的细节,下一章(第4章)将完成对核心语言功能的检查。之后,您将做好充分准备,从第5章开始研究C#的面向对象特性。