Skip to content

Latest commit

 

History

History
1980 lines (1534 loc) · 82.1 KB

File metadata and controls

1980 lines (1534 loc) · 82.1 KB

八、使用常见的.NET 类型

本章介绍.NET 中包含的一些常见类型。这些类型包括用于操作数字、文本、集合、网络访问、反射和属性的类型;改进跨距、索引和范围的使用;图像处理;和国际化。

本章涵盖以下主题:

  • 与数字打交道
  • 处理文本
  • 处理日期和时间
  • 正则表达式模式匹配
  • 在集合中存储多个对象
  • 使用跨距、索引和范围
  • 使用网络资源
  • 使用反射和属性
  • 处理图像
  • 国际化您的代码

与数字打交道

最常见的数据类型之一是数字。下表显示了.NET 中用于处理数字的最常见类型:

| 名称空间 | 示例类型 | 描述 | | `System` | `SByte`、`Int16`、`Int32`、`Int64` | 整数;也就是零和正负整数 | | `System` | `Byte`、`UInt16`、`UInt32`、`UInt64` | 红衣主教;也就是零和正整数 | | `System` | `Half`、`Single`、`Double` | 雷亚尔;也就是说,浮点数 | | `System` | `Decimal` | 精确实数;也就是说,用于科学、工程或金融场景 | | `System.Numerics` | `BigInteger`、`Complex`、`Quaternion` | 任意大整数、复数和四元数 |

自.NET Framework 1.0 以来,.NET 具有 32 位浮点和 64 位双精度类型。IEEE 754 规范还定义了 16 位浮点标准。机器学习和其他算法将受益于这种更小、精度更低的数字类型,因此微软在.NET5 和更高版本中引入了System.Half类型。

目前,C 语言没有定义half别名,因此必须使用.NET 类型System.Half。这在将来可能会改变。

处理大整数

可以存储在具有 C# 别名的.NET 类型中的最大整数约为 185.5 百万,存储在无符号long整数中。但是如果你需要存储比这个大的数字呢?

让我们来探索数字:

  1. 使用您首选的代码编辑器创建名为Chapter08的新解决方案/工作区。

  2. 添加控制台应用项目,如以下列表中所定义:

    1. 项目模板:控制台应用/console
    2. 工作区/解决方案文件和文件夹:Chapter08
    3. 项目文件和文件夹:WorkingWithNumbers
  3. Program.cs中删除已有的语句,增加一条要导入System.Numerics的语句,如下代码所示:

    using System.Numerics; 
  4. Add statements to output the maximum value of the ulong type, and a number with 30 digits using BigInteger, as shown in the following code:

    WriteLine("Working with large integers:");
    WriteLine("-----------------------------------");
    ulong big = ulong.MaxValue;
    WriteLine($"{big,40:N0}");
    BigInteger bigger =
      BigInteger.Parse("123456789012345678901234567890");
    WriteLine($"{bigger,40:N0}"); 

    格式代码中的40表示右对齐 40 个字符,因此两个数字都对齐到右侧边缘。N0表示使用千位分隔符和零位小数。

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

    Working with large integers:
    ----------------------------------------
                  18,446,744,073,709,551,615
     123,456,789,012,345,678,901,234,567,890 

处理复数

复数可以表示为A+bi,其中Ab为实数,i为虚单位,其中i2=−1。如果实部a为零,则为纯虚数。如果虚部b为零,则为实数。

复数在许多STEM科学、技术、工程和数学研究领域都有实际应用。此外,通过分别添加求和的实部和虚部来添加它们;考虑一下:

(a + bi) + (c + di) = (a + c) + (b + d)i 

让我们探讨复数:

  1. Program.cs中添加语句,添加两个复数,如下代码所示:

    WriteLine("Working with complex numbers:");
    Complex c1 = new(real: 4, imaginary: 2);
    Complex c2 = new(real: 3, imaginary: 7);
    Complex c3 = c1 + c2;
    // output using default ToString implementation
    WriteLine($"{c1} added to {c2} is {c3}");
    // output using custom format
    WriteLine("{0} + {1}i added to {2} + {3}i is {4} + {5}i",
      c1.Real, c1.Imaginary, 
      c2.Real, c2.Imaginary,
      c3.Real, c3.Imaginary); 
  2. 运行代码并查看结果,如以下输出所示:

    Working with complex numbers:
    (4, 2) added to (3, 7) is (7, 9)
    4 + 2i added to 3 + 7i is 7 + 9i 

理解四元数

四元数是一种扩展复数的数字系统。它们在实数上形成一个四维联合赋范的除代数,因此也是一个域。

嗯?是的,我知道。我也不明白。别担心;我们不会用它们写任何代码!可以说,它们擅长描述空间旋转,所以视频游戏引擎使用它们,就像许多计算机模拟和飞行控制系统一样。

处理文本

变量的另一种最常见的数据类型是文本。下表显示了.NET 中用于处理文本的最常见类型:

| 名称空间 | 类型 | 描述 | | `System` | `Char` | 单个文本字符的存储 | | `System` | `String` | 多个文本字符的存储 | | `System.Text` | `StringBuilder` | 有效地操纵字符串 | | `System.Text.RegularExpressions` | `Regex` | 高效的模式匹配字符串 |

获取字符串的长度

让我们探讨一下处理文本时的一些常见任务;例如,有时您需要找出存储在string变量中的一段文本的长度:

  1. 使用您首选的代码编辑器将名为WorkingWithText的新控制台应用添加到Chapter08解决方案/工作区:

    1. 在 Visual Studio 中,将解决方案的启动项目设置为当前选择。
    2. 在 Visual Studio 代码中,选择WorkingWithText作为活动的 OmniSharp 项目。
  2. WorkingWithText项目中,在Program.cs中添加语句,定义一个变量来存储伦敦城的名称,然后将其名称和长度写入控制台,如下代码所示:

    string city = "London";
    WriteLine($"{city} is {city.Length} characters long."); 
  3. 运行代码并查看结果,如以下输出所示:

    London is 6 characters long. 

获取字符串的字符

string类在内部使用char数组来存储文本。它还有一个索引器,这意味着我们可以使用数组语法来读取它的字符。数组索引从零开始,因此第三个字符将位于索引 2。

让我们看看这一点:

  1. string变量中添加一条语句写入第一、第三个位置的字符,如下代码所示:

    WriteLine($"First char is {city[0]} and third is {city[2]}."); 
  2. 运行代码并查看结果,如以下输出所示:

    First char is L and third is n. 

拆分字符串

有时,您需要在有字符的地方拆分一些文本,例如逗号:

  1. 添加语句定义一个包含逗号分隔城市名称的单个string变量,然后使用Split方法,指定要将逗号作为分隔符,然后枚举返回的string值数组,如下代码所示:

    string cities = "Paris,Tehran,Chennai,Sydney,New York,Medellín"; 
    string[] citiesArray = cities.Split(',');
    WriteLine($"There are {citiesArray.Length} items in the array.");
    foreach (string item in citiesArray)
    {
      WriteLine(item);
    } 
  2. 运行代码并查看结果,如以下输出所示:

    There are 6 items in the array.
    Paris 
    Tehran 
    Chennai
    Sydney
    New York
    Medellín 

在本章后面,您将学习如何处理更复杂的场景。

获取字符串的一部分

有时候,你需要得到一些文本的部分。IndexOf方法有九个重载,返回string中指定的charstring的索引位置。Substring方法有两个重载,如下表所示:

  • Substring(startIndex, length):返回从startIndex开始并包含下一个length字符的子字符串。
  • Substring(startIndex):返回一个子字符串,该子字符串从startIndex开始,包含字符串末尾的所有字符。

让我们来看一个简单的例子:

  1. 添加语句,将一个人的全名存储在string变量中,在名字和姓氏之间使用空格字符,找到空格的位置,然后将名字和姓氏提取为两部分,以便按不同的顺序重新组合,如下代码所示:

    string fullName = "Alan Jones";
    int indexOfTheSpace = fullName.IndexOf(' ');
    string firstName = fullName.Substring(
      startIndex: 0, length: indexOfTheSpace);
    string lastName = fullName.Substring(
      startIndex: indexOfTheSpace + 1);
    WriteLine($"Original: {fullName}");
    WriteLine($"Swapped: {lastName}, {firstName}"); 
  2. 运行代码并查看结果,如以下输出所示:

    Original: Alan Jones
    Swapped: Jones, Alan 

如果初始全名的格式不同,例如,"LastName, FirstName",则代码需要不同。作为可选练习,尝试编写一些语句,将输入"Jones, Alan"更改为"Alan Jones"

检查字符串的内容

有时,您需要检查一段文本是否以某些字符开头或结尾,或者是否包含某些字符。您可以通过名为StartsWithEndsWithContains的方法来实现:

  1. 添加语句来存储一个string值,然后检查它是否以两个不同的string值开头或包含两个不同的string值,如下代码所示:

    string company = "Microsoft";
    bool startsWithM = company.StartsWith("M"); 
    bool containsN = company.Contains("N");
    WriteLine($"Text: {company}");
    WriteLine($"Starts with M: {startsWithM}, contains an N: {containsN}"); 
  2. 运行代码并查看结果,如以下输出所示:

    Text: Microsoft
    Starts with M: True, contains an N: False 

连接、格式化和其他字符串成员

还有很多其他string成员,如下表所示:

| 成员 | 描述 | | `Trim`、`TrimStart`、`TrimEnd` | 这些方法从开头和/或结尾修剪空格字符,如空格、制表符和回车符。 | | `ToUpper`、`ToLower` | 这些将所有字符转换为大写或小写。 | | `Insert`、`Remove` | 这些方法可以插入或删除一些文本。 | | `Replace` | 这将用其他文本替换某些文本。 | | `string.Empty` | 这可以代替每次使用文本`string`值时使用空双引号(`""`时分配内存。 | | `string.Concat` | 这连接了两个`string`变量。当在`string`操作数之间使用时,+运算符执行等效操作。 | | `string.Join` | 这将连接一个或多个`string`变量,每个变量之间有一个字符。 | | `string.IsNullOrEmpty` | 检查`string`变量是`null`还是空。 | | `string.IsNullOrWhitespace` | 检查`string`变量是`null`还是空白;也就是说,任意数量的水平和垂直间距字符的混合,例如制表符、空格、回车符、换行符等。 | | `string.Format` | 字符串插值的另一种方法,用于输出格式化的`string`值,该方法使用定位参数而不是命名参数。 |

前面的一些方法是静态方法。这意味着该方法只能从类型调用,不能从变量实例调用。在上表中,我通过在静态方法前面加上string.来表示它们,如string.Format中所示。

让我们探讨一下其中的一些方法:

  1. 添加语句以获取字符串值数组,并使用Join方法将它们重新组合成带有分隔符的单个字符串变量,如以下代码所示:

    string recombined = string.Join(" => ", citiesArray); 
    WriteLine(recombined); 
  2. 运行代码并查看结果,如以下输出所示:

    Paris => Tehran => Chennai => Sydney => New York => Medellín 
  3. 添加语句以使用定位参数和插值字符串格式语法两次输出相同的三个变量,如以下代码所示:

    string fruit = "Apples"; 
    decimal price =  0.39M; 
    DateTime when = DateTime.Today;
    WriteLine($"Interpolated:  {fruit} cost {price:C} on {when:dddd}."); 
    WriteLine(string.Format("string.Format: {0} cost {1:C} on {2:dddd}.",
      arg0: fruit, arg1: price, arg2: when)); 
  4. 运行代码并查看结果,如以下输出所示:

    Interpolated:  Apples cost £0.39 on Thursday. 
    string.Format: Apples cost £0.39 on Thursday. 

注意,我们可以简化第二条语句,因为WriteLine支持与string.Format相同的格式代码,如下代码所示:

WriteLine("WriteLine: {0} cost {1:C} on {2:dddd}.",
  arg0: fruit, arg1: price, arg2: when); 

有效地构建字符串

您可以使用String.Concat方法或简单地使用+操作符将两个字符串连接起来以创建新的string。但这两种选择都是错误的做法,因为.NET 必须在内存中创建一个全新的string

如果您只添加两个string值,这可能不明显,但是如果您在一个循环中进行多次迭代,这可能会对性能和内存使用产生显著的负面影响。在第 12 章使用多任务提高性能和可伸缩性,您将学习如何使用StringBuilder类型高效地连接string变量。

处理日期和时间

在数字和文本之后,接下来最流行的数据类型是日期和时间。两种主要类型如下:

  • DateTime:表示固定时间点的日期和时间组合值。
  • TimeSpan:表示一段时间的持续时间。

这两种类型通常一起使用。例如,如果将一个DateTime值与另一个DateTime值相减,则结果为TimeSpan。如果将TimeSpan添加到DateTime,则结果为DateTime值。

指定日期和时间值

创建日期和时间值的常用方法是为日期和时间组件(如天和小时)指定单独的值,如下表所述:

| 日期/时间参数 | 值范围 | | `year` | 1 至 9999 | | `month` | 1 至 12 | | `day` | 1 到该月的天数 | | `hour` | 0 至 23 | | `minute` | 0 至 59 | | `second` | 0 至 59 |

另一种方法是将值提供为要解析的string,但这可能会被误解,具体取决于线程的默认区域性。例如,在英国,日期指定为日/月/年,而在美国,日期指定为月/日/年。

让我们看看您可能希望如何处理日期和时间:

  1. 使用您首选的代码编辑器将名为WorkingWithTime的新控制台应用添加到Chapter08解决方案/工作区。

  2. 在 Visual Studio 代码中,选择WorkingWithTime作为活动的 OmniSharp 项目。

  3. Program.cs中,删除已有的语句,然后添加语句来初始化一些特殊的日期/时间值,如下代码所示:

    WriteLine("Earliest date/time value is: {0}",
      arg0: DateTime.MinValue);
    WriteLine("UNIX epoch date/time value is: {0}",
      arg0: DateTime.UnixEpoch);
    WriteLine("Date/time value Now is: {0}",
      arg0: DateTime.Now);
    WriteLine("Date/time value Today is: {0}",
      arg0: DateTime.Today); 
  4. 运行代码并记录结果,如以下输出所示:

    Earliest date/time value is: 01/01/0001 00:00:00
    UNIX epoch date/time value is: 01/01/1970 00:00:00
    Date/time value Now is: 23/04/2021 14:14:54
    Date/time value Today is: 23/04/2021 00:00:00 
  5. 添加语句定义 2021 年的圣诞节(如果这是过去的,则使用未来的一年),并以各种方式显示,如下代码所示:

    DateTime christmas = new(year: 2021, month: 12, day: 25);
    WriteLine("Christmas: {0}",
      arg0: christmas); // default format
    WriteLine("Christmas: {0:dddd, dd MMMM yyyy}",
      arg0: christmas); // custom format
    WriteLine("Christmas is in month {0} of the year.",
      arg0: christmas.Month);
    WriteLine("Christmas is day {0} of the year.",
      arg0: christmas.DayOfYear);
    WriteLine("Christmas {0} is on a {1}.",
      arg0: christmas.Year,
      arg1: christmas.DayOfWeek); 
  6. 运行代码并记录结果,如以下输出所示:

    Christmas: 25/12/2021 00:00:00
    Christmas: Saturday, 25 December 2021
    Christmas is in month 12 of the year.
    Christmas is day 359 of the year.
    Christmas 2021 is on a Saturday. 
  7. 添加语句对圣诞节进行加减,如下代码所示:

    DateTime beforeXmas = christmas.Subtract(TimeSpan.FromDays(12));
    DateTime afterXmas = christmas.AddDays(12);
    WriteLine("12 days before Christmas is: {0}",
      arg0: beforeXmas);
    WriteLine("12 days after Christmas is: {0}",
      arg0: afterXmas);
    TimeSpan untilChristmas = christmas - DateTime.Now;
    WriteLine("There are {0} days and {1} hours until Christmas.",
      arg0: untilChristmas.Days,
      arg1: untilChristmas.Hours);
    WriteLine("There are {0:N0} hours until Christmas.",
      arg0: untilChristmas.TotalHours); 
  8. 运行代码并记录结果,如以下输出所示:

    12 days before Christmas is: 13/12/2021 00:00:00
    12 days after Christmas is: 06/01/2022 00:00:00
    There are 245 days and 9 hours until Christmas.
    There are 5,890 hours until Christmas. 
  9. 添加语句以定义您的孩子在圣诞节醒来打开礼物的时间,并以各种方式显示,如以下代码所示:

    DateTime kidsWakeUp = new(
      year: 2021, month: 12, day: 25, 
      hour: 6, minute: 30, second: 0);
    WriteLine("Kids wake up on Christmas: {0}",
      arg0: kidsWakeUp);
    WriteLine("The kids woke me up at {0}",
      arg0: kidsWakeUp.ToShortTimeString()); 
  10. 运行代码并记录结果,如以下输出所示:

```cs
Kids wake up on Christmas: 25/12/2021 06:30:00
The kids woke me up at 06:30 
```

全球化与时代

当前区域性控制日期和时间的解析方式:

  1. Program.cs顶部,导入System.Globalization名称空间。

  2. 添加语句以显示用于显示日期和时间值的当前区域性,然后解析美国独立日并以各种方式显示,如以下代码所示:

    WriteLine("Current culture is: {0}",
      arg0: CultureInfo.CurrentCulture.Name);
    string textDate = "4 July 2021";
    DateTime independenceDay = DateTime.Parse(textDate);
    WriteLine("Text: {0}, DateTime: {1:d MMMM}",
      arg0: textDate,
      arg1: independenceDay);
    textDate = "7/4/2021";
    independenceDay = DateTime.Parse(textDate);
    WriteLine("Text: {0}, DateTime: {1:d MMMM}",
      arg0: textDate,
      arg1: independenceDay);
    independenceDay = DateTime.Parse(textDate,
      provider: CultureInfo.GetCultureInfo("en-US"));
    WriteLine("Text: {0}, DateTime: {1:d MMMM}",
      arg0: textDate,
      arg1: independenceDay); 
  3. Run the code and note the results, as shown in the following output:

    Current culture is: en-GB
    Text: 4 July 2021, DateTime: 4 July
    Text: 7/4/2021, DateTime: 7 April
    Text: 7/4/2021, DateTime: 4 July 

    在我的电脑上,现在的文化是英国英语。如果日期为 2021 年 7 月 4 日,则无论当前文化是英国文化还是美国文化,都会正确解析该日期。但是如果日期是 2021 年 7 月 4 日,那么它被错误地解析为 4 月 7 日。您可以通过在解析时指定正确的区域性作为提供程序来覆盖当前区域性,如上面的第三个示例所示。

  4. 将语句添加到 2020 年到 2025 年的循环中,显示该年是否为闰年以及 2 月有多少天,然后显示圣诞节和独立日是否在夏令时,如以下代码所示:

    for (int year = 2020; year < 2026; year++)
    {
      Write($"{year} is a leap year: {DateTime.IsLeapYear(year)}. ");
      WriteLine("There are {0} days in February {1}.",
        arg0: DateTime.DaysInMonth(year: year, month: 2), arg1: year);
    }
    WriteLine("Is Christmas daylight saving time? {0}",
      arg0: christmas.IsDaylightSavingTime());
    WriteLine("Is July 4th daylight saving time? {0}",
      arg0: independenceDay.IsDaylightSavingTime()); 
  5. 运行代码并记录结果,如以下输出所示:

    2020 is a leap year: True. There are 29 days in February 2020.
    2021 is a leap year: False. There are 28 days in February 2021.
    2022 is a leap year: False. There are 28 days in February 2022.
    2023 is a leap year: False. There are 28 days in February 2023.
    2024 is a leap year: True. There are 29 days in February 2024.
    2025 is a leap year: False. There are 28 days in February 2025.
    Is Christmas daylight saving time? False
    Is July 4th daylight saving time? True 

只使用日期或时间工作

.NET6 引入了一些新类型,用于仅使用日期值或仅使用名为DateOnlyTimeOnly的时间值。这比使用时间为零的DateTime值来存储仅日期的值要好,因为它是类型安全的,并且避免了误用。DateOnly还可以更好地映射到数据库列类型,例如 SQL Server 中的date列。TimeOnly用于设置报警和安排定期会议或事件,并映射到 SQL Server 中的time列。

让我们用它们来为英格兰女王策划一个派对:

  1. 添加语句来定义女王的生日,以及她的派对开始的时间,然后将这两个值组合成一个日历条目,这样我们就不会错过她的派对,如下代码所示:

    DateOnly queensBirthday = new(year: 2022, month: 4, day: 21);
    WriteLine($"The Queen's next birthday is on {queensBirthday}.");
    TimeOnly partyStarts = new(hour: 20, minute: 30);
    WriteLine($"The Queen's party starts at {partyStarts}.");
    DateTime calendarEntry = queensBirthday.ToDateTime(partyStarts);
    WriteLine($"Add to your calendar: {calendarEntry}."); 
  2. 运行代码并记录结果,如以下输出所示:

    The Queen's next birthday is on 21/04/2022.
    The Queen's party starts at 20:30.
    Add to your calendar: 21/04/2022 20:30:00. 

正则表达式模式匹配

正则表达式对于验证用户的输入非常有用。它们非常强大,可以变得非常复杂。几乎所有编程语言都支持正则表达式,并使用一组通用的特殊字符来定义它们。

让我们尝试一些正则表达式示例:

  1. 使用您首选的代码编辑器将名为WorkingWithRegularExpressions的新控制台应用添加到Chapter08解决方案/工作区。

  2. 在 Visual Studio 代码中,选择WorkingWithRegularExpressions作为活动的 OmniSharp 项目。

  3. Program.cs中,导入以下命名空间:

    using System.Text.RegularExpressions; 

检查作为文本输入的数字

我们将首先实现验证数字输入的常见示例:

  1. Add statements to prompt the user to enter their age and then check that it is valid using a regular expression that looks for a digit character, as shown in the following code:

    Write("Enter your age: "); 
    string? input = ReadLine();
    Regex ageChecker = new(@"\d"); 
    if (ageChecker.IsMatch(input))
    {
      WriteLine("Thank you!");
    }
    else
    {
      WriteLine($"This is not a valid age: {input}");
    } 

    请注意以下代码:

    • @字符关闭了在字符串中使用转义字符的功能。转义字符的前缀为反斜杠。例如,\t表示制表符,\n表示新行。在编写正则表达式时,我们需要禁用此功能。用电视剧《西翼》的话说,“反斜杠就是反斜杠。”
    • 一旦用@禁用转义字符,就可以用正则表达式对其进行解释。例如,\d表示数字。在本主题后面部分,您将了解更多以反斜杠为前缀的正则表达式。
  2. 运行代码,为年龄输入一个整数,如34,并查看结果,如以下输出所示:

    Enter your age: 34 
    Thank you! 
  3. 再次运行代码,输入carrots,查看结果,如以下输出所示:

    Enter your age: carrots
    This is not a valid age: carrots 
  4. Run the code again, enter bob30smith, and view the result, as shown in the following output:

    Enter your age: bob30smith 
    Thank you! 

    我们使用的正则表达式是\d,表示一位数。但是,它没有指定在这一数字之前和之后可以输入什么。这个正则表达式可以用英语描述为“输入任意字符,只要输入至少一个数字字符。”

    在正则表达式中,用插入符号^表示某些输入的开始,用美元$表示某些输入的结束。让我们使用这些符号来表示,在输入的开始和结束之间,除了一个数字之外,我们不需要其他任何东西。

  5. 将正则表达式更改为^\d$,如下代码中突出显示:

    Regex ageChecker = new(@"^**\d$"**); 
  6. 再次运行代码,请注意,它会拒绝除单个数字以外的任何输入。我们希望允许一个或多个数字。为此,我们在\d表达式后添加+,以将含义修改为一个或多个。

  7. 更改正则表达式,如下代码中突出显示:

    Regex ageChecker = new(@"^**\d+$"**); 
  8. 再次运行代码,注意正则表达式只允许任何长度的零或正整数。

正则表达式性能改进

用于处理正则表达式的.NET 类型在整个.NET 平台和许多用它构建的应用中都使用。因此,它们对性能有着显著的影响,但直到现在,它们还没有得到微软的太多优化关注。

对于.NET5 和更高版本,System.Text.RegularExpressions名称空间重写了内部结构,以挤出最大性能。使用IsMatch等方法的通用正则表达式基准测试现在快了五倍。最好的是,您不必更改代码就可以获得好处!

理解正则表达式的语法

下面是一些常见的正则表达式符号,可以在正则表达式中使用:

| 象征 | 意思 | 象征 | 意思 | | `^` | 输入开始 | `$` | 输入结束 | | `\d` | 一位数 | `\D` | 单个非数字 | | `\s` | 空白 | `\S` | 非空白 | | `\w` | 文字字符 | `\W` | 非单词字符 | | `[A-Za-z0-9]` | 字符范围 | `\^` | ^(插入符号)字符 | | `[aeiou]` | 字符集 | `[^aeiou]` | 不是在一组字符中 | | `.` | 任何单个字符 | `\.` | . (点)字符 |

此外,以下是一些影响正则表达式中先前符号的正则表达式量词:

| 象征 | 意思 | 象征 | 意思 | | `+` | 一个或多个 | `?` | 一个或没有 | | `{3}` | 正好三个 | `{3,5}` | 三五 | | `{3,}` | 至少三个 | `{,3}` | 最多三个 |

正则表达式示例

以下是正则表达式的一些示例,并对其含义进行了说明:

| 表示 | 意思 | | `\d` | 输入中某处的一位数字 | | `a` | 输入中某处的字符*a* | | `Bob` | 输入中某处的单词*Bob* | | `^Bob` | 输入开头的单词*Bob* | | `Bob$` | 输入末尾的单词*Bob* | | `^\d{2}$` | 正好两位数 | | `^[0-9]{2}$` | 正好两位数 | | `^[A-Z]{4,}$` | ASCII 字符集中至少有四个大写英文字母 | | `^[A-Za-z]{4,}$` | ASCII 字符集中至少有四个大写或小写英文字母 | | `^[A-Z]{2}\d{3}$` | ASCII 字符集中的两个大写英文字母和三位数字 | | `^[A-Za-z\u00c0-\u017e]+$` | ASCII 字符集中至少有一个大写或小写英文字母,Unicode 字符集中至少有一个欧洲字母,如下表所示:ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿıŒœŠšŸ Žž | | `^d.g$` | 字母*d*,然后是任意字符,然后是字母*g*,因此它将同时匹配*dig*和*dog*,或者匹配*d*和*g*之间的任何单个字符 | | `^d\.g$` | 字母*d*,然后是一个点(.),然后是字母*g*,因此它将只匹配*d.g* |

良好实践:使用正则表达式验证用户输入。相同的正则表达式可以在其他语言(如 JavaScript 和 Python)中重用。

拆分复杂的逗号分隔字符串

在本章前面,您学习了如何拆分一个简单的逗号分隔的字符串变量。但是下面的电影名称的例子呢?

"Monsters, Inc.","I, Tonya","Lock, Stock and Two Smoking Barrels" 

string值在每个电影标题周围使用双引号。我们可以使用它们来确定是否需要使用逗号(或不使用逗号)进行拆分。Split方法功能不够强大,因此我们可以使用正则表达式。

良好实践:您可以在以下链接阅读激发此任务的堆栈溢出文章中的更全面解释:https://stackoverflow.com/questions/18144431/regex-to-split-a-csv

为了在string值中包含双引号,我们在前面加上反斜杠:

  1. 添加语句来存储一个复杂的逗号分隔的string变量,然后使用Split方法以哑方式拆分,如下代码所示:

    string films = "\"Monsters, Inc.\",\"I, Tonya\",\"Lock, Stock and Two Smoking Barrels\"";
    WriteLine($"Films to split: {films}");
    string[] filmsDumb = films.Split(',');
    WriteLine("Splitting with string.Split method:"); 
    foreach (string film in filmsDumb)
    {
      WriteLine(film);
    } 
  2. 添加语句定义正则表达式,以智能方式拆分和写入电影片名,如下代码所示:

    WriteLine();
    Regex csv = new(
      "(?:^|,)(?=[^\"]|(\")?)\"?((?(1)[^\"]*|[^,\"]*))\"?(?=,|$)");
    MatchCollection filmsSmart = csv.Matches(films);
    WriteLine("Splitting with regular expression:"); 
    foreach (Match film in filmsSmart)
    {
      WriteLine(film.Groups[2].Value);
    } 
  3. 运行代码并查看结果,如以下输出所示:

    Splitting with string.Split method: 
    "Monsters
     Inc." 
    "I
     Tonya" 
    "Lock
     Stock and Two Smoking Barrels" 
    Splitting with regular expression: 
    Monsters, Inc.
    I, Tonya
    Lock, Stock and Two Smoking Barrels 

在集合中存储多个对象

另一种最常见的数据类型是收集。如果需要在变量中存储多个值,则可以使用集合。

集合是内存中的数据结构,可以以不同的方式管理多个项,尽管所有集合都有一些共享功能。

下表显示了.NET 中用于处理集合的最常见类型:

| 名称空间 | 示例类型 | 描述 | | `System .Collections` | `IEnumerable`、`IEnumerable` | 集合使用的接口和基类。 | | `System .Collections .Generic` | `List`、`Dictionary`、`Queue`、`Stack` | 在 C# 2.0 和.NET Framework 2.0 中引入。这些集合允许您使用泛型类型参数(更安全、更快、更高效)指定要存储的类型。 | | `System .Collections .Concurrent` | `BlockingCollection`、`ConcurrentDictionary`、`ConcurrentQueue` | 这些集合在多线程场景中使用是安全的。 | | `System .Collections .Immutable` | `ImmutableArray`、`ImmutableDictionary`、`ImmutableList`、`ImmutableQueue` | 设计用于原始集合的内容永远不会更改的场景,尽管它们可以将修改后的集合创建为新实例。 |

所有收藏的共同特点

所有集合实现与ICollection接口;这意味着它们必须有一个Count属性来告诉您其中有多少个对象,如下代码所示:

namespace System.Collections
{
  public interface ICollection : IEnumerable
  {
    int Count { get; }
    bool IsSynchronized { get; }
    object SyncRoot { get; }
    void CopyTo(Array array, int index);
  }
} 

例如,如果我们有一个名为passengers的集合,我们可以这样做:

int howMany = passengers.Count; 

所有集合都实现了IEnumerable接口,这意味着它们可以使用foreach语句进行迭代。它们必须有一个返回实现了IEnumerator的对象的GetEnumerator方法;这意味着返回的object必须具有在集合中导航的MoveNextReset方法以及包含集合中当前项的Current属性,如下代码所示:

namespace System.Collections
{
  public interface IEnumerable
  {
    IEnumerator GetEnumerator();
  }
}
namespace System.Collections
{
  public interface IEnumerator
  {
    object Current { get; }
    bool MoveNext();
    void Reset();
  }
} 

例如,要对passengers集合中的每个对象执行操作,我们可以编写以下代码:

foreach (Passenger p in passengers)
{
  // perform an action on each passenger
} 

除了基于object的集合接口外,还有泛型接口和类,泛型类型定义了集合中存储的类型,如下代码所示:

namespace System.Collections.Generic
{
  public interface ICollection<T> : IEnumerable<T>, IEnumerable
  {
    int Count { get; }
    bool IsReadOnly { get; }
    void Add(T item);
    void Clear();
    bool Contains(T item);
    void CopyTo(T[] array, int index);
    bool Remove(T item);
  }
} 

通过确保集合的容量提高性能

自.NET 1.1 以来,像StringBuilder这样的类型已经有了一个名为EnsureCapacity的方法,可以将其内部存储阵列预先设定为string的预期最终大小。这提高了性能,因为它不必在追加更多字符时重复增加数组的大小。

自.NET Core 2.1 以来,像Dictionary<T>HashSet<T>这样的类型也有了EnsureCapacity

在.NET 6 及更高版本中,像List<T>Queue<T>Stack<T>这样的集合现在也有一个EnsureCapacity方法,如下代码所示:

List<string> names = new();
names.EnsureCapacity(10_000);
// load ten thousand names into the list 

了解收藏选择

有几种不同的集合选择可用于不同的目的:列表、字典、堆栈、队列、集合和许多其他更专业的集合。

列表

列表,即实现IList<T>的类型,是有序集合,如下面的代码所示:

namespace System.Collections.Generic
{
  [DefaultMember("Item")] // aka this indexer
  public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
  {
    T this[int index] { get; set; }
    int IndexOf(T item);
    void Insert(int index, T item);
    void RemoveAt(int index);
  }
} 

IList<T>源于ICollection<T>,所以它有Count属性,有Add方法将项目放在集合的末尾,有Insert方法将项目放在列表中的指定位置,有RemoveAt方法将项目移除到指定位置。

当您想要手动控制集合中项目的顺序时,列表是一个不错的选择。列表中的每个项目都有一个自动分配的唯一索引(或位置)。项目可以是T定义的任何类型,项目可以重复。索引是int类型,从0开始,所以列表中的第一项位于索引0,如下表所示:

| 指数 | 项目 | | 0 | 伦敦 | | 1. | 巴黎 | | 2. | 伦敦 | | 3. | 悉尼 |

如果在伦敦和悉尼之间插入新项目(例如圣地亚哥),则悉尼的索引将自动递增。因此,您必须注意,在插入或删除项后,项的索引可能会发生更改,如下表所示:

| 指数 | 项目 | | 0 | 伦敦 | | 1. | 巴黎 | | 2. | 伦敦 | | 3. | 圣地亚哥 | | 4. | 悉尼 |

辞典

当每个(或对象)都有一个唯一的子值(或一个虚构的值)时,字典是一个很好的选择,它可以用作,以便在以后的集合中快速查找值。密钥必须是唯一的。例如,如果要存储人员列表,可以选择使用政府颁发的身份号码作为密钥。

把键想象成现实世界字典中的索引项。它允许你快速找到单词的定义,因为单词(例如,键)被保持排序,如果我们知道我们在寻找 HytT0. HATATEE OUTT1T 的定义,那么我们会跳到词典的中间来开始寻找,因为字母 To2 t2 M m Tr3^在字母表的中间。

编程中的字典在查找某些内容时也同样聪明。必须实现IDictionary<TKey, TValue>接口,如下代码所示:

namespace System.Collections.Generic
{
  [DefaultMember("Item")] // aka this indexer
  public interface IDictionary<TKey, TValue>
    : ICollection<KeyValuePair<TKey, TValue>>,
      IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable
  {
    TValue this[TKey key] { get; set; }
    ICollection<TKey> Keys { get; }
    ICollection<TValue> Values { get; }
    void Add(TKey key, TValue value);
    bool ContainsKey(TKey key);
    bool Remove(TKey key);
    bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value);
  }
} 

字典中的项目是struct的实例,也就是值类型KeyValuePair<TKey, TValue>,其中TKey是键的类型,TValue是值的类型,如下代码所示:

namespace System.Collections.Generic
{
  public readonly struct KeyValuePair<TKey, TValue>
  {
    public KeyValuePair(TKey key, TValue value);
    public TKey Key { get; }
    public TValue Value { get; }
    [EditorBrowsable(EditorBrowsableState.Never)]
    public void Deconstruct(out TKey key, out TValue value);
    public override string ToString();
  }
} 

示例Dictionary<string, Person>使用string作为键,使用Person实例作为值。Dictionary<string, string>使用string值,如下表所示:

| 钥匙 | 价值 | | 牛血清白蛋白 | 鲍勃·史密斯 | | 兆瓦 | 马克斯·威廉姆斯 | | BSB | 鲍勃·史密斯 | | 是 | 阿米尔·穆罕默德 |

堆叠

当您想要实现后进先出后进先出行为时,堆栈是一个不错的选择。使用堆栈,您只能直接访问或删除堆栈顶部的一个项目,尽管您可以枚举以读取整个堆栈中的项目。例如,您不能直接访问堆栈中的第二项。

例如,字处理器使用堆栈来记住您最近执行的操作序列,然后当您按 Ctrl+Z 时,它将撤消堆栈中的最后一个操作,然后撤消下一个到最后一个操作,依此类推。

排队

当您想要实现先进先出FIFO行为时,队列是一个不错的选择。对于队列,您只能直接访问或删除队列前面的一个项目,尽管您可以枚举以读取整个队列中的项目。例如,您不能直接访问队列中的第二项。

例如,后台流程使用队列按照工作项到达的顺序处理工作项,就像人们在邮局排队一样。

.NET 6 引入了PriorityQueue,队列中的每个项目都分配了优先级值以及它们在队列中的位置。

设置

当您想要在两个集合之间执行集合操作时,集合是一个不错的选择。例如,您可能有两个城市名称集合,您想知道两个集合中出现的名称(称为集合之间的相交。集合中的项目必须是唯一的。

收集方法摘要

每个集合都有一套不同的方法用于添加和删除项目,如下表所示:

| 收集 | 添加方法 | 删除方法 | 描述 | | 列表 | `Add`、`Insert` | `Remove`、`RemoveAt` | 列表是按顺序排列的,因此项目具有整数索引位置。`Add`将在列表末尾添加一个新项目。`Insert`将在指定的索引位置添加新项目。 | | 词典 | `Add` | `Remove` | 字典没有排序,因此项没有整数索引位置。您可以通过调用`ContainsKey`方法来检查是否使用了密钥。 | | 堆栈 | `Push` | `Pop` | 堆栈始终使用`Push`方法在堆栈顶部添加新项目。第一项在底部。始终使用`Pop`方法从堆栈顶部移除项目。调用`Peek`方法查看此值而不删除它。 | | 队列 | `Enqueue` | `Dequeue` | 队列始终使用`Enqueue`方法在队列末尾添加新项目。第一项在队列的前面。始终使用`Dequeue`方法从队列前面移除项目。调用`Peek`方法查看此值而不删除它。 |

使用列表

让我们浏览以下列表:

  1. 使用您首选的代码编辑器将名为WorkingWithCollections的新控制台应用添加到Chapter08解决方案/工作区。

  2. 在 Visual Studio 代码中,选择WorkingWithCollections作为活动的 OmniSharp 项目。

  3. Program.cs中,删除已有的语句,然后定义一个函数,输出一组带有标题的string值,如下代码所示:

    static void Output(string title, IEnumerable<string> collection)
    {
      WriteLine(title);
      foreach (string item in collection)
      {
        WriteLine($"  {item}");
      }
    } 
  4. 定义一个名为WorkingWithLists的静态方法来说明定义和使用列表的一些常用方法,如以下代码所示:

    static void WorkingWithLists()
    {
      // Simple syntax for creating a list and adding three items
      List<string> cities = new(); 
      cities.Add("London"); 
      cities.Add("Paris"); 
      cities.Add("Milan");
      /* Alternative syntax that is converted by the compiler into
         the three Add method calls above
      List<string> cities = new()
        { "London", "Paris", "Milan" };
      */
      /* Alternative syntax that passes an 
         array of string values to AddRange method
      List<string> cities = new(); 
      cities.AddRange(new[] { "London", "Paris", "Milan" });
      */
      Output("Initial list", cities);
      WriteLine($"The first city is {cities[0]}."); 
      WriteLine($"The last city is {cities[cities.Count - 1]}.");
      cities.Insert(0, "Sydney");
      Output("After inserting Sydney at index 0", cities); 
      cities.RemoveAt(1); 
      cities.Remove("Milan");
      Output("After removing two cities", cities);
    } 
  5. Program.cs顶部导入名称空间后,调用WorkingWithLists方法,如下代码所示:

    WorkingWithLists(); 
  6. 运行代码并查看结果,如以下输出所示:

    Initial list
      London
      Paris
      Milan
    The first city is London. 
    The last city is Milan.
    After inserting Sydney at index 0
      Sydney
      London
      Paris
      Milan
    After removing two cities
      Sydney
      Paris 

使用字典

让我们来探索字典:

  1. Program.cs中,定义一个名为WorkingWithDictionaries的静态方法来说明使用字典的一些常用方法,例如,查找单词定义,如以下代码所示:

    static void WorkingWithDictionaries()
    {
      Dictionary<string, string> keywords = new();
      // add using named parameters
      keywords.Add(key: "int", value: "32-bit integer data type");
      // add using positional parameters
      keywords.Add("long", "64-bit integer data type"); 
      keywords.Add("float", "Single precision floating point number");
      /* Alternative syntax; compiler converts this to calls to Add method
      Dictionary<string, string> keywords = new()
      {
        { "int", "32-bit integer data type" },
        { "long", "64-bit integer data type" },
        { "float", "Single precision floating point number" },
      }; */
      /* Alternative syntax; compiler converts this to calls to Add method
      Dictionary<string, string> keywords = new()
      {
        ["int"] = "32-bit integer data type",
        ["long"] = "64-bit integer data type",
        ["float"] = "Single precision floating point number", // last comma is optional
      }; */
      Output("Dictionary keys:", keywords.Keys);
      Output("Dictionary values:", keywords.Values);
      WriteLine("Keywords and their definitions");
      foreach (KeyValuePair<string, string> item in keywords)
      {
        WriteLine($"  {item.Key}: {item.Value}");
      }
      // lookup a value using a key
      string key = "long";
      WriteLine($"The definition of {key} is {keywords[key]}");
    } 
  2. Program.cs顶部,注释掉前面的方法调用,然后调用WorkingWithDictionaries方法,如下代码所示:

    // WorkingWithLists();
    WorkingWithDictionaries(); 
  3. 运行代码并查看结果,如以下输出所示:

    Dictionary keys:
      int
      long
      float
    Dictionary values:
      32-bit integer data type
      64-bit integer data type
      Single precision floating point number
    Keywords and their definitions
      int: 32-bit integer data type
      long: 64-bit integer data type
      float: Single precision floating point number
    The definition of long is 64-bit integer data type 

使用队列

让我们探索一下队列:

  1. Program.cs中,定义一个名为WorkingWithQueues的静态方法,以说明处理队列的一些常见方式,例如,在咖啡队列中处理客户,如下代码所示:

    static void WorkingWithQueues()
    {
      Queue<string> coffee = new();
      coffee.Enqueue("Damir"); // front of queue
      coffee.Enqueue("Andrea");
      coffee.Enqueue("Ronald");
      coffee.Enqueue("Amin");
      coffee.Enqueue("Irina"); // back of queue
      Output("Initial queue from front to back", coffee);
      // server handles next person in queue
      string served = coffee.Dequeue();
      WriteLine($"Served: {served}.");
      // server handles next person in queue
      served = coffee.Dequeue();
      WriteLine($"Served: {served}.");
      Output("Current queue from front to back", coffee);
      WriteLine($"{coffee.Peek()} is next in line.");
      Output("Current queue from front to back", coffee);
    } 
  2. Program.cs的顶部,注释出前面的方法调用并调用WorkingWithQueues方法。

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

    Initial queue from front to back
      Damir
      Andrea
      Ronald
      Amin
      Irina
    Served: Damir.
    Served: Andrea.
    Current queue from front to back
      Ronald
      Amin
      Irina
    Ronald is next in line.
    Current queue from front to back
      Ronald
      Amin
      Irina 
  4. Define a static method named OutputPQ, as shown in the following code:

    static void OutputPQ<TElement, TPriority>(string title,
      IEnumerable<(TElement Element, TPriority Priority)> collection)
    {
      WriteLine(title);
      foreach ((TElement, TPriority) item in collection)
      {
        WriteLine($"  {item.Item1}: {item.Item2}");
      }
    } 

    注意,OutputPQ方法是通用的。您可以指定作为collection传入的元组中使用的两种类型。

  5. 定义一个名为WorkingWithPriorityQueues的静态方法,如下代码所示:

    static void WorkingWithPriorityQueues()
    {
      PriorityQueue<string, int> vaccine = new();
      // add some people
      // 1 = high priority people in their 70s or poor health
      // 2 = medium priority e.g. middle aged
      // 3 = low priority e.g. teens and twenties
      vaccine.Enqueue("Pamela", 1);  // my mum (70s)
      vaccine.Enqueue("Rebecca", 3); // my niece (teens)
      vaccine.Enqueue("Juliet", 2);  // my sister (40s)
      vaccine.Enqueue("Ian", 1);     // my dad (70s)
      OutputPQ("Current queue for vaccination:", vaccine.UnorderedItems);
      WriteLine($"{vaccine.Dequeue()} has been vaccinated.");
      WriteLine($"{vaccine.Dequeue()} has been vaccinated.");
      OutputPQ("Current queue for vaccination:", vaccine.UnorderedItems);
      WriteLine($"{vaccine.Dequeue()} has been vaccinated.");
      vaccine.Enqueue("Mark", 2); // me (40s)
      WriteLine($"{vaccine.Peek()} will be next to be vaccinated.");
      OutputPQ("Current queue for vaccination:", vaccine.UnorderedItems);
    } 
  6. Program.cs的顶部,注释出前面的方法调用并调用WorkingWithPriorityQueues方法。

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

    Current queue for vaccination:
      Pamela: 1
      Rebecca: 3
      Juliet: 2
      Ian: 1
    Pamela has been vaccinated.
    Ian has been vaccinated.
    Current queue for vaccination:
      Juliet: 2
      Rebecca: 3
    Juliet has been vaccinated.
    Mark will be next to be vaccinated.
    Current queue for vaccination:
      Mark: 2
      Rebecca: 3 

分类集合

List<T>类可以通过手动调用其Sort方法进行排序(但请记住,每个项的索引都会改变)。手动排序string值列表或其他内置类型将在您不需要额外努力的情况下工作,但如果您创建自己类型的集合,则该类型必须实现名为IComparable的接口。您在第 6 章**中学习了如何实现接口和继承类

无法对Stack<T>Queue<T>集合进行排序,因为您通常不需要该功能;例如,您可能永远不会对排队入住酒店的客人进行排序。但有时,您可能需要对字典或集合进行排序。

有时,拥有一个自动排序的集合会很有用,也就是说,在添加和删除项目时,该集合可以按排序顺序维护项目。

有多个自动排序集合可供选择。这些排序的集合之间的差异通常很细微,但可能会对应用的内存需求和性能产生影响,因此值得为您的需求选择最合适的选项。

下表显示了一些常见的自动分拣集合:

| 收集 | 描述 | | `SortedDictionary` | 这表示按键排序的键/值对的集合。 | | `SortedList` | 这表示按键排序的键/值对的集合。 | | `SortedSet` | 这表示按排序顺序维护的唯一对象的集合。 |

更专业的收藏

还有一些特殊情况下的收藏。

使用紧凑的位值数组

System.Collections.BitArray集合管理一个紧凑的位值数组,用布尔表示,其中true表示位为开(值为 1)false表示位为关(值为 0)。

使用高效的列表

System.Collections.Generics.LinkedList<T>集合表示一个双链接列表,其中每个项目都有对其上一个和下一个项目的引用。与List<T>相比,它们提供了更好的性能,适用于您经常在列表中间插入和删除项目的情况。在LinkedList<T>中,项目不必在内存中重新排列。

使用不可变集合

有时,您需要使集合不可变,这意味着其成员不能更改;也就是说,您不能添加或删除它们。

如果导入System.Collections.Immutable名称空间,那么任何实现IEnumerable<T>的集合都会得到六个扩展方法,以将其转换为不可变列表、字典、哈希集等。

让我们看一个简单的例子:

  1. WorkingWithCollections项目中,在Program.cs中导入System.Collections.Immutable名称空间。

  2. WorkingWithLists方法中,在方法的末尾添加语句,将cities列表转换为不可变列表,然后向其添加一个新的城市,如下代码所示:

    ImmutableList<string> immutableCities = cities.ToImmutableList();
    ImmutableList<string> newList = immutableCities.Add("Rio");
    Output("Immutable list of cities:", immutableCities); 
    Output("New list of cities:", newList); 
  3. Program.cs的顶部,注释前面的方法调用,并取消注释对WorkingWithLists方法的调用。

  4. 运行代码,查看结果,注意不可变城市列表在调用Add方法时不会被修改;相反,它返回一个包含新添加城市的新列表,如以下输出所示:

    Immutable list of cities:
      Sydney
      Paris
    New list of cities:
      Sydney
      Paris
      Rio 

良好实践:为了提高性能,许多应用将常用访问对象的共享副本存储在中央缓存中。为了安全地允许多个线程在知道这些对象不会更改的情况下使用这些对象,您应该使它们不可变,或者使用一种并发集合类型,您可以在以下链接中阅读:https://docs.microsoft.com/en-us/dotnet/api/system.collections.concurrent

收藏方面的良好做法

假设您需要创建一个方法来处理集合。为了获得最大的灵活性,您可以将输入参数声明为IEnumerable<T>并将方法设置为泛型,如下代码所示:

void ProcessCollection<T>(IEnumerable<T> collection)
{
  // process the items in the collection,
  // perhaps using a foreach statement
} 

我可以将一个数组、一个列表、一个队列、一个堆栈或任何实现IEnumerable<T>的东西传递到这个方法中,它将处理这些项。但是,将任何集合传递给此方法的灵活性是以性能为代价的。

IEnumerable<T>的性能问题之一也是其优点之一:延迟执行,也称为延迟加载。实现此接口的类型不必实现延迟执行,但许多类型都必须实现。

但是IEnumerable<T>最糟糕的性能问题是迭代必须在堆上分配一个对象。要避免这种内存分配,应使用具体类型定义方法,如以下代码中突出显示的:

void ProcessCollection<T>(**List<T>** collection)
{
  // process the items in the collection,
  // perhaps using a foreach statement
} 

这将使用返回structList<T>.Enumerator GetEnumerator()方法,而不是返回引用类型的IEnumerator<T> GetEnumerator()方法。您的代码将比快两到三倍,并且需要更少的内存。与所有与性能相关的建议一样,您应该通过在产品环境中对实际代码运行性能测试来确认这一好处。您将在第 12 章**中学习如何使用多任务提高性能和可伸缩性。

使用跨距、索引和范围

微软的.NETCore2.1 目标之一是提高性能和资源利用率。启用此功能的关键.NET 功能是Span<T>类型。

有效地使用内存使用跨度

在操作数组时,通常会创建现有数组子集的新副本,以便只处理子集。这是无效的,因为必须在内存中创建重复的对象。

如果需要使用数组的子集,请使用跨度,因为它就像一个进入原始数组的窗口。这在内存使用方面更有效,并提高了性能。跨度仅适用于数组,而不适用于集合,因为内存必须是连续的。

在更详细地研究跨度之前,我们需要了解一些相关对象:索引和范围。

使用索引类型标识位置

C# 8.0 引入了两个特性,用于识别数组中的项目索引和使用两个索引的项目范围。

您在上一主题中了解到,可以通过将整数传递到索引器来访问列表中的对象,如下代码所示:

int index = 3;
Person p = people[index]; // fourth person in array
char letter = name[index]; // fourth letter in name 

Index值类型是一种更正式的位置识别方式,支持从末尾开始计数,如下代码所示:

// two ways to define the same index, 3 in from the start 
Index i1 = new(value: 3); // counts from the start 
Index i2 = 3; // using implicit int conversion operator
// two ways to define the same index, 5 in from the end
Index i3 = new(value: 5, fromEnd: true); 
Index i4 = ^5; // using the caret operator 

使用范围类型标识范围

Range值类型使用Index值表示其范围的开始和结束,使用其构造函数、C# 语法或其静态方法,如下代码所示:

Range r1 = new(start: new Index(3), end: new Index(7));
Range r2 = new(start: 3, end: 7); // using implicit int conversion
Range r3 = 3..7; // using C# 8.0 or later syntax
Range r4 = Range.StartAt(3); // from index 3 to last index
Range r5 = 3..; // from index 3 to last index
Range r6 = Range.EndAt(3); // from index 0 to index 3
Range r7 = ..3; // from index 0 to index 3 

扩展方法已添加到string值(内部使用char数组)、int数组和跨度,以使范围更易于使用。这些扩展方法接受一个范围作为参数,并返回一个Span<T>。这使得它们的内存效率非常高。

使用索引、范围和跨度

让我们探索使用索引和范围返回跨度:

  1. 使用您首选的代码编辑器将名为WorkingWithRanges的新控制台应用添加到Chapter08解决方案/工作区。

  2. 在 Visual Studio 代码中,选择WorkingWithRanges作为活动的 OmniSharp 项目。

  3. Program.cs中,使用string类型的Substring方法键入语句进行比较,使用范围提取某人姓名的部分,如下代码所示:

    string name = "Samantha Jones";
    // Using Substring
    int lengthOfFirst = name.IndexOf(' ');
    int lengthOfLast = name.Length - lengthOfFirst - 1;
    string firstName = name.Substring(
      startIndex: 0,
      length: lengthOfFirst);
    string lastName = name.Substring(
      startIndex: name.Length - lengthOfLast,
      length: lengthOfLast);
    WriteLine($"First name: {firstName}, Last name: {lastName}");
    // Using spans
    ReadOnlySpan<char> nameAsSpan = name.AsSpan();
    ReadOnlySpan<char> firstNameSpan = nameAsSpan[0..lengthOfFirst]; 
    ReadOnlySpan<char> lastNameSpan = nameAsSpan[^lengthOfLast..^0];
    WriteLine("First name: {0}, Last name: {1}", 
      arg0: firstNameSpan.ToString(),
      arg1: lastNameSpan.ToString()); 
  4. 运行代码并查看结果,如输出中的所示:

    First name: Samantha, Last name: Jones 
    First name: Samantha, Last name: Jones 

使用网络资源

有时你需要使用网络资源。下表显示了.NET 中用于处理网络资源的最常见类型:

| 名称空间 | 示例类型 | 描述 | | `System.Net` | `Dns`、`Uri`、`Cookie`、`WebClient`、`IPAddress` | 这些用于使用 DNS 服务器、URI、IP 地址等。 | | `System.Net` | `FtpStatusCode`、`FtpWebRequest`、`FtpWebResponse` | 这些用于使用 FTP 服务器。 | | `System.Net` | `HttpStatusCode`、`HttpWebRequest`、`HttpWebResponse` | 这些用于使用 HTTP 服务器;即网站和服务。`System.Net.Http`中的类型更易于使用。 | | `System.Net.Http` | `HttpClient`、`HttpMethod`、`HttpRequestMessage`、`HttpResponseMessage` | 这些用于使用 HTTP 服务器;即网站和服务。您将在*第 16 章*、*构建和消费 Web 服务*中学习如何使用这些服务。 | | `System.Net.Mail` | `Attachment`、`MailAddress`、`MailMessage`、`SmtpClient` | 这些用于使用 SMTP 服务器;也就是说,发送电子邮件消息。 | | `System.Net .NetworkInformation` | `IPStatus`、`NetworkChange`、`Ping`、`TcpStatistics` | 这些用于处理低级网络协议。 |

使用 URI、DNS 和 IP 地址

让我们来探索一些使用网络资源的常见类型:

  1. 使用您首选的代码编辑器将名为WorkingWithNetworkResources的新控制台应用添加到Chapter08解决方案/工作区。

  2. 在 VisualStudio 代码中,选择WorkingWithNetworkResources作为活动的 OmniSharp 项目。

  3. Program.cs顶部,导入名称空间,用于处理网络,如下代码所示:

    using System.Net; // IPHostEntry, Dns, IPAddress 
  4. Type statements to prompt the user to enter a website address, and then use the Uri type to break it down into its parts, including the scheme (HTTP, FTP, and so on), port number, and host, as shown in the following code:

    Write("Enter a valid web address: "); 
    string? url = ReadLine();
    if (string.IsNullOrWhiteSpace(url))
    {
      url = "https://stackoverflow.com/search?q=securestring";
    }
    Uri uri = new(url);
    WriteLine($"URL: {url}"); 
    WriteLine($"Scheme: {uri.Scheme}"); 
    WriteLine($"Port: {uri.Port}"); 
    WriteLine($"Host: {uri.Host}"); 
    WriteLine($"Path: {uri.AbsolutePath}"); 
    WriteLine($"Query: {uri.Query}"); 

    为方便起见,该代码还允许用户按 ENTER 键使用示例 URL。

  5. 运行代码,输入有效网址或按 enter 键,查看结果,如以下输出所示:

    Enter a valid web address:
    URL: https://stackoverflow.com/search?q=securestring 
    Scheme: https
    Port: 443
    Host: stackoverflow.com 
    Path: /search
    Query: ?q=securestring 
  6. 添加语句获取输入的网站的 IP 地址,如下代码所示:

    IPHostEntry entry = Dns.GetHostEntry(uri.Host); 
    WriteLine($"{entry.HostName} has the following IP addresses:"); 
    foreach (IPAddress address in entry.AddressList)
    {
      WriteLine($"  {address} ({address.AddressFamily})");
    } 
  7. 运行代码,输入有效的网址或按 enter 键,然后查看结果,如以下输出所示:

    stackoverflow.com has the following IP addresses: 
      151.101.193.69 (InterNetwork)
      151.101.129.69 (InterNetwork)
      151.101.1.69 (InterNetwork)
      151.101.65.69 (InterNetwork) 

ping 服务器

现在您将向 ping web 服务器添加代码以检查的运行状况:

  1. 导入名称空间以获取有关网络的更多信息,如以下代码所示:

    using System.Net.NetworkInformation; // Ping, PingReply, IPStatus 
  2. 在输入的网站上添加 ping 语句,如下代码所示:

    try
    {
      Ping ping = new();
      WriteLine("Pinging server. Please wait...");
      PingReply reply = ping.Send(uri.Host);
      WriteLine($"{uri.Host} was pinged and replied: {reply.Status}.");
      if (reply.Status == IPStatus.Success)
      {
        WriteLine("Reply from {0} took {1:N0}ms", 
          arg0: reply.Address,
          arg1: reply.RoundtripTime);
      }
    }
    catch (Exception ex)
    {
      WriteLine($"{ex.GetType().ToString()} says {ex.Message}");
    } 
  3. 运行代码,按 ENTER 键,查看结果,如 macOS 上的输出所示:

    Pinging server. Please wait...
    stackoverflow.com was pinged and replied: Success.
    Reply from 151.101.193.69 took 18ms took 136ms 
  4. 再次运行代码,但这次输入http://google.com ,如以下输出所示:

    Enter a valid web address: http://google.com
    URL: http://google.com
    Scheme: http
    Port: 80
    Host: google.com
    Path: /
    Query: 
    google.com has the following IP addresses:
      2a00:1450:4009:807::200e (InterNetworkV6)
      216.58.204.238 (InterNetwork)
    Pinging server. Please wait...
    google.com was pinged and replied: Success.
    Reply from 2a00:1450:4009:807::200e took 24ms 

使用反射和属性

反射是一种编程特性,允许代码理解和操作自身。组件最多由四个零件组成:

  • 程序集元数据和清单:名称、程序集、文件版本、引用的程序集等。
  • 类型元数据:关于类型及其成员等的信息。
  • IL 代码:方法的实现、属性、构造函数等。
  • 嵌入式资源(可选):图像、字符串、JavaScript、等。

元数据包含有关代码的信息项。元数据是从代码(例如,关于类型和成员的信息)自动生成的,或者使用属性应用于代码。

属性可以应用于多个级别:程序集、类型及其成员,如下代码所示:

// an assembly-level attribute
[assembly: AssemblyTitle("Working with Reflection")]
// a type-level attribute
[Serializable] 
public class Person
{
  // a member-level attribute 
  [Obsolete("Deprecated: use Run instead.")] 
  public void Walk()
  {
... 

基于属性的编程在 ASP.NET Core 等应用模型中被大量使用,以支持路由、安全和缓存等功能。

程序集的版本控制

NET 中的版本号是三个数字的组合,带有两个可选的添加项。如果遵循语义版本控制规则,则三个数字表示以下内容:

  • 主要:突破性变化。
  • 次要:非破坏性更改,包括新功能,通常是错误修复。
  • 补丁:非破坏性 bug 修复。

良好实践:在更新项目中已经使用的 NuGet 软件包时,为了安全起见,您应该指定一个可选标志,以确保您只升级到最高的次要版本以避免破坏更改,或者如果您非常谨慎并且只想得到 bug 修复,则升级到最高的补丁,如以下命令所示:Update-Package Newtonsoft.Json -ToHighestMinorUpdate-Package Newtonsoft.Json -ToHighestPatch

可选地,版本可以包括以下内容:

  • 预发布:不支持预览发布。
  • 建造编号:夜间建造。

良好实践:遵循语义版本控制规则,如下链接所述:http://semver.org

读取程序集元数据

让我们探索如何使用属性:

  1. 使用您首选的代码编辑器将名为WorkingWithReflection的新控制台应用添加到Chapter08解决方案/工作区。

  2. 在 Visual Studio 代码中,选择WorkingWithReflection作为活动的 OmniSharp 项目。

  3. Program.cs顶部,导入名称空间进行反射,如下代码所示:

    using System.Reflection; // Assembly 
  4. 添加语句获取控制台应用的程序集,输出其名称和位置,获取所有程序集级属性并输出其类型,如下代码所示:

    WriteLine("Assembly metadata:");
    Assembly? assembly = Assembly.GetEntryAssembly();
    if (assembly is null)
    {
      WriteLine("Failed to get entry assembly.");
      return;
    }
    WriteLine($"  Full name: {assembly.FullName}"); 
    WriteLine($"  Location: {assembly.Location}");
    IEnumerable<Attribute> attributes = assembly.GetCustomAttributes(); 
    WriteLine($"  Assembly-level attributes:");
    foreach (Attribute a in attributes)
    {
      WriteLine($"   {a.GetType()}");
    } 
  5. Run the code and view the result, as shown in the following output:

    Assembly metadata:
      Full name: WorkingWithReflection, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
      Location: /Users/markjprice/Code/Chapter08/WorkingWithReflection/bin/Debug/net6.0/WorkingWithReflection.dll
      Assembly-level attributes:
        System.Runtime.CompilerServices.CompilationRelaxationsAttribute
        System.Runtime.CompilerServices.RuntimeCompatibilityAttribute
        System.Diagnostics.DebuggableAttribute
        System.Runtime.Versioning.TargetFrameworkAttribute
        System.Reflection.AssemblyCompanyAttribute
        System.Reflection.AssemblyConfigurationAttribute
        System.Reflection.AssemblyFileVersionAttribute
        System.Reflection.AssemblyInformationalVersionAttribute
        System.Reflection.AssemblyProductAttribute
        System.Reflection.AssemblyTitleAttribute 

    请注意,由于程序集的全名必须唯一标识该程序集,因此它是以下各项的组合:

    • 名称,例如WorkingWithReflection
    • 版本,例如1.0.0.0
    • 培养,例如neutral
    • 公钥令牌,虽然可以是null

    既然我们知道了一些装饰程序集的属性,我们就可以具体地要求它们了。

  6. 添加语句获取AssemblyInformationalVersionAttributeAssemblyCompanyAttribute类,然后输出它们的值,如下代码所示:

    AssemblyInformationalVersionAttribute? version = assembly
      .GetCustomAttribute<AssemblyInformationalVersionAttribute>(); 
    WriteLine($"  Version: {version?.InformationalVersion}");
    AssemblyCompanyAttribute? company = assembly
      .GetCustomAttribute<AssemblyCompanyAttribute>();
    WriteLine($"  Company: {company?.Company}"); 
  7. Run the code and view the result, as shown in the following output:

     Version: 1.0.0
      Company: WorkingWithReflection 

    嗯,除非设置版本,否则默认为 1.0.0,除非设置公司,否则默认为程序集的名称。让我们明确地设置这个信息。传统的.NET Framework 设置这些值的方法是在 C# 源代码文件中添加属性,如下代码所示:

    [assembly: AssemblyCompany("Packt Publishing")] 
    [assembly: AssemblyInformationalVersion("1.3.0")] 

    NET 使用的 Roslyn 编译器会自动设置这些属性,因此我们不能使用旧方法。相反,它们必须在项目文件中设置。

  8. 编辑WorkingWithReflection.csproj项目文件,为版本和公司添加元素,如下标记中突出显示:

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
     **<Version>****6.3.12****</Version>**
     **<Company>Packt Publishing</Company>**
      </PropertyGroup>
    </Project> 
  9. 运行代码并查看结果,如以下输出所示:

     Version: 6.3.12
      Company: Packt Publishing 

创建自定义属性

您可以通过继承Attribute类来定义自己的属性:

  1. 将名为CoderAttribute.cs的类文件添加到项目中。

  2. 定义一个属性类,该属性类可以用两个属性修饰类或方法,以存储编码者的名称和他们上次修改某些代码的日期,如以下代码所示:

    namespace Packt.Shared;
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
      AllowMultiple = true)]
    public class CoderAttribute : Attribute
    {
      public string Coder { get; set; }
      public DateTime LastModified { get; set; }
      public CoderAttribute(string coder, string lastModified)
      {
        Coder = coder;
        LastModified = DateTime.Parse(lastModified);
      }
    } 
  3. Program.cs中,导入一些名称空间,如下代码所示:

    using System.Runtime.CompilerServices; // CompilerGeneratedAttribute
    using Packt.Shared; // CoderAttribute 
  4. Program.cs的底部,添加一个带有方法的类,并用两个编码者的数据将Coder属性装饰该方法,如下代码所示:

    class Animal
    {
      [Coder("Mark Price", "22 August 2021")]
      [Coder("Johnni Rasmussen", "13 September 2021")] 
      public void Speak()
      {
        WriteLine("Woof...");
      }
    } 
  5. Program.cs中,在Animal类上方,添加代码获取类型,枚举其成员,读取这些成员上的任何Coder属性,并输出信息,如下代码所示:

    WriteLine(); 
    WriteLine($"* Types:");
    Type[] types = assembly.GetTypes();
    foreach (Type type in types)
    {
      WriteLine();
      WriteLine($"Type: {type.FullName}"); 
      MemberInfo[] members = type.GetMembers();
      foreach (MemberInfo member in members)
      {
        WriteLine("{0}: {1} ({2})",
          arg0: member.MemberType,
          arg1: member.Name,
          arg2: member.DeclaringType?.Name);
        IOrderedEnumerable<CoderAttribute> coders = 
          member.GetCustomAttributes<CoderAttribute>()
          .OrderByDescending(c => c.LastModified);
        foreach (CoderAttribute coder in coders)
        {
          WriteLine("-> Modified by {0} on {1}",
            coder.Coder, coder.LastModified.ToShortDateString());
        }
      }
    } 
  6. 运行代码并查看结果,如以下部分输出所示:

    * Types:
    ...
    Type: Animal
    Method: Speak (Animal)
    -> Modified by Johnni Rasmussen on 13/09/2021
    -> Modified by Mark Price on 22/08/2021
    Method: GetType (Object)
    Method: ToString (Object)
    Method: Equals (Object)
    Method: GetHashCode (Object)
    Constructor: .ctor (Program)
    ...
    Type: <Program>$+<>c
    Method: GetType (Object)
    Method: ToString (Object)
    Method: Equals (Object)
    Method: GetHashCode (Object)
    Constructor: .ctor (<>c)
    Field: <>9 (<>c)
    Field: <>9__0_0 (<>c) 

什么是<Program>$+<>c类型?

编译器生成的显示类<>表示编译器生成,c表示显示类。它们是未记录的编译器的实现细节,可能随时更改。您可以忽略它们,作为一个可选的挑战,将语句添加到控制台应用中,通过跳过用CompilerGeneratedAttribute修饰的类型来过滤编译器生成的类型。

更多地进行反思

这只是通过反思可以实现的体验。我们只使用反射从代码中读取元数据。反射也可以执行以下操作:

处理图像

ImageSharp 是第三方跨平台 2D 图形库。当.NET Core 1.0 处于开发阶段时,社区对缺少用于处理 2D 图像的System.Drawing名称空间表示了负面反馈。

ImageSharp项目开始填补现代.NET 应用的这一空白。

System.Drawing的官方文档中,微软表示:“由于 Windows 或 ASP.NET 服务不支持System.Drawing名称空间,因此不建议进行新的开发,System.Drawing名称空间也不跨平台。建议使用 ImageSharp 和 SkiaSharp 作为替代方案。”

让我们看看 ImageSharp 可以实现什么:

  1. 使用您首选的代码编辑器将名为WorkingWithImages的新控制台应用添加到Chapter08解决方案/工作区。

  2. 在 Visual Studio 代码中,选择WorkingWithImages作为活动的 OmniSharp 项目。

  3. 创建一个images文件夹,并从以下链接下载九幅图片:https://github.com/markjprice/cs10dotnet6/tree/mastimg/Categories

  4. SixLabors.ImageSharp添加一个包引用,如下标记所示:

    <ItemGroup>
      <PackageReference Include="SixLabors.ImageSharp" Version="1.0.3" />
    </ItemGroup> 
  5. 建设WorkingWithImages项目。

  6. Program.cs顶部,导入一些用于处理图像的名称空间,如下代码所示:

    using SixLabors.ImageSharp;
    using SixLabors.ImageSharp.Processing; 
  7. Program.cs中,输入语句将 images 文件夹中的所有文件转换成十分之一大小的灰度缩略图,如下代码所示:

    string imagesFolder = Path.Combine(
      Environment.CurrentDirectory, "images");
    IEnumerable<string> images =
      Directory.EnumerateFiles(imagesFolder);
    foreach (string imagePath in images)
    {
      string thumbnailPath = Path.Combine(
        Environment.CurrentDirectory, "images",   
        Path.GetFileNameWithoutExtension(imagePath)
        + "-thumbnail" + Path.GetExtension(imagePath));
      using (Image image = Image.Load(imagePath))
      {
        image.Mutate(x => x.Resize(image.Width / 10, image.Height / 10));   
        image.Mutate(x => x.Grayscale());
        image.Save(thumbnailPath);
      }
    }
    WriteLine("Image processing complete. View the images folder."); 
  8. 运行代码。

  9. In the filesystem, open the images folder and note the much-smaller-in-bytes grayscale thumbnails, as shown in Figure 8.1:

    ![A picture containing application Description automatically generated](img/B17442_08_01.png)

    图 8.1:处理后的图像

ImageSharp 还有 NuGet 软件包,用于以编程方式绘制图像和在 web 上处理图像,如下表所示:

  • SixLabors.ImageSharp.Drawing
  • SixLabors.ImageSharp.Web

国际化您的代码

国际化是使您的代码在世界各地正确运行的过程。它分为两部分:全球化本土化

全球化是关于编写代码以适应多种语言和区域组合。一种语言和一个地区的结合称为文化。您的代码必须同时了解语言和地区,因为例如,魁北克和巴黎的日期和货币格式不同,尽管它们都使用法语。

所有培养组合都有国际标准化组织ISO代码)。例如,在代码da-DK中,da表示丹麦语,DK表示丹麦地区,在代码fr-CA中,fr表示法语,CA表示加拿大地区。

ISO 不是首字母缩略词。ISO 是指希腊语单词isos(意思是相等)。

本地化是指定制用户界面以支持一种语言,例如,将按钮的标签更改为 Close(en)或 Fermer(fr)。由于本地化更多的是关于语言,它并不总是需要了解该地区,尽管具有讽刺意味的是,标准化(en-US)和标准化(en-GB)却提出了不同的建议。

检测和更改当前文化

国际化是一个巨大的话题,已经有几千页的书被写了出来。在本节中,您将简要介绍使用System.Globalization名称空间中的CultureInfo类型的基础知识。

让我们编写一些代码:

  1. 使用您首选的代码编辑器将名为Internationalization的新控制台应用添加到Chapter08解决方案/工作区。

  2. 在 Visual Studio 代码中,选择Internationalization作为活动的 OmniSharp 项目。

  3. Program.cs顶部,导入使用全球化类型的名称空间,如下代码所示:

    using System.Globalization; // CultureInfo 
  4. Add statements to get the current globalization and localization cultures and output some information about them, and then prompt the user to enter a new culture code and show how that affects the formatting of common values such as dates and currency, as shown in the following code:

    CultureInfo globalization = CultureInfo.CurrentCulture; 
    CultureInfo localization = CultureInfo.CurrentUICulture;
    WriteLine("The current globalization culture is {0}: {1}",
      globalization.Name, globalization.DisplayName);
    WriteLine("The current localization culture is {0}: {1}",
      localization.Name, localization.DisplayName);
    WriteLine();
    WriteLine("en-US: English (United States)"); 
    WriteLine("da-DK: Danish (Denmark)"); 
    WriteLine("fr-CA: French (Canada)"); 
    Write("Enter an ISO culture code: ");  
    string? newCulture = ReadLine();
    if (!string.IsNullOrEmpty(newCulture))
    {
      CultureInfo ci = new(newCulture); 
      // change the current cultures
      CultureInfo.CurrentCulture = ci;
      CultureInfo.CurrentUICulture = ci;
    }
    WriteLine();
    Write("Enter your name: "); 
    string? name = ReadLine();
    Write("Enter your date of birth: "); 
    string? dob = ReadLine();
    Write("Enter your salary: "); 
    string? salary = ReadLine();
    DateTime date = DateTime.Parse(dob);
    int minutes = (int)DateTime.Today.Subtract(date).TotalMinutes; 
    decimal earns = decimal.Parse(salary);
    WriteLine(
      "{0} was born on a {1:dddd}, is {2:N0} minutes old, and earns {3:C}",
      name, date, minutes, earns); 

    运行应用时,它会自动将其线程设置为使用操作系统的区域性。我正在英国伦敦运行我的代码,所以线程设置为英语(英国)。

    该代码提示用户输入替代 ISO 代码。这允许应用在运行时替换默认区域性。

    然后,应用使用标准格式代码输出使用格式代码dddd的星期几;使用格式代码N0的千位分隔符的分钟数;以及带有货币符号的工资。它们会根据线程的区域性自动调整。

  5. Run the code and enter en-GB for the ISO code and then enter some sample data including a date in a format valid for British English, as shown in the following output:

    Enter an ISO culture code: en-GB 
    Enter your name: Alice
    Enter your date of birth: 30/3/1967 
    Enter your salary: 23500
    Alice was born on a Thursday, is 25,469,280 minutes old, and earns
    £23,500.00 

    如果输入的是en-US而不是en-GB,则必须使用月/日/年输入日期。

  6. 重新运行代码并尝试不同的文化,例如丹麦的丹麦文化,如以下输出所示:

    Enter an ISO culture code: da-DK 
    Enter your name: Mikkel
    Enter your date of birth: 12/3/1980 
    Enter your salary: 340000
    Mikkel was born on a onsdag, is 18.656.640 minutes old, and earns 340.000,00 kr. 

在本例中,只有日期和工资全球化为丹麦语。正文的其余部分硬编码为英语。本书目前不包括如何将文本从一种语言翻译成另一种语言。如果你想让我在下一版中包括这一点,请让我知道。

好实践:请考虑你的应用是否需要国际化,并在开始编码之前进行计划。写下用户界面中需要本地化的所有文本。考虑所有需要全球化的数据(日期格式、数字格式和排序文本行为)。

实践与探索

通过回答一些问题来测试您的知识和理解,进行一些实际操作,并对本章中的主题进行更深入的研究。

练习 8.1–测试您的知识

使用 web 回答以下问题:

  1. string变量中可以存储的最大字符数是多少?
  2. 何时以及为什么要使用SecureString类型?
  3. 什么时候使用StringBuilder类合适?
  4. 你应该什么时候使用LinkedList<T>课程?
  5. 什么时候应该使用SortedDictionary<T>类而不是SortedList<T>类?
  6. 威尔士的 ISO 文化代码是什么?
  7. 本地化、全球化和国际化之间有什么区别?
  8. 在正则表达式中,$是什么意思?
  9. 在正则表达式中,如何表示数字?
  10. 为什么使用电子邮件地址的官方标准来创建正则表达式来验证用户的电子邮件地址?

练习 8.2–练习正则表达式

Chapter08解决方案/工作区中,创建一个名为Exercise02的控制台应用,提示用户输入正则表达式,然后提示用户输入一些输入并比较两者是否匹配,直到用户按下Esc,如下输出所示:

The default regular expression checks for at least one digit.
Enter a regular expression (or press ENTER to use the default): ^[a-z]+$ 
Enter some input: apples
apples matches ^[a-z]+$? True
Press ESC to end or any key to try again.
Enter a regular expression (or press ENTER to use the default): ^[a-z]+$ 
Enter some input: abc123xyz
abc123xyz matches ^[a-z]+$? False
Press ESC to end or any key to try again. 

练习 8.3–练习写作扩展方法

Chapter08解决方案/工作区中,创建一个名为Exercise03的类库,该类库定义了扩展数字类型(如BigIntegerint的扩展方法,该扩展方法使用名为ToWords的方法返回一个描述数字的string;例如,18,000,000将是 1800 万,18,456,002,032,011,000,007将是 185 亿、456 亿、2 万亿、320 亿、1100 万和 700 万。

您可以通过以下链接阅读有关大数字名称的更多信息:https://en.wikipedia.org/wiki/Names_of_large_numbers

练习 8.4–探索主题

使用下页上的链接了解有关本章所涵盖主题的更多详细信息:

[https://github.com/markjprice/cs10dotnet6/blob/main/book-links.md# chapter-8---使用普通网络类型](https://github.com/markjprice/cs10dotnet6/blob/main/book-links.md# chapter-8---working-with-common-net-types)

总结

在本章中,您探讨了用于存储和操作数字、日期和时间、文本(包括正则表达式)的类型的一些选择,以及用于存储多个项的集合;使用索引、范围和跨度;使用了一些网络资源;体现在代码和属性上;使用微软推荐的第三方库操作图像;并学习了如何国际化您的代码。

在下一章中,我们将管理文件和流,编码和解码文本,并执行序列化。