网站首页 > 基础教程 正文
数组的列表模式匹配
在本章的前面部分,您看到单个对象如何支持针对其类型和属性的模式匹配。模式匹配也适用于数组和集合。
在 C# 11 中引入的列表模式匹配适用于任何具有公共 Length 或 Count 属性并且使用 int 或 System.Index 参数的索引器的类型。您将在第 5 章《使用面向对象编程构建自己的类型》中学习有关索引器的内容。
当您在同一个 switch 表达式中定义多个列表模式时,必须按顺序排列,使得更具体的模式优先,否则编译器会抱怨,因为更一般的模式也会匹配更具体的模式,从而使得更具体的模式无法到达。
表 3.3 显示了列表模式匹配的示例,假设有一个 int 值的列表:
示例 | 描述 |
[] | 匹配一个空数组或集合。 |
[..] | 匹配一个数组或集合,可以包含任意数量的项,包括零,因此如果您需要同时开启 [..] 和 [] ,则 [..] 必须在 [] 之后。 |
[_] | 与列表中的任何单个项目匹配。 |
[int item1] 或 [var item1] | 将列表与任何单个项目匹配,并可以通过引用 item1 在返回表达式中使用该值。 |
[7, 2] | 完全匹配按该顺序排列的两个项目的列表。 |
[_, _] | 匹配包含任意两个项目的列表。 |
[var item1, var item2] | 将列表中的任意两个项目进行匹配,并可以通过引用 item1 和 item2 在返回表达式中使用这些值。 |
[_, _, _] | 匹配包含任意三个项目的列表。 |
[var item1, ..] | 匹配一个或多个项目的列表。可以通过引用 item1 来引用其返回表达式中第一个项目的值。 |
[var firstItem, .., var lastItem] | 匹配包含两个或更多项的列表。可以通过引用 firstItem 和 lastItem 来指代返回表达式中第一个和最后一个项的值。 |
[.., var lastItem] | 匹配一个或多个项目的列表。可以通过引用 lastItem 来指代其返回表达式中最后一个项目的值。 |
表 3.3:列表模式匹配示例
让我们看看一些代码示例:
- 在 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 章“编写、调试和测试函数”中详细介绍这一点。
- 运行代码并注意结果,如以下输出所示:
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 变量是安全的,因为不会丢失任何信息,如下所示:
- 使用您喜欢的代码编辑器向 Chapter03 解决方案添加一个名为 CastingConverting 的新控制台应用程序 / console 项目。
- 在 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 类型的类型括起来。这对圆括号是转换运算符。即便如此,您必须意识到小数点后的部分将会在没有警告的情况下被截断,因为您选择执行显式转换,因此理解其后果。
- 修改 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
在将较大整数和较小整数之间转换值时,我们必须执行类似的操作。同样,请注意,您可能会丢失信息,因为任何过大的值将其位复制,然后以您可能意想不到的方式进行解释!
- 输入语句以声明并将一个 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 (一),那么它是一个负数。
让我们写一些代码来说明这一点:
- 输入语句以输出 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 位宽。
- 运行代码以查看结果,如下所示的输出:
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
- 请注意,所有正的二进制数表示以 0 开头,所有负的二进制数表示以 1 开头。十进制值 -1 在二进制中表示为全 1。这就是为什么当你有一个太大而无法适应 32 位整数的整数时,它变成了 -1 。但这种类型的强制转换结果并不总是 -1 。从更宽的整数数据类型转换为更窄的整数数据类型时,最重要的额外位会被截断。例如,如果你从 32 位整数转换为 16 位整数,32 位整数的 16 个最重要位(MSB)将被截断。最不重要的位(LSB)表示强制转换的结果。例如,如果你转换为 16 位整数,原始值的 16 个最不重要位将表示强制转换后的结果。
- 输入语句以显示一个 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 类型进行转换
您只能在相似类型之间进行转换,例如在整数之间,如 byte 、 int 和 long ,或在类与其子类之间。您不能将 long 转换为 string 或将 byte 转换为 DateTime 。
使用类型 System.Convert 是使用强制转换运算符的替代方案。类型 System.Convert 可以在所有 C# 数字类型、布尔值、字符串以及日期和时间值之间进行转换。
让我们写一些代码来看看这个实际效果:
- 在 Program.cs 的顶部,静态导入 System.Convert 类,如下代码所示:
using static System.Convert; // To use the ToInt32 method.
或者,向 CastingConverting.csproj 添加一个条目,如下所示的标记: <Using Include="System.Convert" Static="true" /> 。
- 在 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
一个重要的区别在于,转换将 double 值 9.8 向上舍入到 10 ,而不是截断小数点后的部分。另一个区别是,强制转换可能会导致溢出,而转换则会抛出异常。
四舍五入和默认的四舍五入规则
您现在已经看到强制转换运算符会去掉实数的小数部分,而 System.Convert 方法则会进行四舍五入。但是,四舍五入的规则是什么?
在英国的 5 到 11 岁儿童的初级学校中,学生们被教导如果小数部分是 0.5 或更高则向上取整,如果小数部分较低则向下取整。当然,这些术语只有在这个年龄段的学生只处理正数时才有意义。对于负数,这些术语会变得令人困惑,因此应该避免使用。这就是为什么.NET API 使用 enum 值 AwayFromZero 、 ToZero 、 ToEven 、 ToPositiveInfinity 和 ToNegativeInfinity 以提高清晰度。
让我们探讨一下 C#是否遵循相同的小学规则:
- 输入语句以声明和赋值一个包含 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 方法来控制舍入规则:
- 输入语句以使用“远离零”的舍入规则(也称为向上舍入)对每个 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));
}
- 您可以使用 foreach 语句来枚举多维数组中的所有项。
- 运行代码并查看结果,如下所示的部分输出:
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 :
- 输入语句以声明一些变量,将它们转换为 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 开发游戏,这可以帮助您避免内存垃圾回收问题。
猜你喜欢
- 2025-01-10 SpringBoot教程Thymeleaf详解
- 2025-01-10 前端教程:JavaScript页面打印
- 2025-01-10 Flutter实战之一-面向java程序员的Dart语言入门
- 2025-01-10 开发自己的nodejs命令行工具并使用工具
- 2025-01-10 记一次 Vue2 迁移 Vue3 的实践总结
- 2025-01-10 想要字体图标设计师却给了SVG?没关系,自己转
- 2025-01-10 Flutter Isar 数据库使用快速入门
- 2025-01-10 SpringSecurity和JWT实现认证和授权
- 2025-01-10 ES6 的新增语法
- 2025-01-10 F5负载均衡器如何通过irules实现应用的灵活转发?
- 05-21HTML DOM Columngroup 对象
- 05-21零基础学习HTML图像热区特殊字符无序列表和有序定义列表表格
- 05-21让div填充屏幕剩余高度的方法
- 05-21高效设计表格 - 用我们的HTML表格生成器轻松搞定
- 05-21前端入门——html 表单
- 05-21我问AI——以前网页编程流行用table布局,为什么不用了
- 05-21平和!晨间攻克 HTML 表格属性题,面试难题轻松化解
- 05-21一键超4400MHz!豪华灯效+高频至尊享受
- 最近发表
- 标签列表
-
- jsp (69)
- pythonlist (60)
- gitpush (78)
- gitreset (66)
- python字典 (67)
- dockercp (63)
- gitclone命令 (63)
- dockersave (62)
- linux命令大全 (65)
- pythonif (86)
- location.href (69)
- dockerexec (65)
- deletesql (62)
- c++模板 (62)
- linuxgzip (68)
- 字符串连接 (73)
- nginx配置文件详解 (61)
- html标签 (69)
- c++初始化列表 (64)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- console.table (62)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)