![C# 8.0本质论](https://wfqqreader-1252317822.image.myqcloud.com/cover/306/43475306/b_43475306.jpg)
2.2 更多基本类型
迄今为止只讨论了基本数值类型。C#还包括其他一些类型:bool、char和string。
2.2.1 布尔类型
另一个C#基元类型是布尔(Boolean)或条件类型bool。它在条件语句和表达式中表示真或假。允许的值包括关键字true和false。bool的BCL名称是System.Boolean。例如,为了在不区分大小写的前提下比较两个字符串,可以调用string.Compare()方法并传递bool字面值true,如代码清单2.10所示。
代码清单2.10 不区分大小写比较两个字符串
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.10.jpg?sign=1739130491-u4hCtUJgJoAZxewjY5F2ekRaTsOe1ut9-0-9f5a13878ca152c9f78c437d4dd0c2b4)
本例在不区分大小写的前提下比较变量option的内容和字面值/Help,结果赋给comparison。
虽然理论上一个二进制位足以容纳一个布尔类型的值,但bool实际大小是一个字节。
2.2.2 字符类型
字符类型char表示16位字符,取值范围对应于Unicode字符集。从技术上说,char的大小和16位无符号整数(ushort)相同,后者取值范围是0~65 535。但char是C#的特有类型,在代码中要单独对待。
char的BCL名称是System.Char。
初学者主题:Unicode标准
Unicode是一个国际性标准,用来表示大多数语言中的字符。它使得计算机系统可以构建本地化应用程序,更加方便地显示不同语言文化的语言和特色字符。
高级主题:16位不足以表示所有Unicode字符
令人遗憾的是,不是所有Unicode字符都能用一个16位char表示。刚开始提出Unicode的概念时,它的设计者以为16位已经足够。但随着支持的语言越来越多,才发现当初的假定是错误的。结果是,一些Unicode字符要由一对称为“代理项”的char构成,总共32位。
输入char字面值需要将字符放到一对单引号中,比如'A'。所有键盘字符都可这样输入,包括字母、数字以及特殊符号。
有的字符不能直接插入源代码,需进行特殊处理。首先输入反斜杠(\)前缀,再跟随一个特殊字符代码。反斜杠和特殊字符代码统称为转义序列(escape sequence)。例如,\n代表换行符,而\t代表制表符。由于反斜杠标志转义序列开始,所以要用\\表示反斜杠字符。
代码清单2.11输出用\'表示的一个单引号。
代码清单2.11 使用转义序列显示单引号
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.11.jpg?sign=1739130491-PleFEAlltMRdDRw60tIKVqyiAJGCNjiT-0-62578e65bff3764a3e0b4bf99894471d)
表2.4总结了转义序列以及字符的Unicode编码。
表2.4 转义字符
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/2b4.jpg?sign=1739130491-tEtnOmRMpaAKWkKLs85v8YYrAo1XxtRj-0-87d43e151803f4110f7ab39e1703ba7f)
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/2b4x.jpg?sign=1739130491-ksebCNAWBkZdt0Q2TByHhCUbZqW4qb71-0-31f52fb1f57243f9b80c942f4180fed3)
可用Unicode编码表示任何字符。为此,请为Unicode值附加\u前缀。可用十六进制记数法表示Unicode字符。例如,字母A的十六进制值是0x41,代码清单2.12使用Unicode字符显示笑脸符号(:)),输出2.8展示了结果。
代码清单2.12 使用Unicode编码显示笑脸符号
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.12.jpg?sign=1739130491-Antig5WM3sIaIMZD8aJlTmb1GNvqHBLm-0-35f6a79877e8abf9cfdfce41d39df2ec)
输出2.8
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.8.jpg?sign=1739130491-YXE5hUaFiHerlkrIpFGPd74EqEGqIiYw-0-946a48193f121584be6ab50ca83ccf08)
2.2.3 字符串
零或多个字符的有限序列称为字符串。C#的基本字符串类型是string,BCL名称是System.String。对于已熟悉了其他语言的开发者,string的一些特点或许会出人意料。除了第1章讨论的字符串字面值格式,还允许使用逐字前缀@,允许用$前缀进行字符串插值。最后,string是一种“不可变”类型。
字面值
为了将字面值字符串输入代码,要将文本放入双引号(")内,就像HelloWorld程序中那样。字符串由字符构成,所以转义序列可嵌入字符串内。
例如,代码清单2.13显示两行文本。但这里没有使用System.Console.WriteLine(),而是使用System.Console.Write()来输出换行符\n。输出2.9展示了结果。
代码清单2.13 用字符\n插入换行符
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.13.jpg?sign=1739130491-faqr9QOT1QQRMCrGT8OrFQXwV7OceLom-0-c070d612987a2f11e7389ba5c172036e)
输出2.9
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.9.jpg?sign=1739130491-JlbYENmTACaLOYBDmjuzn7Bnes2sA9gX-0-324b0c5d679bb33d10b96d3ade516ac7)
双引号要用转义序列输出,否则会被用于定义字符串开始与结束。
C#允许在字符串前使用@符号,指明转义序列不被处理。结果是一个逐字字符串字面值(verbatim string literal),它不仅将反斜杠当作普通字符,还会逐字解释所有空白字符。例如,代码清单2.14的三角形会在控制台上原样输出,其中包括反斜杠、换行符和缩进。输出2.10展示了结果。
不使用@字符,这些代码甚至无法通过编译。事实上,即便将形状变成正方形,避免使用反斜杠,代码仍然不能通过编译,因为不能将换行符直接插入不以@符号开头的字符串中。
代码清单2.14 使用逐字字符串字面值来显示三角形
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.14.jpg?sign=1739130491-2mHRnUAAA7zsViwHsot3E10gs2wBsNa3-0-13de0262eb2ef4e14d20b86a769319f6)
输出2.10
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.10.jpg?sign=1739130491-YNRj3PpbL2T3KuOpr7jfM1jgitoZBtVa-0-17b9a746479a6d0c973d2bd66a88773b)
以@开头的字符串唯一支持的转义序列是"",代表一个双引号,不会终止字符串。
语言对比:C++——在编译时连接字符串
和C++不同,C#不自动连接字符串字面值。例如,不能像下面这样指定字符串字面值:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/061-i.jpg?sign=1739130491-tiGuwzm8q2vGlWvzvryuVvBkaFPYcCx3-0-20e9b74afc92b0545d1c46a35f2b20f3)
必须用+操作符连接(但如果编译器能在编译时计算结果,最终的CIL代码将包含连接好的字符串)。
假如同一字符串字面值在程序集中多次出现,编译器在程序集中只定义字符串一次,且所有变量都指向它。这样一来,假如在代码中多处插入包含大量字符的同一个字符串字面值,最终的程序集只反映其中一个的大小。
字符串插值
如第1章所述,从C# 6.0起,字符串可用插值技术嵌入表达式。语法是在字符串前添加$符号,并在字符串中用一对大括号嵌入表达式。例如:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/062-i.jpg?sign=1739130491-HFhE6EQX3gzgk137pR3S0gvI1Ko2iG6E-0-c05553596a19bfd1ee962ff0d16a3368)
其中,firstName和lastName是引用了变量的简单表达式。
注意逐字和插值可组合使用,但要先指定$,再指定@(或者在C# 8.0开头的@$"..."),例如:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/062-2-i.jpg?sign=1739130491-kO6gk2ZE2f3x3jIEdj59GiNQxmoUUYEG-0-313c9c4a4a61f4c186858fdfe7ec9274)
由于是逐字字符串,所以按字符串的样子分两行输出。在大括号中换行则起不到换行效果:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/062-3-i.jpg?sign=1739130491-NSHfrIUAEr5LUW9FTKoMhIXBYwANe6Ab-0-056ca5fe1b68d1dcef9e2d216fb21da0)
上述代码在一行中输出字符串内容。注意此时仍需@符号,否则无法编译。
高级主题:理解字符串插值的内部工作原理
字符串插值是调用string.Format()方法的语法糖。例如以下语句:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/062-4-i.jpg?sign=1739130491-25nMSLL55FDB9uI2pIUv6Ey4077sQNfk-0-d341f2b2a7527e8f564c770b5349ae00)
会被转换成以下形式的C#代码:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/062-5-i.jpg?sign=1739130491-FlojQuGbKt9lovgBFvvGRqLI0HrEQI0V-0-95fd9df1538d92b1b4d0b3f1af8d7ba5)
这就和复合字符串一样实现了某种程度的本地化支持,而且不会因为字符串造成编译后代码注入。
字符串方法
和System.Console类型相似,string类型也提供了几个方法来格式化、连接和比较字符串。
表2.5中的Format()方法具有与Console.Write()和Console.WriteLine()方法相似的行为。区别在于,string.Format()不是在控制台窗口中显示结果,而是返回结果。当然,有了字符串插值后,用到string.Format()的机会减少了很多(本地化时还是用得着)。但在幕后,字符串插值编译成CIL后都会转换为调用string.Concat()和string.Format()来处理字符串字面值。
表2.5 string的静态方法
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/2b5.jpg?sign=1739130491-gc43z3iNx6gbVkFzHzma2B2f4xu1mWt3-0-ef03d0feba0fd9b82cbc5829311dcfcc)
表2.5列出的都是静态方法。这意味着为了调用方法,需在方法名(例如Concate)之前附加方法所在类型的名称(例如string)。但string类还有一些实例方法。实例方法不以类型名作为前缀,而是以变量名(或者对实例的其他引用)作为前缀。表2.6列出了部分实例方法和例子。
表2.6 string的实例方法
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/2b6.jpg?sign=1739130491-86win7j8vODlble23Iw4CN8VTlpUnf5D-0-78a3175763ea4439a74ef873345b241a)
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/2b6x.jpg?sign=1739130491-6HQYzkHFON6D2ow0Mn5g7dEk4rWNQ1hs-0-80c7de90e4ec52708927115ac6bb4dff)
高级主题:using和using static指令
之前调用静态方法需附加命名空间和类型名前缀。例如在调用System.Console.WriteLine时,虽然调用的方法是WriteLine(),且当前上下文无其他同名方法,但仍然必须附加命名空间(System)和类型名(Console)前缀。可利用C# 6.0新增的using static指令避免这些前缀,如代码清单2.15所示。
代码清单2.15 using static指令
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.15.jpg?sign=1739130491-FgRyUgi08MHABpg8BMLrSkQSIVCGLlvf-0-9e0ac03d07d4bc8a64bea83eabcbc69a)
using static指令需添加到文件顶部[1]。每次使用System.Console类的成员,都不再需要添加System.Console前缀。相反,直接使用其中的方法名即可。注意该指令只支持静态方法和属性,不支持实例成员。
类似地,using指令用于省略命名空间前缀(例如System)。和using static不同,using作用于它所在的整个文件(或命名空间),而非仅作用于静态成员。使用using指令,不管在实例化时,在调用静态方法时,还是在使用C# 6.0新增的nameof操作符时,都可省略对命名空间的引用。
字符串格式化
无论使用string.Format()还是C# 6.0字符串插值来构造复杂格式的字符串,都可以通过一组覆盖面广且复杂的格式化模式组合来显示数字、日期、时间、时间段等。例如,给定decimal类型的price变量,则string.Format("{0,20:C2}", price)和等价的插值字符串$"{price,20:C2}"都使用默认的货币格式化规则将decimal值转换成字符串。即添加本地货币符号,小数点后四舍五入保留两位,整个字符串在20个字符的宽度内右对齐。因篇幅有限,无法详细讨论所有可能的格式字符串,请在MSDN文档中查阅“composite formatting”(组合格式化)(http://itl.tc/CompositeFormatting)获取字符串格式化的完整说明。
要在插值或格式化的字符串中添加实际的左右大括号,可连写两个大括号来表示。例如,插值字符串$"{{{price:C2}}}"可生成字符串"{$1,234.56}"。
换行符
输出换行所需的字符由操作系统决定。Microsoft Windows的换行符是\r和\n这两个字符的组合,UNIX则是单个\n。为消除平台之间的不一致,一个办法是使用System.Console.WriteLine()自动输出空行。为确保跨平台兼容性,可用System.Environment.NewLine代表换行符。换言之,System.Console.WriteLine("Hello World")和System.Console.Write("Hello World"+System.Environment.NewLine)等价。注意在Windows上,System.WriteLine()和System.Console.Write(System.Environment.NewLine)等价于System.Console.Write("\r\n")而非System.Console.Write("\n")。总之,要依赖System.WriteLine()和System.Environment.NewLine而不是\n来确保跨平台兼容。
设计规范
·要依赖System.WriteLine()和System.Environment.NewLine而不是\n来确保跨平台兼容。
高级主题:C#属性
下一节提到的Length成员实际不是方法,因为调用时没有使用圆括号。Length是string的属性(property),C#语法允许像访问成员变量(在C#中称为字段)那样访问属性。换言之,属性定义了称为赋值方法(setter)和取值方法(getter)的特殊方法,但用字段语法访问那些方法。
研究属性的底层CIL实现,发现它编译成两个方法:set_<PropertyName>和get_<PropertyName>。但这两个方法不能直接从C#代码中访问,只能通过C#属性构造来访问。第6章更详细地讨论了属性。
字符串长度
判断字符串长度可以使用string的Length成员。该成员是只读属性。不能设置,调用时也不需要任何参数。代码清单2.16演示了如何使用Length属性,输出2.11是结果。
代码清单2.16 使用string的Length成员
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.16.jpg?sign=1739130491-rMDcwZNrKtabJLfBAOAl7PKEdNB4ntEY-0-468113750cce76c04eb0a5996c746a65)
输出2.11
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.11.jpg?sign=1739130491-40zdCOKzLtpTiQQKZf201wl2qp3zGUDr-0-f8fef541d2b4e17f600aaf55573928b5)
字符串长度不能直接设置,它是根据字符串中的字符数计算得到的。此外,字符串长度不能更改,因为字符串不可变。
字符串不可变
string类型的一个关键特征是它不可变(immutable)。可为string变量赋一个全新的值,但出于性能考虑,没有提供修改现有字符串内容的机制。所以,不可能在同一个内存位置将字符串中的字母全部转换为大写。只能在其他内存位置新建字符串,让它成为旧字符串大写字母版本,旧字符串在这个过程中不会被修改。代码清单2.17展示了一个例子。
代码清单2.17 错误,string不可变
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.17.jpg?sign=1739130491-gvPP5ajEtxxrupqQ2u3gTomwFecfWu0s-0-2f4ce7f085c76e0627c6a3da4ca39795)
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.17x.jpg?sign=1739130491-YcpvgUlYmlP1RkE9YSWo4hD2BDsLaG8u-0-28d620000e166b6fb3fc7423cfdd0f3f)
输出2.12展示了结果。
输出2.12
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.12.jpg?sign=1739130491-Dt2UZtF2SntH1rcJH6Y5kigtgw35RqIO-0-50d1c8e09d3233822d428ba4d08c13b7)
从表面上看,text.ToUpper()似乎应该将text中的字符转换成大写。但由于string类型不可变,所以text.ToUpper()不会进行这样的修改。相反,text.ToUpper()会返回新字符串,它需要保存到变量中,或直接传给System.Console.WriteLine()。代码清单2.18给出了纠正后的代码,输出2.13是结果。
代码清单2.18 正确的字符串处理
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.18.jpg?sign=1739130491-5IZ5kbthp9TKWOxt69Skr7SeFVRBHTPo-0-f48d5f900da3bd2edbf730cbbd801542)
输出2.13
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.13.jpg?sign=1739130491-xOIcIgrYUE91DkwERJYiIH0SyVkztglP-0-ef3dd217fde26e41d3835c31d510b6e9)
如忘记字符串不可变的特点,很容易会在使用其他字符串方法时犯下和代码清单2.17相似的错误。
要真正更改text中的值,将ToUpper()的返回值赋回给text即可。如下例所示:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/067-6-i.jpg?sign=1739130491-p2kghWjjjmR0q7YSl9Sz5LX2BV8hXjsv-0-e5eb503bb2fae9e5929c674fb2877bd6)
如有大量字符串需要修改,比如要经历多个步骤来构造一个长字符串,可考虑使用System.Text.StringBuilder类型而不是string。StringBuilder包含Append()、AppendFormat()、Insert()、Remove()和Replace()等方法。虽然string也提供了其中一些方法,但两者关键的区别在于,在StringBuilder上,这些方法会修改StringBuilder本身中的数据,而不是返回新字符串。
2.2.4 null和void
与类型有关的另外两个关键字是null和void。null值表明变量不引用任何有效的对象[2]。void表示无类型,或者没有任何值。
null
null也可以用作“文字”的一种类型,表明变量为“空”,不指向任何位置。将一个变量设为null,会明确地将其设置为“空”(即不指向任何数据)。事实上,甚至可以检查引用是否为空。
将null赋给引用类型的变量和根本不赋值是不一样的概念。换言之,赋值了null的变量已设置,而未赋值的变量未设置。使用未赋值的变量会造成编译时错误。
将null值赋给string变量和为变量赋值""也是不一样的概念。null意味着变量无任何值,而""意味着变量有一个称为“空白字符串”的值。这种区分相当有用。例如,编程逻辑可将为null的homePhoneNumber解释成“家庭电话未知”,将为""的homePhoneNumber解释成“无家庭电话”。
高级主题:可空修饰符
声明一个变量时,在其名称后面加一个问号,则表示该变量可以被设置为null。这便是可空修饰符。代码清单2.19演示了使用可空修饰符声明一个整型变量,并为其设置null值。
代码清单2.19 将null赋给整型变量
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.19.jpg?sign=1739130491-nnliUGfWDOB11JqTc8b2b7Svvda1l27g-0-6767945c2558ffc97f01ef7f70667311)
到目前为止,我们已经介绍了C#里的很多数据类型。但是在C# 2.0引入可空修饰符之前,前面提到过的任何数据类型都无法被设置为null,唯一的例外是string。这是因为string是引用类型,而其他的都是值类型。关于值类型和引用类型的更多知识将在第3章详细讲述。
此外,在C# 8.0之前,可空修饰符不能用于引用类型(比如string)变量的声明中。这是因为引用类型变量默认可被赋值为null,所以可空修饰符对于引用类型变量是多余的。
高级主题:可空引用类型
在C# 8.0之前,因为引用类型变量默认可被赋值为null,所以那时没有“可空引用类型”的概念。然而从C# 8.0开始,这一默认行为变为了可配置行为。声明引用类型变量时可以使用可空修饰符,将变量声明为可空;或者不使用该修饰符,将变量默认地声明为不可复制为null。这样一来,在C# 8.0里便有了“可空引用类型”的概念。当这一概念被启用时,将没有可空修饰符的变量设置为null将会产生警告信息。
目前我们已经介绍过唯一引用类型为string。若要声明一个可空的string变量,可以使用类似“string? homeNumber=null;”的写法。
在C# 8.0或后续版本中,若要启用“可空引用类型”的概念,需要在声明可空引用类型变量之前的任意位置放置“#nullable enable”语句。
名为void的“类型”
有时C#语法要求指定数据类型但不传递任何数据。例如,假定方法无返回值,C#就允许在数据类型的位置放一个void关键字。HelloWorld程序(代码清单1.1)的Main方法声明就是一个例子。在返回类型的位置使用void意味着方法不返回任何数据,同时告诉编译器不要指望会有一个值。void本质上不是数据类型,它只是指出没有数据类型这一事实。
语言对比:C++
无论是C++还是C#,void都有两个含义:标记方法不返回任何数据,以及代表指向未知类型的存储位置的一个指针。C++程序经常使用void**这样的指针类型。C#也可用相同的语法表示指向未知类型的存储位置的指针。但这种用法在C#中比较罕见,一般仅在需要与非托管代码库进行互操作时才会用到。
语言对比:Visual Basic——返回void相当于定义子程序
在Visual Basic中,与C#的“返回void”等价的是定义子程序(Sub/End Sub)而非返回值的函数。
[1] 放在命名空间声明之前。
[2] 英文单词null的含义为“空”,因此本书将它的衍生词nullable译作“可空”,例如将nullable reference type译作“可空引用类型”。——译者注