专业编程基础技术教程

网站首页 > 基础教程 正文

C# 13 和 .NET 9 全知道 : 3控制流程、转换类型和处理异常 (3)

ccvgpt 2025-01-10 11:58:56 基础教程 32 ℃

数组的列表模式匹配

在本章的前面部分,您看到单个对象如何支持针对其类型和属性的模式匹配。模式匹配也适用于数组和集合。

在 C# 11 中引入的列表模式匹配适用于任何具有公共 LengthCount 属性并且使用 intSystem.Index 参数的索引器的类型。您将在第 5 章《使用面向对象编程构建自己的类型》中学习有关索引器的内容。

C# 13 和 .NET 9 全知道 : 3控制流程、转换类型和处理异常 (3)

当您在同一个 switch 表达式中定义多个列表模式时,必须按顺序排列,使得更具体的模式优先,否则编译器会抱怨,因为更一般的模式也会匹配更具体的模式,从而使得更具体的模式无法到达。

表 3.3 显示了列表模式匹配的示例,假设有一个 int 值的列表:

示例

描述

[]

匹配一个空数组或集合。

[..]

匹配一个数组或集合,可以包含任意数量的项,包括零,因此如果您需要同时开启 [..][] ,则 [..] 必须在 [] 之后。

[_]

与列表中的任何单个项目匹配。

[int item1]

[var item1]

将列表与任何单个项目匹配,并可以通过引用 item1 在返回表达式中使用该值。

[7, 2]

完全匹配按该顺序排列的两个项目的列表。

[_, _]

匹配包含任意两个项目的列表。

[var item1, var item2]

将列表中的任意两个项目进行匹配,并可以通过引用 item1item2 在返回表达式中使用这些值。

[_, _, _]

匹配包含任意三个项目的列表。

[var item1, ..]

匹配一个或多个项目的列表。可以通过引用 item1 来引用其返回表达式中第一个项目的值。

[var firstItem, .., var lastItem]

匹配包含两个或更多项的列表。可以通过引用 firstItemlastItem 来指代返回表达式中第一个和最后一个项的值。

[.., var lastItem]

匹配一个或多个项目的列表。可以通过引用 lastItem 来指代其返回表达式中最后一个项目的值。

表 3.3:列表模式匹配示例

让我们看看一些代码示例:

  1. Program.cs 的底部,添加语句以定义一些 int 值的数组,然后将它们传递给一个方法,该方法根据最佳匹配的模式返回描述性文本,如以下代码所示:
int[] sequentialNumbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int[] oneTwoNumbers = { 1, 2 };
int[] oneTwoTenNumbers = { 1, 2, 10 };
int[] oneTwoThreeTenNumbers = { 1, 2, 3, 10 };
int[] primeNumbers = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 };
int[] fibonacciNumbers = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
int[] emptyNumbers = { }; // Or use Array.Empty<int>()
int[] threeNumbers = { 9, 7, 5 };
int[] sixNumbers = { 9, 7, 5, 4, 2, 10 };
WriteLine(#34;{nameof(sequentialNumbers)}: {CheckSwitch(sequentialNumbers)}");
WriteLine(#34;{nameof(oneTwoNumbers)}: {CheckSwitch(oneTwoNumbers)}");
WriteLine(#34;{nameof(oneTwoTenNumbers)}: {CheckSwitch(oneTwoTenNumbers)}");
WriteLine(#34;{nameof(oneTwoThreeTenNumbers)}: {CheckSwitch(oneTwoThreeTenNumbers)}");
WriteLine(#34;{nameof(primeNumbers)}: {CheckSwitch(primeNumbers)}");
WriteLine(#34;{nameof(fibonacciNumbers)}: {CheckSwitch(fibonacciNumbers)}");
WriteLine(#34;{nameof(emptyNumbers)}: {CheckSwitch(emptyNumbers)}");
WriteLine(#34;{nameof(threeNumbers)}: {CheckSwitch(threeNumbers)}");
WriteLine(#34;{nameof(sixNumbers)}: {CheckSwitch(sixNumbers)}");
static string CheckSwitch(int[] values) => values switch
{
  [] => "Empty array",
  [1, 2, _, 10] => "Contains 1, 2, any single number, 10.",
  [1, 2, .., 10] => "Contains 1, 2, any range including empty, 10.",
  [1, 2] => "Contains 1 then 2.",
  [int item1, int item2, int item3] =>
    #34;Contains {item1} then {item2} then {item3}.",
  [0, _] => "Starts with 0, then one other number.",
  [0, ..] => "Starts with 0, then any range of numbers.",
  [2, .. int[] others] => #34;Starts with 2, then {others.Length} more numbers.",
  [..] => "Any items in any order.", // <-- Note the trailing comma for easier re-ordering.
  // Use Alt + Up or Down arrow to move statements.
};

在 C# 6 中,微软添加了对表达式主体函数成员的支持。上面的 CheckSwitch 函数使用了这种语法。在 C# 中,lambda 是使用 => 字符来指示函数的返回值。我将在第 4 章“编写、调试和测试函数”中详细介绍这一点。

  1. 运行代码并注意结果,如以下输出所示:
sequentialNumbers: Contains 1, 2, any range including empty, 10.
oneTwoNumbers: Contains 1 then 2.
oneTwoTenNumbers: Contains 1, 2, any range including empty, 10.
oneTwoThreeTenNumbers: Contains 1, 2, any single number, 10.
primeNumbers: Starts with 2, then 9 more numbers.
fibonacciNumbers: Starts with 0, then any range of numbers.
emptyNumbers: Empty array
threeNumbers: Contains 9 then 7 then 5.
sixNumbers: Any items in any order.

您可以通过以下链接了解更多关于列表模式匹配的信息:https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/patterns#list-patterns。

尾随逗号

switch 表达式中,最后一个项目后的尾随逗号是可选的,编译器不会对此提出异议。

大多数语言,包括 C#,允许使用尾随逗号的代码风格。当多个项目用逗号分隔时(例如,在声明匿名对象、数组、集合初始化器、枚举和 switch 表达式时),C#允许在最后一个项目后面添加尾随逗号。这使得重新排列项目的顺序变得简单,而无需不断添加和删除逗号。

您可以在以下链接阅读关于允许 switch 表达式的尾随逗号的讨论,时间回溯到 2018 年:https://github.com/dotnet/csharplang/issues/2098。

即使是 JSON 序列化器也有一个选项允许这样做,因为使用它是如此普遍,具体讨论请参见以下链接: https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions.allowtrailingcommas。

理解内联数组

内联数组是在 C# 12 中引入的;它们是 .NET 运行时团队用于提高性能的高级特性。除非您是公共库的作者,否则您不太可能自己使用它们,但您将自动受益于其他人对它们的使用。

更多信息:您可以通过以下链接了解有关内联数组的更多信息:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/inline-arrays。

总结数组

我们使用略微不同的语法来声明不同类型的数组,如表 3.4 所示:

数组类型

声明语法

一维

datatype[] ,例如, string[]

两个维度

string[,]

三维

string[,,]

十个维度

string[,,,,,,,,,]

数组的数组,即二维锯齿状数组

string[][]

数组的数组的数组,即三维不规则数组

string[][][]

表 3.4:数组声明语法摘要

数组对于临时存储多个项目非常有用,但集合在动态添加和删除项目时是一个更灵活的选择。您现在不需要担心集合,因为我们将在第 8 章“使用常见 .NET 类型”中讨论它们。

您可以使用 ToArray 扩展方法将任何项目序列转换为数组,我们将在第 11 章中讨论,使用 LINQ 查询和操作数据。

良好实践:如果您不需要动态添加和删除项目,则应使用数组而不是像 List<T> 这样的集合,因为数组在内存使用上更高效,并且项目是连续存储的,这可以提高性能。

类型之间的转换和强制转换

您通常需要在不同类型之间转换变量的值。例如,数据输入通常以文本形式在控制台中输入,因此它最初存储在 string 类型的变量中,但随后需要根据存储和处理的方式将其转换为日期/时间、数字或其他数据类型。

有时您需要在数字类型之间转换,例如在整数和浮点数之间,以便进行计算。

转换也称为类型转换,分为两种:隐式转换和显式转换。隐式转换是自动发生的,并且是安全的,这意味着您不会丢失任何信息。

显式转换必须手动执行,因为它可能会丢失信息,例如数字的精度。通过显式转换,您是在告诉 C#编译器您理解并接受风险。

铸造编号隐式和显式

隐式将一个 int 变量转换为 double 变量是安全的,因为不会丢失任何信息,如下所示:

  1. 使用您喜欢的代码编辑器向 Chapter03 解决方案添加一个名为 CastingConverting 的新控制台应用程序 / console 项目。
  2. Program.cs 中,删除现有的语句,然后输入语句以声明并赋值一个 int 变量和一个 double 变量,然后在将整数的值赋给 double 变量时隐式转换其值,如以下代码所示:
int a = 10;
double b = a; // An int can be safely cast into a double.
WriteLine(#34;a is {a}, b is {b}");

输入语句以声明和赋值一个 double 变量和一个 int 变量,然后在将 double 值赋给 int 变量时隐式转换,如以下代码所示:

double c = 9.8;
int d = c; // Compiler gives an error if you do not explicitly cast.
WriteLine(#34;c is {c}, d is {d}");

运行代码并注意错误信息,如以下输出所示:

Error: (6,9): error CS0266: Cannot implicitly convert type 'double' to 'int'. An explicit conversion exists (are you missing a cast?)

此错误消息也会出现在 Visual Studio 错误列表、VS Code 问题窗口或 Rider 问题窗口中。

您不能隐式地将一个 double 变量转换为 int 变量,因为这可能不安全并可能导致数据丢失,例如小数点后的值。您必须显式地将一个 double 变量转换为 int 变量,使用一对圆括号将您想要转换为 double 类型的类型括起来。这对圆括号是转换运算符。即便如此,您必须意识到小数点后的部分将会在没有警告的情况下被截断,因为您选择执行显式转换,因此理解其后果。

  1. 修改 d 变量的赋值语句,以显式地将变量 c 转换为 int ,并添加注释以解释将会发生什么,如以下代码中突出显示的内容所示:
double c = 9.8;
int d = (int)c; // Compiler gives an error if you do not explicitly cast.
WriteLine(#34;c is {c}, d is {d}"); // d loses the .8 part.

运行代码以查看结果,如下所示的输出:

a is 10, b is 10
c is 9.8, d is 9

在将较大整数和较小整数之间转换值时,我们必须执行类似的操作。同样,请注意,您可能会丢失信息,因为任何过大的值将其位复制,然后以您可能意想不到的方式进行解释!

  1. 输入语句以声明并将一个 long (64 位)整数变量赋值给一个 int (32 位)整数变量,使用一个小值(有效)和一个过大的值(无效),如以下代码所示:
long e = 10;
int f = (int)e;
WriteLine(#34;e is {e:N0}, f is {f:N0}");
e = long.MaxValue;
f = (int)e;
WriteLine(#34;e is {e:N0}, f is {f:N0}");

运行代码以查看结果,如下所示的输出:

e is 10, f is 10
e is 9,223,372,036,854,775,807, f is -1

e 的值修改为 50 亿,如以下代码所示:

e = 5_000_000_000;

运行代码以查看结果,如下所示的输出:

e is 5,000,000,000, f is 705,032,704

五十亿无法容纳在 32 位整数中,因此它溢出(回绕)到大约 7 亿。 这与整数的二进制表示有关。 你将在本章后面看到更多整数溢出的例子以及如何处理它。

负数在二进制中的表示方式

您可能会想知道为什么 f 在之前的代码中具有值 -1 。负数,即带符号数,使用第一个位来表示负性。如果该位是 0 (零),那么它是一个正数。如果该位是 1 (一),那么它是一个负数。

让我们写一些代码来说明这一点:

  1. 输入语句以输出 int 的十进制和二进制数格式的最大值,然后输出 8-8 的值,递减一个,最后输出 int 的最小值,如以下代码所示:
WriteLine("{0,12} {1,34}", "Decimal", "Binary");
WriteLine("{0,12} {0,34:B32}", int.MaxValue);
for (int i = 8; i >= -8; i--)
{
  WriteLine("{0,12} {0,34:B32}", i);
}
WriteLine("{0,12} {0,34:B32}", int.MinValue);

请注意, ,12,34 意味着在这些列宽内右对齐。 :B32 意味着格式化为二进制,前面用零填充到 32 位宽。

  1. 运行代码以查看结果,如下所示的输出:
  Decimal                             Binary
  2147483647   01111111111111111111111111111111
           8   00000000000000000000000000001000
           7   00000000000000000000000000000111
           6   00000000000000000000000000000110
           5   00000000000000000000000000000101
           4   00000000000000000000000000000100
           3   00000000000000000000000000000011
           2   00000000000000000000000000000010
           1   00000000000000000000000000000001
           0   00000000000000000000000000000000
          -1   11111111111111111111111111111111
          -2   11111111111111111111111111111110
          -3   11111111111111111111111111111101
          -4   11111111111111111111111111111100
          -5   11111111111111111111111111111011
          -6   11111111111111111111111111111010
          -7   11111111111111111111111111111001
          -8   11111111111111111111111111111000
 -2147483648   10000000000000000000000000000000
  1. 请注意,所有正的二进制数表示以 0 开头,所有负的二进制数表示以 1 开头。十进制值 -1 在二进制中表示为全 1。这就是为什么当你有一个太大而无法适应 32 位整数的整数时,它变成了 -1 。但这种类型的强制转换结果并不总是 -1 。从更宽的整数数据类型转换为更窄的整数数据类型时,最重要的额外位会被截断。例如,如果你从 32 位整数转换为 16 位整数,32 位整数的 16 个最重要位(MSB)将被截断。最不重要的位(LSB)表示强制转换的结果。例如,如果你转换为 16 位整数,原始值的 16 个最不重要位将表示强制转换后的结果。
  2. 输入语句以显示一个 long 整数的示例,当其转换为 int 时,会被截断为非负一值,如以下代码所示:
long r = 0b_101000101010001100100111010100101010;
int s = (int) r;
Console.WriteLine(#34;{r,38:B38} = {r}");
Console.WriteLine(#34;{s,38:B32} = {s}");

运行代码以查看结果,如下所示的输出:

00101000101010001100100111010100101010 = 43657622826
      00101010001100100111010100101010 = 707949866

更多信息:如果您有兴趣了解有关计算机系统中如何表示带符号数的更多信息,您可以阅读以下文章:https://en.wikipedia.org/wiki/Signed_number_representations。

使用 System.Convert 类型进行转换

您只能在相似类型之间进行转换,例如在整数之间,如 byteintlong ,或在类与其子类之间。您不能将 long 转换为 string 或将 byte 转换为 DateTime

使用类型 System.Convert 是使用强制转换运算符的替代方案。类型 System.Convert 可以在所有 C# 数字类型、布尔值、字符串以及日期和时间值之间进行转换。

让我们写一些代码来看看这个实际效果:

  1. Program.cs 的顶部,静态导入 System.Convert 类,如下代码所示:
using static System.Convert; // To use the ToInt32 method.

或者,向 CastingConverting.csproj 添加一个条目,如下所示的标记: <Using Include="System.Convert" Static="true" />

  1. Program.cs 的底部,输入语句以声明并赋值给 double 变量,将其转换为整数,然后将这两个值写入控制台,如以下代码所示:
double g = 9.8;
int h = ToInt32(g); // A method of System.Convert.
WriteLine(#34;g is {g}, h is {h}");

运行代码并查看结果,如下所示的输出:

g is 9.8, h is 10

一个重要的区别在于,转换将 double9.8 向上舍入到 10 ,而不是截断小数点后的部分。另一个区别是,强制转换可能会导致溢出,而转换则会抛出异常。

四舍五入和默认的四舍五入规则

您现在已经看到强制转换运算符会去掉实数的小数部分,而 System.Convert 方法则会进行四舍五入。但是,四舍五入的规则是什么?

在英国的 5 到 11 岁儿童的初级学校中,学生们被教导如果小数部分是 0.5 或更高则向上取整,如果小数部分较低则向下取整。当然,这些术语只有在这个年龄段的学生只处理正数时才有意义。对于负数,这些术语会变得令人困惑,因此应该避免使用。这就是为什么.NET API 使用 enumAwayFromZeroToZeroToEvenToPositiveInfinityToNegativeInfinity 以提高清晰度。

让我们探讨一下 C#是否遵循相同的小学规则:

  1. 输入语句以声明和赋值一个包含 double 个值的数组,将每个值转换为整数,然后将结果写入控制台,如以下代码所示:
double[,] doubles = {
  { 9.49, 9.5, 9.51 },
  { 10.49, 10.5, 10.51 },
  { 11.49, 11.5, 11.51 },
  { 12.49, 12.5, 12.51 } ,
  { -12.49, -12.5, -12.51 },
  { -11.49, -11.5, -11.51 },
  { -10.49, -10.5, -10.51 },
  { -9.49, -9.5, -9.51 }
};
WriteLine(#34;| double | ToInt32 | double | ToInt32 | double | ToInt32 |");
for (int x = 0; x < 8; x++)
{
  for (int y = 0; y < 3; y++)
  {
    Write(#34;| {doubles[x, y],6} | {ToInt32(doubles[x, y]),7} ");
  }
  WriteLine("|");
}
WriteLine();

运行代码并查看结果,如下所示的输出:

| double | ToInt32 | double | ToInt32 | double | ToInt32 |
|   9.49 |       9 |    9.5 |      10 |   9.51 |      10 |
|  10.49 |      10 |   10.5 |      10 |  10.51 |      11 |
|  11.49 |      11 |   11.5 |      12 |  11.51 |      12 |
|  12.49 |      12 |   12.5 |      12 |  12.51 |      13 |
| -12.49 |     -12 |  -12.5 |     -12 | -12.51 |     -13 |
| -11.49 |     -11 |  -11.5 |     -12 | -11.51 |     -12 |
| -10.49 |     -10 |  -10.5 |     -10 | -10.51 |     -11 |
|  -9.49 |      -9 |   -9.5 |     -10 |  -9.51 |     -10 |

我们已经表明,C# 中的舍入规则与小学规则有细微的不同:

  • 如果小数部分小于中点 0.5,它总是向零舍入。
  • 如果小数部分超过中点 0.5,它总是向远离零的方向舍入。
  • 如果小数部分是中点 0.5 且非小数部分是奇数,它将向远离零的方向舍入,但如果非小数部分是偶数,它将向零的方向舍入。

这个规则被称为银行家舍入,它被优先使用,因为它通过交替舍入到零的方向来减少偏差。遗憾的是,其他语言如 JavaScript 使用的是小学规则。

控制舍入规则

您可以通过使用 Math 类的 Round 方法来控制舍入规则:

  1. 输入语句以使用“远离零”的舍入规则(也称为向上舍入)对每个 double 值进行舍入,然后将结果写入控制台,如以下代码所示:
foreach (double n in doubles)
{
  WriteLine(format:
    "Math.Round({0}, 0, MidpointRounding.AwayFromZero) is {1}",
    arg0: n,
    arg1: Math.Round(value: n, digits: 0,
            mode: MidpointRounding.AwayFromZero));
}
  1. 您可以使用 foreach 语句来枚举多维数组中的所有项。
  1. 运行代码并查看结果,如下所示的部分输出:
Math.Round(9.49, 0, MidpointRounding.AwayFromZero) is 9
Math.Round(9.5, 0, MidpointRounding.AwayFromZero) is 10
Math.Round(9.51, 0, MidpointRounding.AwayFromZero) is 10
Math.Round(10.49, 0, MidpointRounding.AwayFromZero) is 10
Math.Round(10.5, 0, MidpointRounding.AwayFromZero) is 11
Math.Round(10.51, 0, MidpointRounding.AwayFromZero) is 11
...

良好实践:对于您使用的每种编程语言,请检查其舍入规则。它们可能并不像您预期的那样工作!您可以在以下链接中阅读更多关于 Math.Round 的信息:https://learn.microsoft.com/en-us/dotnet/api/system.math.round。

从任何类型转换为字符串

最常见的转换是将任何类型转换为 string 变量,以便输出为人类可读的文本,因此所有类型都有一个名为 ToString 的方法,它们从 System.Object 类继承。

ToString 方法将任何变量的当前值转换为文本表示。一些类型无法合理地表示为文本,因此它们返回其命名空间和类型名称。

让我们将一些类型转换为 string :

  1. 输入语句以声明一些变量,将它们转换为 string 表示,并将其写入控制台,如以下代码所示:
int number = 12;
WriteLine(number.ToString());
bool boolean = true;
WriteLine(boolean.ToString());
DateTime now = DateTime.Now;
WriteLine(now.ToString());
object me = new();
WriteLine(me.ToString());

运行代码并查看结果,如下所示的输出:

12
True
08/28/2024 17:33:54
System.Object

将任何对象传递给 WriteLine 方法会隐式地将其转换为 string ,因此不需要显式调用 ToString 。我们在这里这样做只是为了强调发生了什么。显式调用 ToString 确实可以避免装箱操作,因此如果您正在使用 Unity 开发游戏,这可以帮助您避免内存垃圾回收问题。

Tags:

最近发表
标签列表