第6章 框架基础
6.1 字符串与文本处理
6.1.1 字符
C#中 char
代表一个 Unicode 字符。char
是 System.Char
的别名,System.Char
定义了一系列静态方法对字符进行处理:
C7.0 核心技术指南 第7版.pdf - p267 - C7.0 核心技术指南 第 7 版-P267-20240205145109
C7.0 核心技术指南 第7版.pdf - p268 - C7.0 核心技术指南 第 7 版-P268-20240205145204
6.1.2 字符串
6.1.2.4 字符串内搜索
可以使用 StartsWith
、 EndsWith
、 Contains
、 IndexOf
、 LastIndexOf
、 IndexOfAny
、 LastIndexOfAny
进行搜索。搜索时可以使用 StringComparison
限定文化相关规则。
Console.WriteLine ("quick brown fox".Contains ("brown")); // True
Console.WriteLine ("quick brown fox".EndsWith ("fox")); // True
Console.WriteLine ("abcde".IndexOf ("cd")); // 2
Console.WriteLine ("abcde".IndexOf ("xx")); // -1
Console.WriteLine ("abcde".IndexOf ("CD", StringComparison.CurrentCultureIgnoreCase)); // 2
Console.WriteLine ("ab,cd ef".IndexOfAny (new char[] {' ', ','} )); // 2
Console.WriteLine ("pas5w0rd".IndexOfAny ("0123456789".ToCharArray() )); // 3
6.1.2.5 字符串处理
PadLeft
和 PadRight
会用特定的字符(如果未指定则使用空格)将字符串 填充为指定的长度 :
Console.WriteLine ("12345".PadLeft (9, '*')); // ****12345
Console.WriteLine ("12345".PadLeft (9)); // 12345
如果输入字符串长度大于填充长度,则返回 不发生变化的原始 字符串。
TrimStart
和 TrimEnd
会从字符串的开始或结尾删除 指定的 字符,Trim
则是从 开始 和 结尾 执行删除操作。这些方法默认会删除 空白 字符(包括空格、制表符、换行和这些字符的 Unicode 变体):
Console.WriteLine (" abc \t\r\n ".Trim().Length); // 3
ToUpper
和 ToLower
默认情况下会受用户当前语言设置的影响; ToUpperInvariant
和 ToLowerInvariant
则总是应用英语字母表规则。
6.1.2.6 字符串的分割(Split)与连接(Join)
默认情况下,Split
使用 空白 字符作为分隔符。Split
还可以接受一个 StringSplitOptions
枚举值用以** 删除 ** 空项 。这在一行文本中有多种单词分隔符时很有用。
Notice
默认参数下,面对连续的空白字符,
Split
分隔时只会移除第一个空白,剩余空白仍会保留。此时可以传入上述 StringSplitOptions
枚举参数。
静态方法 Join
则执行和 Split
相反的操作。它需要一个分隔符和字符串的数组:
string[] words = "The quick brown fox".Split();
string together = string.Join (" ", words);
6.1.2.7 String.Format
与组合格式字符串
调用 String.Format
时,需要提供一个组合格式字符串,后面紧跟每一个嵌入的变量。
花括号中的格式项(format item)对应参数的位置,后面可以跟随:
- 逗号与 最小宽度(minimum width)
- 冒号与 格式字符串(format string)
最小宽度用于对齐各个列。如果其值为负数,则为 左 对齐,否则为 右 对齐,例如:
composite = "Name={0,-20} Credit Limit={1,15:C}";
Console.WriteLine (string.Format (composite, "Mary", 500));
Console.WriteLine (string.Format (composite, "Elizabeth", 20000));
输出:
Name=Mary Credit Limit= ¥500.00
Name=Elizabeth Credit Limit= ¥20,000.00
等价于:
s = "Name=" + "Mary".PadRight (20) + " Credit Limit=" + 500.ToString ("C").PadLeft (15);
Tips
当我们不需要最小宽度,或不需要格式字符串,都是可以省略的。如下代码分别省略了格式字符串和最小宽度:
var composite = "Name={0,-20} Credit Limit={1:C}"; Console.WriteLine(string.Format(composite, "Mary", 500)); Console.WriteLine(string.Format(composite, "Elizabeth", 20000));
6.1.3 字符串的比较
6.1.3.1 序列比较与文化相关的字符串比较
-
序列比较(ordinal)
按照 Unicode 字符数值 进行比较
-
文化相关比较(culture-sensitive)
-
不变文化(invariant culture)
依托于美国文化,所有计算机都相同
-
当前文化
基于计算机控制面板的设定
-
假设有如下字符串:
Atom、atom、Zamia
使用不变文化其排序结果是:
atom、Atom、Zamia
使用序列比较排序结果是:
Atom、Zamia、atom
6.1.3.2 字符串的相等比较
字符串的 ==
运算符执行的是 区分 大小写的** 序列 比较**,与 不带 参数的 String.Equals
相同。
string
的 Equals
方法有静态版本, 且 推荐静态版本,因为它在字符串为 null 时仍然有效:
public bool Equals(string value, StringComparison comparisonType);
public static bool Equals(string a, string b, StringComparison comparisonType);
StringComparison
是枚举类型,其定义如下:
public enum stringComparison
{
CurrentCulture,
CurrentCultureIgnoreCase,
InvariantCulture,
InvariantCultureIgnoreCase,
Ordinal,
OrdinalIgnoreCase
}
6.1.3.3 字符串的顺序比较
字符串顺序比较(CompareTo
方法)执行** 文化 相关**的比较。其他类型的比较需调用静态方法 Compare
和 CompareOrdinal
:
public static int Compare(string strA, string strB, StringComparison comparisonType);
public static int Compare(string strA, string strB, bool ignoreCase, CultureInfo culture);
public static int Compare(string strA, string strB, bool ignoreCase)
public static int CompareOrdinal(string strA, string strB)
后两个方法是前面两个方法的快捷调用形式。
6.1.4 StringBuilder
StringBuilder.AppendFormat
与 String.Format
相似,接受一个组合格式字符串。
此外,StringBuilder
还有可写的** 索引器 **,用于获得/设置每一个字符。
StringBuilder.Length
属性可写,可以将其设置为 0 以清除内容。
C7.0 核心技术指南 第7版.pdf - p276 - C7.0 核心技术指南 第 7 版-P276-20240205182444
6.1.5 文本编码和 Unicode
6.1.5.1 获取一个 Encoding 对象
我们可以通过 Encoding.GetEncoding
方法获取编码:
Encoding utf8 = Encoding.GetEncoding ("utf-8");
Encoding chinese = Encoding.GetEncoding ("GB18030");
也可以通过 Encoding
的 静态属性 获得相应编码:
C7.0 核心技术指南 第7版.pdf - p278 - C7.0 核心技术指南 第 7 版-P278-20240205184839
还可以通过 实例化 获得相应编码。构造器有各种选项:
- 解码时,如果遇到无效字节,是否抛出异常。(默认为 false)
- 对 UTF-16/UTF-32 编码时,是大端存储还是小端存储。(Windows 默认为小端)
- 是否使用字节顺序标记。(BOM)
6.1.5.2 文件与流 I/O 编码
C7.0 核心技术指南 第7版.pdf - p278 - C7.0 核心技术指南 第 7 版-P278-20240205185935
6.1.5.4 UTF-16 和替代组
对于超过 16 位的字符,UTF-16 需要 两 个 char
来表示,这会有两个问题:
- 字符串的
Length
属性值可能大于它的实际字符数。 - 一个
char
有时无法完整表示一个 Unicode 字符。
如果需要支持双字字符,那么可以用 char
类型下的静态方法将 32 位代码点转换为一个包含两个字符的字符串,同样也可以进行反向转换:
string ConvertFromUtf32 (int utf32);
int ConvertToUtf32 (char hightSurrogate, char lowSurrogate);
双字节字符每个字的范围在 0xD800~0xDFFF ,我们可以通过 char
的静态方法进行判断:
bool IsSurrogate (char c);
bool IsHighSurrogate (char c);
bool IsLowSurrogate (char c);
bool IsSurrogatePair (char hightSurrogate, char lowSurrogate);
6.2 日期和时间
6.2.1 TimeSpan
TimeSpan
最小单位为 100 纳秒 ,最大值为 一千万天 ,可以为正数也可以为负数。
创建 TimeSpan 的方法有三种:
- 使用构造器
Console.WriteLine (new TimeSpan (2, 30, 0)); // 02:30:00
- 调用工厂方法
TimeSpan.FromXXX
Console.WriteLine (TimeSpan.FromHours (2.5)); // 02:30:00
Console.WriteLine (TimeSpan.FromHours (-2.5)); // -02:30:00
- 两个
DateTime
相减
Console.WriteLine (DateTime.MaxValue - DateTime.MinValue);
TimeSpan
重载了 <
、>
、+
和 -
运算符:
(TimeSpan.FromHours(2) + TimeSpan.FromMinutes(30)).Dump ("2.5 hours");
TimeSpan
的默认值是 TimeSpan.Zero
6.2.2 DateTime
和 DateTimeOffset
最小单位均为 100 纳秒 ,取值范围为 ** 0001 年到 ** 9999 年。
6.2.2.1 使用 DateTime
还是 DateTimeOffset
DateTime
和 DateTimeOffset
在比较时有如下区别:
-
DateTime
比较时会忽略 Kind
状态标记 。当两个值的 年、月、日、时、分等 相同就认为它们是相等的。 - 如果两个值指的是 相同的时间点 ,那么
DateTimeOffset
则认为它们是相等的。
因此,DateTime
认为如下时间是 不相等的 ,DateTimeOffset
则认为是 相同的 :
July 01 2017 09:00:00 +00:00 (GMT)
July 01 2017 03:00:00 -06:00 (local time, Central America)
DateTimeOffset
适用于等值比较,但用户体验较差(需要在格式化前显式转换为本地时间);DateTime
适用于本地计算机的时间。
6.2.2.2 创建一个 DateTime
DateTime(..., DateTimeKind kind)
DateTime
构造器允许指定一个 DateTimeKind
枚举,其元素如下:
-
Unspecified
未指定时区,默认为该值。
-
Local
-
Utc
DateTime(..., Calendar calendar)
DateTime
默认使用 公 历,其构造器可以接收 Calendar
对象,可以使用 System.Globalization
下的 Calendar
子类来指定一个日期:
// 希伯来历
DateTime d = new DateTime (5767, 1, 1, new System.Globalization.HebrewCalendar());
Console.WriteLine (d); // 12/12/2006 12:00:00 AM
DateTime(long tick)
还可以使用 long 类型的计数值(tick)来构造 DateTime
,其中计数值从 01/01/0001 年午夜开始计时,单位为 100ns 。
DateTime.FromFileTime(long fileTime)
和 DateTime.FromFileTimeUtc(long fileTime)
上述静态方法将 Windows 文件时间转换为本地时间和 UTC 时间。
DateTime.FromOADate(double d)
将 OLE 时间转换为等效的 DateTime
时间。
OLE 时间通常以双精度浮点数(double)表示,其中整数部分代表自1899年12月30日以来的天数,小数部分代表一天中的具体时间。例如:
1.0 表示1899年12月31日(即基点日期后的第一天)。
2.5 表示1900年1月1日中午12点(即基点日期后的第二天,加上半天)。
6.2.2.3 创建一个 DateTimeOffset
DateTimeOffset
和 DateTime
具有类似的构造器,其区别是 DateTimeOffset
还需要指定一个 TimeSpan
类型的 UTC 偏移量:
public DateTimeoffset (int year, int month, int day,
int hour, int minute, int second,
TimeSpan offset);
public DateTimeoffset (int year, int month, int day,
int hour, int minute, int second, int millisecond,
TimeSpan offset);
TimeSpan
必须是 整数分钟数 ,否则构造器会抛出一个异常。
DateTime
可以 隐 式转换为 DateTimeOffset
:
DateTimeOffset dt = new DateTime(2000, 2, 3);
6.2.2.4 获取当前 DateTime
/DateTimeOffset
DateTime
有一个 Today
属性,返回日期的部分:
Console.WriteLine (DateTime.Today); // 2024/2/6 0:00:00
这些方法(Now
等)的返回精度取决于操作系统,一般情况下都在 10~20 毫秒范围内。
6.2.2.5 日期和时间的处理
DateTime
和 DateTimeOffset
都提供了如下相似的属性和方法(有删减):
-
DateTime.DayOfWeek
-
DateTime.DayOfYear
-
DateTime.TimeOfDay
返回
TimeSpan
类型。 -
DateTimeOffset.Offset
输出 时区偏移量 。
另外,DateTime
和 DateTimeOffset
实例均可进行加减操作,其背后调用的是 Add()
方法。
6.2.2.6 格式化和解析
DateTime
有多种格式化方式:
-
ToString()
2024/2/6 17:57:28
-
ToShortDateString()
2024/2/6
-
ToLongDateString()
2024 年 2 月 6 日
-
ToShortTimeString()
17:57
-
ToLongTimeString()
17:57:28
DateTimeOffset
仅有 ToString()
方法。
C7.0 核心技术指南 第7版.pdf - p287 - C7.0 核心技术指南 第 7 版-P287-20240206180202
使用“o”,则输出的是如下格式:
2024-02-06T18:01:33.1248308+08:00
而 Parse
/TryParse
和 ParseExact
/TryParseExact
静态方法则执行和 ToString
相反的操作:它们将字符串转换为 DateTime(Offset)
。这些方法同样进行了重载以接受格式提供器。
6.2.2.7 DateTime
和 DateTimeOffset
空值
由于 DateTime
和 DateTimeOffset
都是结构体因此它们是不能为 null。当需要将其设置为 null 的时候可以使用如下两种方法:
- 使用
Nullable
类型(例如DateTime?
或DateTimeOffset?
)。 - 使用
DateTime(Offset).MinValue
静态字段(它们同时也是这些类型的默认值)。
使用可空类型通常是最佳方法,因为编译器可以防止代码出现错误。DateTime.MinValue
对于兼容 C#2.0(引入了可空类型)之前编写的代码是很有用的。
C7.0 核心技术指南 第7版.pdf - p287 - C7.0 核心技术指南 第 7 版-P287-20240206180806
6.3 日期和时区
6.3.1 DateTime
与时区
DateTime
有如下实例方法用于转换得到其他类型 DateTimeKind
的时间:
-
ToUniversalTime()
-
ToLocalTime()
还可以通过 DateTime.SpecifyKind
静态方法创建一个 Kind
不同的 DateTime
:
DateTime d = new DateTime (2000, 12, 12); // Unspecified
DateTime utc2 = DateTime.SpecifyKind (d, DateTimeKind.Utc);
注意:上述方法仅改变 Kind
属性,不改变时间内容。
6.3.2 DateTimeOffset
与时区
DateTimeOffset
内部包括一个总是 UTC 的 DateTime
字段和一个用 16 位整数表示的以 分钟 计量的 UTC 偏移量。在比较时仅仅比较(UTC 的) DateTime
字段 ,而偏移量主要用于 格式化 。
DateTimeOffset
有如下实例方法用于转换得到其他 UTC 的 DateTimeOffset
时间:
-
ToUniversalTime()
-
ToLocalTime()
与 DateTime
不同,这些方法不会影响底层的日期/时间值,只是影响 偏移量 ,时间点相同,其比较结果 也相同 。
如果要在比较中将 Offset
(偏移量)也考虑在内,可以使用 EqualsExact
方法,如下代码将输出“ True,False ”:
DateTimeOffset local = DateTimeOffset.Now;
DateTimeOffset utc = local.ToUniversalTime();
Console.WriteLine (local == utc);
Console.WriteLine (local.EqualsExact(utc));
6.3.3 TimeZone
和 TimeZoneInfo
TimeZone
和 TimeZoneInfo
类均提供时区名称、时区的 UTC 偏移量和夏令时规则。
-
TimeZone
可以访问 本地 时区。 -
TimeZoneInfo
可以访问 全世界 时区,具有更加丰富的(虽然有时使用不便)夏令时描述规则模型。
6.3.3.1 TimeZone
TimeZone.CurrentTimeZone
静态方法会根据本地设置返回一个 TimeZone
实例,该实例常用的方法、属性有:
-
StandardName
、DaylightName
获取 标准时区 名称、获取 夏时制时区 名称。
TimeZone zone = TimeZone.CurrentTimeZone; zone.StandardName.Dump ("StandardName"); // 中国标准时间 zone.DaylightName.Dump ("DaylightName"); // 中国夏令时
-
IsDaylightSavingTime
传入
DateTime
实例,判断 当前日期是否为夏令时 。DateTime dt1 = new DateTime (2008, 1, 1); DateTime dt2 = new DateTime (2008, 6, 1); zone.IsDaylightSavingTime (dt1).Dump ("IsDaylightSavingTime (January)"); // True zone.IsDaylightSavingTime (dt2).Dump ("IsDaylightSavingTime (June)"); // False
-
GetUtcOffset
返回指定本地时间的 协调世界时 (UTC) 偏移量 。示例以加利福尼亚时间为准:
zone.GetUtcOffset (dt1).Dump ("UTC Offset (January)"); // 08:00:00 zone.GetUtcOffset (dt2).Dump ("UTC Offset (June)"); // 09:00:00
-
GetDaylightChanges
获取 指定年份的夏令时信息 。示例以加利福尼亚时间为准:
DaylightTime day = zone.GetDaylightChanges (2010); day.Start.Dump ("day.Start"); // 08 March day.End.Dump ("day.End"); // 01 November day.Delta.Dump ("day.Delta"); // 01:00:00
6.3.3.2 TimeZoneInfo
TimeZoneInfo
与 TimeZone
相似的属性、方法有:
-
TimeZoneInfo.Local
相当于
TimeZone.CurrentTimeZone
-
StandardName
、DaylightName
-
IsDaylightSavingTime
可接受
DateTime
实例 -
GetUtcOffset
可接受
DateTime
实例
下面将介绍与 TimeZone
不同的方法、属性
TimeZoneInfo.FindSystemTimeZoneById()
该方法根据 时区 ID 获得任意时区的 TimeZoneInfo
,使用方法如下:
TimeZoneInfo wa = TimeZoneInfo.FindSystemTimeZoneById ("W. Australia Standard Time");
Console.WriteLine (wa.Id); // W. Australia Standard Time
Console.WriteLine (wa.DisplayName); // (GMT+08:00) Perth
Console.WriteLine (wa.BaseUtcOffset); // 08:00:00
Console.WriteLine (wa.SupportsDaylightSavingTime); // True
TimeZoneInfo.GetSystemTimeZones()
该静态方法返回 全世界所有的时区 :
foreach (TimeZoneInfo z in TimeZoneInfo.GetSystemTimeZones())
Console.WriteLine (z.Id);
C7.0 核心技术指南 第7版.pdf - p290 - C7.0 核心技术指南 第 7 版-P290-20240207142423
TimeZoneInfo.ConvertTime()
该静态方法将 DateTime
或 DateTimeOffset
从 一个时区 转换到 另一个时区 。若想要直接与 UTC 时间互转,可以使用 ConvertTimeFromUtc()
和 ConvertTimeToUtc()
。
夏令时的处理方法
太平洋时间的夏令时规则如下:夏令时开始于每年三月的第二个星期日凌晨 2 点,此时钟表会调快一小时变为 3 点,减少那一天的时长一小时(即 02:00:00~03:00:00 消失)。夏令时结束于每年十一月的第一个星期日凌晨 2 点,此时钟表会调慢一小时变为 1 点,使那一天增加一小时。这意味着,在夏令时期间,夜晚到来的时间会更晚,以便利用更多的日光时间。
太平洋时间的夏令时结束于每年十一月的第一个星期日凌晨 2 点,此时钟表会调慢一小时变为 1 点,使那一天增加一小时(即 01:00:00~02:00:00 发生重复)。这意味着在夏令时结束时,人们会经历一个“额外”的小时,通常用于多睡一个小时。这个调整标志着回到标准时间的转变,为即将到来的冬季做准备。
TimeZoneInfo
专门提供了处理夏令时的方法,罗列如下:
-
IsInvalidTime()
当
DateTime
是夏令时时钟的无效时间(即凌晨 2 点至凌晨 3 点),返回true
。 -
IsAmbiguousTime()
当
DateTime
或DateTimeoffset
是夏令时结束的重复时间(即凌晨 1 点至凌晨 2 点),返回true
。 -
GetAmbiguousTimeOffsets()
当
DateTime
或者DateTimeOffset
表示的时间为夏令时结束的重复时间,该方法会返回TimeSpan
数组,表示可能的 UTC 偏移量。
TimeZoneInfo
获取夏令时起止日期
TimeZoneInfo 获取夏令时起止日期非常复杂,此处不做赘述。
6.3.4 夏令时与 DateTime
如果使用 DateTimeOffset
或 UTC 的 DateTime
进行相等比较,那么结果 不受 夏令时影响。对于本地时间的 DateTime
则 可能会受到 夏令时的影响。
这些规则可以总结为:
- 夏令时只影响 本地 时间,而不影响 UTC 时间。
- 当且仅当使用本地时间的
DateTime
时,当夏令时的时钟回调时,基于时间前移的比较将得到错误的结果。 - 即使时钟回调了,也总是可以可靠地将本地时间转换为 UTC 时间,反之亦然。
夏令时的时间进行比较时,建议使用 ToUniversalTime
将本地时间转换为 UTC 时间再进行。
C7.0 核心技术指南 第7版.pdf - p294 - C7.0 核心技术指南 第 7 版-P294-20240207191838
IsDaylightSavingTime()
当实例为 DateTimeOffset
类型,或实例 DateTime.Kind
为 UTC ,该方法始终返回 false
。
6.4 格式化和解析
6.4.1 ToString
和 Parse
所有简单的值类型的 ToString
方法都能产生有意义的字符串输出。同时这些类型都定义了静态的 Parse
方法来反向转换,转换失败则会抛出 FormatException
。
此外,DateTime(Offset)
和数字类型的 Parse
和 TryParse
方法会使用 本地的文化设置 。我们可以通过两种方式解决:
- 指定一个
CultureInfo
对象覆盖本地文化设置; - 转换时指定一个 不变文化 。
例如在德国,“.”表示千位分隔符而非小数点。可以通过指定一个 不变文化( CultureInfo.InvariantCulture
) 解决上述问题:
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo ("de-DE"); // Germany
double.Parse ("1.234").Dump ("Parsing 1.234"); // 1234
(1.234).ToString ().Dump ("1.234.ToString()"); // 1,234
// Specifying invariant culture fixes this:
double.Parse ("1.234", CultureInfo.InvariantCulture).Dump ("Parsing 1.234 Invariantly"); // 1.234
(1.234).ToString (CultureInfo.InvariantCulture).Dump ("1.234.ToString Invariant"); // 1.234
6.4.2 格式提供器
所有 数字 类型以及 DateTime(offset)
类型都实现了 IFormattable
接口,该接口定义如下:
public interface IFormattable
{
string ToString(string format, IFormatProvider formatProvider);
}
IFormattable
IFormattable
接口方法的第一个参数是 格式字符串(format string) ,用于提供指令;第二个参数是 格式提供器( IFormatProvider
) ,决定指令将如何转换。例如:
NumberFormatInfo formatInfo = new NumberFormatInfo();
f.CurrencySymbol = "$$";
Console.WriteLine (3.ToString ("C", formatInfo)); // $$ 3.00
这里的“C”是表示货币的格式字符串,而 NumberFormatInfo
对象是一个 格式提供器 ,它决定了货币(和其他数字形式)该如何展示。这个机制也支持全球化。
默认格式提供器(IFormatProvider
)
如果格式字符串(format string)或格式提供器(IFormatProvider
)为 null,则使用默认格式提供器( CultureInfo.CurrentCulture
),它的默认值由控制面板设置决定。
方便起见,大多数类型都重载了 ToString
方法,调用无参 ToString
方法,相当于直接使用 默认 格式提供器,格式字符串为 空字符串 :
// 使用空字符串,默认格式提供器。以下代码等价
Console.WriteLine (10.3.ToString ("", null));
Console.WriteLine (10.3.ToString ());
// 输出结果相同
Console.WriteLine (10.3.ToString ("C", null));
Console.WriteLine (10.3.ToString ("C"));
.NET Framework 定义了以下三种格式提供器(它们都实现了 IFormatProvider
):
-
NumberFormatInfo
-
DateTimeFormatInfo
-
CultureInfo
6.4.2.1 格式提供器和 CultureInfo
不变文化总是保持相同的设置,和计算机设置无关:
DateTime dt = new DateTime(2000, 1, 2);
CultureInfo iv = CultureInfo.InvariantCulture;
Console.WriteLine(3.ToString("C", iv)); // ¤3.00
Console.WriteLine (dt.ToString (iv)); // 01/02/2000 00:00:00
Console.WriteLine (dt.ToString ("d", iv)); // 01/02/2000
C7.0 核心技术指南 第7版.pdf - p297 - C7.0 核心技术指南 第 7 版-P297-20240207231306
6.4.2.2 使用 NumberFormatInfo
和 DateTimeFormatInfo
通过构造器得到的 NumberFormatInfo
、DateTimeFormatInfo
实例,其初始设置基于不变文化。若想以其他文化做为起点,可以 Clone 一个现有的格式提供器:
NumberFormatInfo f2 = (NumberFormatInfo) CultureInfo.CurrentCulture.NumberFormat.Clone();
// Now we can edit f2:
f2.NumberGroupSeparator = "*";
Console.WriteLine (12345.6789.ToString ("N3", f2)); // 12*345.679
原生格式提供器(
CultureInfo.CurrentCulture.NumberFormat
)为只读的,克隆得到的实例是可写的
6.4.2.3 组合格式化
String.Format
、Console.Write(Line)
、StringBuilder.AppendFormat
等方法都可以接受组合格式字符串,例如:
string composite = "Credit={0:C}";
Console.WriteLine (string.Format (composite, 500)); // Credit=$500.00
// 可简化为
Console.WriteLine ("Credit={0:C}", 500); // Credit=$500.00
不过,String.Format
还可以接受一个** 格式提供器 **:
object someObject = DateTime.Now;
string s = string.Format (CultureInfo.InvariantCulture, "{0}", someObject);
这段代码等价于:
object someObject = DateTime.Now;
string s;
if (someObject is IFormattable)
s = ((IFormattable)someObject).ToString (null, CultureInfo.InvariantCulture);
else if (someObject == null)
s = "";
else
s = someObject.ToString();
6.4.2.5 IFormatProvider
和 ICustomFormatter
自定义格式提供器,需要实现 IFormatProvider
、 ICustomeFormatter
接口:
public interface IFormatProvider
{
object? GetFormat(Type? formatType);
}
public interface ICustomFormatter
{
string Format(string? format, object? arg, IFormatProvider? formatProvider);
}
其中 ICustomFormatter.Format
方法负责 执行格式化 ,IFormatProvider.GetFormat
方法返回 格式器(即自定义类型) :
static void Main()
{
double n = -123.45;
IFormatProvider fp = new WordyFormatProvider();
Console.WriteLine(string.Format(fp, "{0:C} in words is {0:W}", n));
}
public class WordyFormatProvider : IFormatProvider
{
public object GetFormat (Type formatType)
{
if (formatType == typeof (ICustomFormatter)) return new WordFormatter();
return null;
}
}
public class WordFormatter : ICustomFormatter
{
static readonly string[] _numberWords =
"zero one two three four five six seven eight nine minus point".Split();
IFormatProvider _parent; // Allows consumers to chain format providers
public WordFormatter() : this(CultureInfo.CurrentCulture) { }
public WordFormatter(IFormatProvider parent)
{
_parent = parent;
}
public string Format(string format, object arg, IFormatProvider prov)
{
// If it's not our format string, defer to the parent provider:
if (arg == null || format != "W")
return string.Format(_parent, "{0:" + format + "}", arg);
StringBuilder result = new StringBuilder();
string digitList = string.Format(CultureInfo.InvariantCulture, "{0}", arg);
foreach (char digit in digitList)
{
int i = "0123456789-.".IndexOf(digit);
if (i == -1) continue;
if (result.Length > 0) result.Append(' ');
result.Append(_numberWords[i]);
}
return result.ToString();
}
}
Notice
上述代码和书中代码不同,我将
IFormatProvider
和ICustomFormatter
分成了两个类,便于理解。原书代码为:static void Main() { double n = -123.45; IFormatProvider fp = new WordyFormatProvider(); Console.WriteLine (string.Format (fp, "{0:C} in words is {0:W}", n)); } public class WordyFormatProvider : IFormatProvider, ICustomFormatter { static readonly string[] _numberWords = "zero one two three four five six seven eight nine minus point".Split(); IFormatProvider _parent; // Allows consumers to chain format providers public WordyFormatProvider () : this (CultureInfo.CurrentCulture) { } public WordyFormatProvider (IFormatProvider parent) { _parent = parent; } public object GetFormat (Type formatType) { if (formatType == typeof (ICustomFormatter)) return this; return null; } public string Format (string format, object arg, IFormatProvider prov) { // If it's not our format string, defer to the parent provider: if (arg == null || format != "W") return string.Format (_parent, "{0:" + format + "}", arg); StringBuilder result = new StringBuilder(); string digitList = string.Format (CultureInfo.InvariantCulture, "{0}", arg); foreach (char digit in digitList) { int i = "0123456789-.".IndexOf (digit); if (i == -1) continue; if (result.Length > 0) result.Append (' '); result.Append (_numberWords[i]); } return result.ToString(); } }
6.5 标准格式字符串与解析标记
6.5.1 数字格式字符串
C7.0 核心技术指南 第7版.pdf - p301 - C7.0 核心技术指南 第 7 版-P301-20240208161024
如果不提供数值格式字符串(或者使用 null 空字符串),那么相当于使用不带数字的“G”标准格式字符串。这包括了以下两种形式:
- 小于 \(10^{-4}\) 或大于该类型精度的数字将表示为指数形式(科学记数法)。
- 受
float
和double
精度限制的两位小数是经过舍入的,以避免从二进制形式转换为十进制时的内在不精确性。
C7.0 核心技术指南 第7版.pdf - p303 - C7.0 核心技术指南 第 7 版-P303-20240208161813
6.5.2 NumberStyles
数字类型都定义了一个静态 Parse
方法,接受 NumberStyles
(标记枚举)参数,它决定了字符串转换为数字的读取方式。
它有如下枚举成员:
// 允许字符串前、后有空白字符
AllowLeadingWhite = 1,
AllowTrailingWhite = 2,
// 允许字符串前、后有正负号
AllowLeadingSign = 4,
AllowTrailingSign = 8,
// 允许使用括号表示负数(财会常用)
AllowParentheses = 0x10,
// 允许字符串有小数点
AllowDecimalPoint = 0x20,
// 允许字符串包含千分位分隔符
AllowThousands = 0x40,
// 允许字符串使用科学计数法
AllowExponent = 0x80,
// 允许字符串包含货币符号
AllowCurrencySymbol = 0x100,
// 指定字符串表示16进制数字
AllowHexSpecifier = 0x200,
// 允许字符串表示二进制数字
AllowBinarySpecifier = 0x400,
这些枚举成员组合后构成了常用的几个值:
None = 0,
Integer = 7,
HexNumber = 0x203,
BinaryNumber = 0x403,
Number = 0x6F,
Float = 0xA7,
Currency = 0x17F,
Any = 0x1FF
Parse
方法默认使用这些常用值。
若不想使用默认值,需要显式指定 NumberStyles
:
int thousand = int.Parse ("3E8", NumberStyles.HexNumber);
int minusTwo = int.Parse ("(2)", NumberStyles.Integer | NumberStyles.AllowParentheses);
double.Parse ("1,000,000", NumberStyles.Any).Dump ("million");
decimal.Parse ("3e6", NumberStyles.Any).Dump ("3 million");
decimal.Parse ("$5.20", NumberStyles.Currency).Dump ("5.2");
因为我们没有指定格式提供器,因此这个例子支持本地货币符号、分组符号、小数点等。下一个例子以硬编码的方式使用欧元符号和空格分组符号来表示货币:
NumberFormatInfo ni = new NumberFormatInfo();
ni.CurrencySymbol = "€";
ni.CurrencyGroupSeparator = " ";
double.Parse ("€1 000 000", NumberStyles.Currency, ni).Dump ("million");
6.5.3 DateTime 格式字符串
DateTime
、DateTimeOffset
常用的格式字符串有:
C7.0 核心技术指南 第7版.pdf - p305 - C7.0 核心技术指南 第 7 版-P305-20240208172527
C7.0 核心技术指南 第7版.pdf - p305 - C7.0 核心技术指南 第 7 版-P305-20240208172548
"x"、"R"和"u"格式字符串会添加一个表示 UTC 的后缀,但是它们不能将一个本地时间自动转换为 UTC 时间(所以必须自行进行转换)。奇怪的是,"U"会自动转换为 UTC,但是它不会添加时区后缀!事实上,"o"是上述分类符中唯一一个不需要干预就能够产生一个明确的
DateTime
的格式分类符。
DateTime
的解析与误解析
解析日期时,“月/日/年”和“日/月/年”很容易混淆。避免该问题有两种方式:
- 格式化、解析时 总是显式指定相同的
CultureInfo
( 如 CultureInfo.InvariantCulture
====)。 - 格式化、解析时 使用文化无关的方式(如格式字符串“o”) 。
此处更推荐第二种。
这里说明:
格式字符串可以和
CultureInfo
一起搭配使用例如,格式字符串“C”和
CultureInfo
共同决定输出货币的形式。部分格式字符串将使
CultureInfo
参数失效例如,使用格式字符串“o”,将忽略
CultureInfo
的作用。
Parse
和 ParseExact
区别
-
Parse
方法 隐 式接受日期格式“o”和格式化提供器CurrentCulture
:DateTime dt2 = DateTime.Parse (s);
-
ParseExact
要求 显 式提供日期格式和格式化提供器:DateTime dt1 = DateTime.ParseExact (s, "o", null);
C7.0 核心技术指南 第7版.pdf - p306 - C7.0 核心技术指南 第 7 版-P306-20240209091429
6.5.4 DateTimeStyles
DateTimeStyles
为标记枚举,用于 DateTime(Offset).Parse
方法。以下是枚举成员:
None = 0,
AllowLeadingWhite = 1,
AllowTrailingWhite = 2,
AllowInnerWhite = 4,
AllowWhiteSpaces = AllowLeadingWhite | AllowTrailingWhite | AllowInnerWhite,
// 解析时分秒字符串,年月日是否使用当前时间
NoCurrentDateDefault = 8,
// 参照字符串时区信息,将输出的日期转换为UTC时间
AdjustToUniversal = 0x10,
// AssumeLocal、AssumeUniversal 用于字符串没有时区信息
AssumeLocal = 0x20,
AssumeUniversal = 0x40,
RoundtripKind = 0x80
6.6 其他转换机制
6.6.1 Convert
类
.NET Framework 将以下类型称为基本类型:
-
bool
、char
、string
、System.DateTime
和System.DateTimeOffset
- 所有的 C# 数字类型
Convert
类可以对上述数据进行转换。
C7.0 核心技术指南 第7版.pdf - p308 - C7.0 核心技术指南 第 7 版-P308-20240209094355
6.6.1.1 实数到实数的舍入转换
Convert
类的数字转换采用 银行家 舍入方式,避免截断产生不符合要求的数据。
double d = 3.9;
int i = Convert.ToInt32 (d); // i == 4
如果银行舍入方式不适用,可以使用 Math.Round
方法,该方法可以使用额外参数控制中间值舍入方式。
银行家舍入方式,又称为“四舍六入五考虑”,是一种特殊的四舍五入规则,其核心原则如下:
四舍六入:当舍去位的数值小于 5 时直接舍去;大于等于 6 时,则在舍去该位的同时向前一位进一。
五考虑:当舍去位的数值等于 5 时,需要考虑五后面的数字以及五前面的数字:
五后非零就进一:如果 5 后面有非零数字,则向前一位进一。
五后为零看奇偶:
如果 5 后面没有数字(即 5 是最后一位),则看 5 前面的数字:
- 如果 5 前面的数字为偶数,则直接舍去 5。
- 如果 5 前面的数字为奇数,则向前一位进一。
这种舍入方式的优点是减少了在大量计算时舍入误差积累的偏差,使得结果更加公平和中性。银行家舍入法广泛应用于财务计算、统计学以及计算机科学中,特别是在浮点数运算和金融软件开发中,它有助于确保在四舍五入时整体上保持数值的准确性和公平性。
6.6.1.2 解析二、八、十六进制
ToXXX
方法包括一些重载方法,可以将字符串解析为其他进制:
int thirty = Convert.ToInt32 ("1E", 16); // Parse in hexadecimal
uint five = Convert.ToUInt32 ("101", 2); // Parse in binary
第二个参数指定的进制必须是 2 、 8 、 10 、 16 进制。
6.6.1.3 动态转换
Convert.ChangeType()
方法可以进行动态转换:
Type targetType = typeof (int);
object source = "42";
object result = Convert.ChangeType(source, targetType);
其中 source
和 targetType
必须是 基本 类型。
上述方法的用途之一是编写可以处理多种类型的 反序列化器 。它还能够将任意 枚举 类型转换为对应的整数类型。
6.6.1.4 Base64 转换
Base64 使用 ASCII 字符集中的 64 个字符将二进制数据编码为可读的字符。
可以使用 Convert.ToBase64String
将字节数组转换为 Base64 格式,使用 Convert.FromBase64String
执行相反的操作。
6.6.2 XmlConvert
非 DateTime
转换
XmlConvert
的方法不需要提供特殊的格式字符串就能够处理 XML 格式的细微差别。
其格式化方法均为重载的 ToString
方法,解析方法为 ToBoolean
、ToDateTime
等:
string s = XmlConvert.ToString (true); // s = "true",而非"True"
XmlConvert.ToBoolean (s).Dump();
DateTime
转换
XmlConvert.ToString(DateTime)
和 XmlConvert.ToDateTime()
(格式化和解析)方法可以接受 XmlDateTimeSerializationMode
枚举参数,其元素为:
// 字符串为Local时区信息,转化/解析得到的时间为Local时间
Local,
// 字符串无时区信息,转化/解析得到的时间为UTC时间
Utc,
// 字符串无时区信息
Unspecified,
// 保持DateTime的原DateTimeKind
RoundtripKind
6.6.3 类型转换器
类型转换器用于解析 XAML。
所有类型转换器都继承自 TypeConverter
,获取转换器需要通过 TypeDescriptor
类的 GetConverter
方法,方式如下:
TypeConverter cc = TypeDescriptor.GetConverter (typeof (Color));
TypeConverter
常用的方法有 ConvertToString
和 ConvertFromString
:
Color beige = (Color) cc.ConvertFromString ("Beige");
Color purple = (Color) cc.ConvertFromString ("#800080");
Color window = (Color) cc.ConvertFromString ("Window");
按照惯例,类型转换器的名称应以 Converter
结尾,并且通常与它们转换的类型位于同一个命名空间中。类型是通过 TypeConverterAttribute
与转换器联系在一起的。这样设计器就可以自动获得对应的转换器了。
类型转换器还可以提供一些设计时的服务,例如为设计器生成标准的下拉列表项,或者辅助代码序列化。
6.6.4 BitConverter
除 decimal
和 DateTime(Offset)
类型外,其他基本类型都可以通过 BitConverter.GetBytes
转换为字节数组:
foreach (byte b in BitConverter.GetBytes (3.5))
Console.Write (b + " "); // 0 0 0 0 0 0 12 64
BitConverter
还提供了将字节数组反向解析的方法,如 BitConverter.ToDouble
Warn
BitConverter
也不支持string
BitConverter
和 decimal
BitConverter
不支持 decimal
。可以通过 decimal.GetBits
方法得到相似的结果。
decimal.GetBits
方法返回 int
数组 ,且 decimal
也有接受 int
数组 的构造器。
BitConverter
和 DateTime(Offset)
BitConverter
不支持 DateTime(Offset)
,但是可以通过 DateTime.ToBinary
方法得到日期对应的 long
,再通过 BitConverter.GetBytes
得到对应数组。
解析时通过 BitConverter.ToLong
和 DateTime.FromBinary
方法进行。
DateTime(long ticks)
和DateTime.FromBinary(long dateData)
的区别
new DateTime(long ticks)
直接根据刻度数创建日期时间0001 年 1 月 1 日午夜 12:00:00 以来所经过的时间以 100 纳秒为单位的刻度数(Ticks)
DateTime.FromBinary(long value)
从包含额外日期时间类型信息的二进制值重建DateTime
实例。可以准确地还原由
DateTime
实例转换成二进制表示的日期和时间,包括它的Kind
属性。这使得通过FromBinary
方法重建的DateTime
实例能够保留原实例的本地或 UTC 时间信息。
总结:
-
System.Convert
适用于基本数据类型之间的通用转换, 接受
IFormatableProvider
进行本地化。 -
System.Xml.XmlConvert
专注于 XML 数据与 .NET 数据类型之间的转换,确保数据的正确格式化和编码。 不接受
IFormatableProvider
参数。 -
System.BitConverter
用于基本数据类型和它们的字节表示之间的转换,适用于需要处理二进制数据的场景。 不接受
IFormatableProvider
参数,不支持 decimal
、 DateTime(Offset)
类型。
6.7 全球化
全球化专注于三个任务(重要性从大到小):
- 保证程序在其他文化环境中运行时不会出错。
- 采用一种本地文化的格式化规则,例如日期的显示。
- 设计程序,使之能够从将来编写和部署的附属程序集中读取文化相关的数据和字符串。
6.7.1 全球化检查清单
以下是一些必要任务的总结:
- 认识 Unicode 和文本编码
- 要记住
char
和string
的一些方法是文化相关的,如ToUpper
和ToLower
,若不区分文化,则应当使用 ToUpperInvariant
和 ToLowerInvariant
。 - 推荐使用文化无关的方式对
DateTime(Offset)
进行格式化和解析。例如ToString("o")
以及XmlConvert
。 - 除非希望使用本地文化行为,否则请在格式化和解析数字或日期/时间时指定一个文化。
6.7.2 测试
可以通过 Thread.CurrentCulture
属性来模拟不同文化:
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("tr-TR");
这里推荐模拟土耳其文化进行测试,原因如下:
-
"i".ToUpper() != "I"
,且"I".ToLower() != "i"
。 - 日期使用“日.月.年”的方式进行格式化,分隔符为“
.
”。 - 小数点符号为“逗号”,而非"点"。
可以通过修改控制面板中的数字、日期格式进行测试,这将影响默认文化设置(CultureInfo.CurrentCulture
)。
Info
更多内容见18.6.4.2 测试附属程序集、18.6.5 文化和子文化
6.8 操作数字
6.8.2 Math
Math 中定义的舍入方法有 4 个:
-
Math.Round
可以指定舍入位数、如何处理中间值
-
Math.Truncate
丢弃小数部分
-
Math.Floor
向下舍入
-
Math.Ceiling
向上舍入
Eureka
Math.Truncate
和Math.Floor
看起来功能相同,但是面对 负数 他们的行为将出现大不同!
6.8.3 BigInteger
BigInteger
可以表示任意大的整数而不会丢失精度。它有如下特点:
-
可以从任意整数类型 隐 式转换为
BigInteger
BigInteger twentyFive = 25; // implicit cast from integer
-
可以通过其 静态 方法表示更大的数字
例如使用
BigInteger.Pow
方法:BigInteger googol = BigInteger.Pow (10, 100);
通过
Parse
方法创建大数:BigInteger googolFromString = BigInteger.Parse ("1".PadRight (100, '0'));
-
可以通过 字节数组 创建大数:
RandomNumberGenerator rand = RandomNumberGenerator.Create(); byte[] bytes = new byte [32]; rand.GetBytes (bytes); var bigRandomNumber = new BigInteger (bytes); // Convert to BigInteger
-
ToString
方法将打印所有数字
基础数值类型和 BigInteger
可以显式相互转换,但可能 损失精度 :
double g1 = 1e100; // implicit cast
BigInteger g2 = (BigInteger) g1; // explicit cast
g2.Dump(); // 输出:10000000000000000159028911097599180468360808563945281389781327557747838772170381060813469985856815104
6.8.4 Complex
(复数)
Complex
实例化方式如下:
var c1 = new Complex (2, 3.5);
var c2 = new Complex (3, 0);
Complex
类型有如下特点:
-
标准数值类型可以 隐 式转换为
Complex
类型 -
可以通过属性访问实部和虚部
-
Complex.Real
实 部 -
Complex.Imaginary
虚 部 -
Complex.Phase
相位角 -
Complex.Magnitude
模
-
-
可以通过 相位角 和 模 来构建
Complex
Complex c3 = Complex.FromPolarCoordinates (1.3, 5);
-
可以使用标准运算符进行运算
Console.WriteLine (c1 + c2); // (5, 3.5) Console.WriteLine (c1 * c2); // (6, 10.5)
-
一些高级静态方法
- 三角函数
- 取对数和求幂
- Conjugate(求共轭复数)
Complex.Atan (c1).Dump ("Atan"); Complex.Log10 (c1).Dump ("Log10"); Complex.Conjugate (c1).Dump ("Conjugate");
6.8.5 Random
Random
类能够生成类型为 byte
、 integer
、 double
的伪随机数序列。若不指定种子参数,则将用当前 系统时间 来生成种子。
C7.0 核心技术指南 第7版.pdf - p316 - C7.0 核心技术指南 第 7 版-P316-20240210093951
RandomNumberGenerator
Random
的随机性不够高,.NET Framework 提供了一种密码强度的随机数生成器 RandomNumberGenerator
。使用方式如下:
var rand = System.Security.Cryptography.RandomNumberGenerator.Create();
byte[] bytes = new byte [4];
rand.GetBytes (bytes); // Fill the byte array with random numbers.
BitConverter.ToInt32 (bytes, 0).Dump ("A cryptographically strong random integer");
该生成器通过 填充字节数组 产生随机数,不够灵活,要通过 BitConverter
获取相应的随机数。
6.9 枚举
枚举隐式派生自 System.Enum
类型,因此都可以隐式转换为 System.Enum
实例:
enum Nut { Walnut, Hazelnut, Macadamia }
enum Size { Small, Medium, Large }
static void Main()
{
Display (Nut.Macadamia); // Nut.Macadamia
Display (Size.Large); // Size.Large
}
static void Display (Enum value) // The Enum type unifies all enums
{
Console.WriteLine (value.GetType().Name + "." + value.ToString());
}
6.9.1 枚举值转换
6.9.1.1 将枚举转换为整数
对于 System.Enum
实例,将枚举值转换为整数有 4 种方式:
方式一:先将实例转换为 object
再进行:
static int GetIntegralValue (Enum anyEnum)
=> (int) (object) anyEnum;
该方法也有缺陷,当 anyEnum
对应的是 long
类型,上述转化将抛出 InvalidCastException
。
方式二:使用 Convert.ToDecimal
static decimal GetAnyIntegralValue (Enum anyEnum)
=> Convert.ToDecimal (anyEnum);
此处用到了任何整形都可以转换为 decimal
的特点。
Warn
此处不能是如下代码,否则也会抛出
InvalidCastException
:(decimal)(object)anyEnum;
方法三:使用 Convert.ChangeType
static object GetBoxedIntegralValue (Enum anyEnum)
{
Type integralType = Enum.GetUnderlyingType(anyEnum.GetType());
return Convert.ChangeType(anyEnum, integralType);
}
此处先用 Enum.GetUnderlyingType
获取 enum
的整数类型,再使用 Convert.ChangeType
进行转化。该方法会保持原始的整数类型。
C7.0 核心技术指南 第7版.pdf - p318 - C7.0 核心技术指南 第 7 版-P318-20240210104748
方法四:调用 Format
或 ToString
方法
注意,需要指定格式字符串“ D”或“d ”,否则将得到 枚举对应的字符串 而非数字。
static string GetIntegralValueAsString (Enum anyEnum)
=> anyEnum.ToString ("D"); // returns something like "4"
这种方式在编写自定义的序列化器时很有用。
6.9.1.2 将整数转换为枚举
Enum.ToObject
方法将整数值转换为一个给定类型的 enum 实例:
[Flags] public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 }
static void Main()
{
object bs = Enum.ToObject (typeof (BorderSides), 3);
Console.WriteLine (bs); // Left, Right
// This is the dynamic equivalent of this:
BorderSides bs2 = (BorderSides) 3;
}
6.9.1.3 字符串转换
将 enum 转换为字符串,可以调用静态的 Enum.Format
或实例的 ToString
方法。可用的格式字符串有:
格式字符串 | 行为 |
---|---|
G | 默认格式化行为,输出枚举名称 |
D | 输出对应整数值 |
X | 输出对应整数值(16 进制) |
F | 非标记枚举也按照标记枚举的方式格式化 |
Enum.Parse
方法可以将一个字符串转换为 enum,使用方式如下:
BorderSides leftRight = (BorderSides) Enum.Parse (typeof (BorderSides), "Left, Right");
BorderSides leftRightCaseInsensitive = (BorderSides)
Enum.Parse (typeof (BorderSides), "left, right", true);
第三个可选参数选择是否执行大小写不敏感的解析,如果成员不存在则抛出 ArgumentException
。
6.9.2 列举枚举值
Enum.GetValues
方法用于获取枚举中的所有成员(包括组合成员):
foreach (Enum value in Enum.GetValues (typeof (BorderSides)))
Console.WriteLine (value);
Enum.GetNames
执行相同的操作,但返回的是一个字符串数组。
C7.0 核心技术指南 第7版.pdf - p320 - C7.0 核心技术指南 第 7 版-P320-20240210111851
6.10 Guid
结构体
Guid
可以表示的值总共有 \(2^{128}\) 或 \(3.4×10^{18}\) 个。获取 Guid
方法有二:
-
通过
Guid.NewGuid
方法创建。 -
通过构造器实例化。
常用的构造器为:
-
public Guid (byte[] b);
需传入长度为 16 的
byte
数组,可以配合Guid.ToByteArray
方法一起使用。 -
public Guid (string g);
需传入
Guid
形式的字符串,该字符串可以放在圆括号或花括号中:var g1 = new Guid("7e33a841-b392-40b5-a8c4-de90eac5da7d"); var g2 = new Guid("{7e33a841-b392-40b5-a8c4-de90eac5da7d}"); var g3 = new Guid("(7e33a841-b392-40b5-a8c4-de90eac5da7d)"); var g4 = new Guid("7e33a841b39240b5a8c4de90eac5da7d");
-
Guid
有如下特点:
- 当以字符串形式出现的时候,
Guid
是一个由 32 个 十六 进制数字表示的值, Guid
是一个 结构体 ,支持 值 类型的语义,因而前面的例子可以使用相等运算符。-
Guid
的ToByteArray
方法可以将其转换为一个字节数组。 -
Guid.Empty
静态属性将返回一个空的Guid
(全部为 零 ),它通常用来表示 null
。
6.11 相等比较
6.11.2 标准等值比较协议
6.11.2.1 == 和 !=
==
和 !=
运算符执行静态解析(编译时确定执行方法),如下两种比较绑定的方法不同,结果不同:
如下代码将输出 True
:
int x = 5;
int y = 5;
Console.WriteLine(x == y);
如下代码将输出 False
:
object x = 5;
object y = 5;
Console.WriteLine(x == y);
6.11.2.2 Object.Equals
虚方法
Equals
是在运行时根据对象的实际类型解析的。对于引用类型,Equals
默认进行 引用相等 比较。对于结构体,Equals
会 调用每一个字段的 Equals
进行结构化比较。
其使用方式如下,如下代码将输出 True
:
object x = 5;
object y = 5;
Console.WriteLine(x.Equals(y));
6.11.2.3 object.Equals
静态方法
object.Equals
静态方法执行的操作如下:
public static bool AreEqual (object objA, object objB)
=> objA == null ? objB == null : objA.Equals (objB);
与 Object.Equals
虚方法不同,该静态方法接受两个参数。它常用于 ==
和 !=
无法使用的场景( 编译 时无法确认类型的场景),譬如泛型实例比较:
class Test<T>
{
T _value;
public void SetValue(T newValue)
{
if (!object.Equals(newValue, _value))
{
_value = newValue;
OnValueChanged();
}
}
protected virtual void OnValueChanged() {}
}
上述代码无法使用 ==
和 !=
(因为类型不确定,编译时无法绑定);对于 Object.Equals
虚方法,如果 newValue
为 null,则会抛出 NullReferenceException
异常,因此这里使用静态方法 Object.Equals
。
C7.0 核心技术指南 第7版.pdf - p325 - C7.0 核心技术指南 第 7 版-P325-20240210160719
6.11.2.4 object.ReferenceEquals
静态方法
object.ReferenceEquals
可以进行引用比较,防止 Equals
方法、==
、!=
重载 导致的引用比较失效。
C7.0 核心技术指南 第7版.pdf - p326 - C7.0 核心技术指南 第 7 版-P326-20240210165127
在 C#8.0 可以通过 is 运算符判断是否引用相同,详见 Pattern Matching
6.11.2.5 IEquatable<T>
接口
详见 DO:值类型需要实现IEquatable
6.11.2.6 Equals
和 ==
在何时并不等价
之前提到,有时 ==
和 Equals
应用不同的相等定义是非常有用的。
常见的不等价原因有两种:
1. 数值比较保证 自反性
例如如下代码分别输出“ False 、 True ”:
double x = double.NaN;
Console.WriteLine (x == x);
Console.WriteLine (x.Equals(x))
double
类型的 ==
运算符强制规定一个 NaN
不等于任何对象,即使是另一个值也是 NaN
。这从数学角度来说是非常自然的,并且也反映了底层 CPU 的行为。然而,Equals
方法必须支持 自反相等 ,换句话说:
x.Equals(x)
必须总是返回 true
。
集合和字典需要 Equals
保持这个行为,否则就无法找到之前存储的项目了。
2. 引用类型不同的相等含义
Equals
和 ==
含义不同这种做法在引用类型中要多得多,开发者自定义 Equals
实现 值 的相等比较,而仍旧令 ==
执行(默认的) 引用 相等比较。StringBuilder
类就是采用了这种方式,如下代码将输出“ False 、 True ”:
var sb1 = new StringBuilder ("foo");
var sb2 = new StringBuilder ("foo");
Console.WriteLine (sb1 == sb2);
Console.WriteLine (sb1.Equals (sb2));
6.11.3 相等比较和自定义类型
6.11.3.3 如何重写相等语义
下面是重写相等语义的步骤:
- 重写
GetHashCode()
和 Equals()
方法。 - (可选)重载
!=
和 ==
。 - (可选)实现
IEquatable<T>
。
6.11.3.8 范例:Area 结构体
在实现 GetHashCode
方法时,当类型拥有多于两个的字段,由 Josh Bloch 推荐的模式能够在结果良好的情况下同时保证性能:
int hash = 17; // 17 = some prime number
hash = hash * 31 + field1.GetHashCode(); // 31 = another prime number
hash = hash * 31 + field2.GetHashCode();
hash = hash * 31 + field3.GetHashCode();
...
return hash;
6.12 顺序比较
6.12.1 IComparable
IComparable
的定义方式如下:
public interface IComparable { int CompareTo (object other); }
public interface IComparable<in T> { int CompareTo (T other); }
大多数基本类型都实现了这两种 IComparable
接口。
IComparable
与 Equals
- 当
Equals
返回true
时,CompareTo
仅能返回 0 - 当
Equals
返回false
时,CompareTo
可以返回 任何结果,包括 0
换句话说,相等比较是严格的。例如:字符串"ṻ"和"ǖ"用 Equals
比较时是不同的,然而用 CompareTo
比较时则是相同的。总之,CompareTo
永远比不上 Equals
更严格。
C7.0 核心技术指南 第7版.pdf - p333 - C7.0 核心技术指南 第 7 版-P333-20240210214839
6.13 实用类
6.13.1 Console
类
Console.Out
属性、Console.SetOut
、Console.SetIn
可以搭配使用,重定向 Console
的输入和输出流:
// 保留原输出流
var oldOut = Console.Out;
// 输出至文件
using (TextWriter w = File.CreateText("D:\\output.txt"))
{
Console.SetOut(w);
Console.WriteLine("Hello world");
}
// 恢复原标准输出流
Console.SetOut(oldOut);
6.13.2 Environment
类
该类提供了很多有用的属性:
-
文件和文件夹
-
CurrentDirectory
、SystemDirectory
、CommandLine
-
-
计算机和操作系统
-
MachineName
、ProcessorCount
、OSVersion
、NewLine
-
-
用户登录
-
UserName
、UserInteractive
、UserDomainName
-
-
诊断信息
-
TickCount
、StackTrace
、WorkingSet
、Version
-
-
设置应用程序返回值
-
ExitCode
-
一些方法:
-
访问系统变量
-
GetEnvironmentVariable
、GetEnvironmentVariables
、SetEnvironmentVariable
-
6.13.4 AppContext
类
System.AppContext
类提供了一个 全局的开关配置表 ,以方便消费者控制新功能的开启和关闭状态。这种隐含的方式可用于添加实验功能而大多数用户并不知道它的存在。
例如,开发者可以使用如下方式启用程序库的某个功能:
AppContext.SetSwitch ("MyLibrary.SomeBreakingChange", true);
程序库的代码则可以使用如下的方式进行相关配置的开关检查:
bool isDefined, switchValue;
isDefined = AppContext.TryGetSwitch ("MyLibrary.SomeBreakingChange", out switchValue);
TryGetSwitch
方法将在开关未定义的情况下返回 false
。这样我们就可以区分未定义和值为 false
这两种不同的情况了,这种区分是非常必要的。
标签:Console,string,框架,C7.0,基础,DateTime,WriteLine,字符串 From: https://www.cnblogs.com/hihaojie/p/18639809/chapter-6-framework-basis-z1ggv44