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#的面向对象特性。

发表评论