- C# 8.0本质论
- (美)马克·米凯利斯
- 5014字
- 2022-03-30 10:15:07
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=1739121840-RAy1Lzo1tRRBCXGVw80C2BmsJj4UTXJe-0-62117f6fa071f15f2ae7c532e25aed7e)
本例在不区分大小写的前提下比较变量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=1739121840-rTygjXPElnk8KW7CTnlXKOyV591lRyrz-0-5a83ee6cc1174ec4d6da8b8349570b5e)
表2.4总结了转义序列以及字符的Unicode编码。
表2.4 转义字符
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/2b4.jpg?sign=1739121840-dosnEdqWdHZRXPRS4UO0SOcSopW1snwo-0-56b35418f38e5ab559e7f757090878b1)
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/2b4x.jpg?sign=1739121840-0dcJLqOkCDZxd54S4R1HwHmNoy7MyZ1N-0-cf2ba193ab3b6b0d5026622c683b92a7)
可用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=1739121840-bA3x1FUPP1k8YSy9gy0QKGY3QYdakmXj-0-e7c2f3f1c29e35286c9368f95b37ba21)
输出2.8
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.8.jpg?sign=1739121840-bBZj584uMK2XptRXwy3XnH5I2Te3LmzJ-0-415e5afdc48df155d69f804610f8bfb9)
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=1739121840-21hlj4vexKcRbm2fMqITnNte0NZh2JWO-0-886e3ba069f76ac0be4b3b652dbf62d7)
输出2.9
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.9.jpg?sign=1739121840-lktF1RzCDi7kWtA8rAC46ckR9eNp3sQi-0-bbfd8f65f30a5a433067562fc635f04e)
双引号要用转义序列输出,否则会被用于定义字符串开始与结束。
C#允许在字符串前使用@符号,指明转义序列不被处理。结果是一个逐字字符串字面值(verbatim string literal),它不仅将反斜杠当作普通字符,还会逐字解释所有空白字符。例如,代码清单2.14的三角形会在控制台上原样输出,其中包括反斜杠、换行符和缩进。输出2.10展示了结果。
不使用@字符,这些代码甚至无法通过编译。事实上,即便将形状变成正方形,避免使用反斜杠,代码仍然不能通过编译,因为不能将换行符直接插入不以@符号开头的字符串中。
代码清单2.14 使用逐字字符串字面值来显示三角形
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.14.jpg?sign=1739121840-hUHoAjj7joPZnbo6umru9MgfOcGWbRAC-0-6497af37aacbca1bed42db1f53ac7ee7)
输出2.10
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.10.jpg?sign=1739121840-WtNLYyFZXBZgVW4daaOYCPmHCuRn9e1A-0-338def80fbfd39906ed0491dafd3f80a)
以@开头的字符串唯一支持的转义序列是"",代表一个双引号,不会终止字符串。
语言对比:C++——在编译时连接字符串
和C++不同,C#不自动连接字符串字面值。例如,不能像下面这样指定字符串字面值:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/061-i.jpg?sign=1739121840-rRfrGU1xj4goj40PHxZCjhBvk20dMm7O-0-de226b691f09b20a840fc43a5cef33be)
必须用+操作符连接(但如果编译器能在编译时计算结果,最终的CIL代码将包含连接好的字符串)。
假如同一字符串字面值在程序集中多次出现,编译器在程序集中只定义字符串一次,且所有变量都指向它。这样一来,假如在代码中多处插入包含大量字符的同一个字符串字面值,最终的程序集只反映其中一个的大小。
字符串插值
如第1章所述,从C# 6.0起,字符串可用插值技术嵌入表达式。语法是在字符串前添加$符号,并在字符串中用一对大括号嵌入表达式。例如:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/062-i.jpg?sign=1739121840-K8T856NrYDC9ewpKnqnlaxT8Zr4de7DO-0-ed97c747794b69dcadba944a576d8cc3)
其中,firstName和lastName是引用了变量的简单表达式。
注意逐字和插值可组合使用,但要先指定$,再指定@(或者在C# 8.0开头的@$"..."),例如:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/062-2-i.jpg?sign=1739121840-b9W5G7YKQeL3CtsSPvfnyE5QdmpwOTf8-0-e12f1b01c771b14d4fb462879d1fed6e)
由于是逐字字符串,所以按字符串的样子分两行输出。在大括号中换行则起不到换行效果:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/062-3-i.jpg?sign=1739121840-mNoX8RKHJZgp8yCqQdahaTA19ULBnUb6-0-0d5628392247b2e343bddb7cfb45db0a)
上述代码在一行中输出字符串内容。注意此时仍需@符号,否则无法编译。
高级主题:理解字符串插值的内部工作原理
字符串插值是调用string.Format()方法的语法糖。例如以下语句:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/062-4-i.jpg?sign=1739121840-b8eKqtsaCcPqFPe2LYaCck17KGjhkqsC-0-1b7241f1168c9d1fffc2c615212aa5d5)
会被转换成以下形式的C#代码:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/062-5-i.jpg?sign=1739121840-DLAynNdyrc3BXWoOYfnbOud7c2XzaF06-0-0525ad7f2bd48a585b5ef43dc87c7c78)
这就和复合字符串一样实现了某种程度的本地化支持,而且不会因为字符串造成编译后代码注入。
字符串方法
和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=1739121840-vTi2WPU3jTkOImFEhvBN5UY8ZDqB8lGc-0-7a09afeb3a49e5afc87399f23def3508)
表2.5列出的都是静态方法。这意味着为了调用方法,需在方法名(例如Concate)之前附加方法所在类型的名称(例如string)。但string类还有一些实例方法。实例方法不以类型名作为前缀,而是以变量名(或者对实例的其他引用)作为前缀。表2.6列出了部分实例方法和例子。
表2.6 string的实例方法
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/2b6.jpg?sign=1739121840-tF1gnne6kdFejLkFh3ObGJgiW8Uo9pfm-0-1f334bd1cf61cfc50d8c666d447b4efa)
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/2b6x.jpg?sign=1739121840-CI8IF1HuxCN07fPOZyVq2qrXPCm07Esn-0-4142facf4ded474bed11cd57ecb49851)
高级主题: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=1739121840-qyo2UJTfZ9NMRYUd6qHwYQ3MZcvxhaU3-0-ae079a1b09025ff16e33e4d3bfc094ad)
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=1739121840-tc8zNetQciUcoYZB9YZdubyacf6OULer-0-bf4237c723cf01734904550f995c0076)
输出2.11
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.11.jpg?sign=1739121840-g03acWfCmLaQg9QQ8WWgKV7exGgNw6jH-0-b5166bd098ecb38bf186f841fe0dddea)
字符串长度不能直接设置,它是根据字符串中的字符数计算得到的。此外,字符串长度不能更改,因为字符串不可变。
字符串不可变
string类型的一个关键特征是它不可变(immutable)。可为string变量赋一个全新的值,但出于性能考虑,没有提供修改现有字符串内容的机制。所以,不可能在同一个内存位置将字符串中的字母全部转换为大写。只能在其他内存位置新建字符串,让它成为旧字符串大写字母版本,旧字符串在这个过程中不会被修改。代码清单2.17展示了一个例子。
代码清单2.17 错误,string不可变
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.17.jpg?sign=1739121840-zc6Vmj7oVyDOWVIPEAgclaKU6GpHPvS1-0-ac45f705e1cb8c0b3afd271d58903a05)
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/d2.17x.jpg?sign=1739121840-MhHnJM70Mhn4ZcslVYglVa5heKMqr3rh-0-26c5d3ecb9f718e0fd08254092d8d502)
输出2.12展示了结果。
输出2.12
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.12.jpg?sign=1739121840-wJdLcCrKegVzLYbziUKEP7WnE3xWUshS-0-b61c04c41a936d8f2c6c00fe17d68260)
从表面上看,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=1739121840-LEguoiyMLTlqfN9fyT1MvDpduSLfPT9T-0-b6f170f73f2d196005e4c885e563c1dc)
输出2.13
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/s2.13.jpg?sign=1739121840-D4b4donMyQ7RaFe5nbmQXeNiEZS7GIsU-0-5be6a5b7432083b2017a6d984b3ac386)
如忘记字符串不可变的特点,很容易会在使用其他字符串方法时犯下和代码清单2.17相似的错误。
要真正更改text中的值,将ToUpper()的返回值赋回给text即可。如下例所示:
![](https://epubservercos.yuewen.com/7885FF/22815793809130806/epubprivate/OEBPS/Images/067-6-i.jpg?sign=1739121840-mVWxdyVdDGVC1LrbTFlxCtkCXKpeCSrH-0-6b86fd8b90aa2dee5f634d66b26a740e)
如有大量字符串需要修改,比如要经历多个步骤来构造一个长字符串,可考虑使用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=1739121840-gF8Col8gX68dQmS9awN40YO5T7dUkFQj-0-19d0a7626c16e47b0e65ea88d9f43017)
到目前为止,我们已经介绍了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译作“可空引用类型”。——译者注