Effective C#:改善C#代码的50个有效方法(原书第3版)
上QQ阅读APP看书,第一时间看更新

第4条:用内插字符串取代string.Format()

自从有了编程这门职业,开发者就需要把计算机里面所保存的信息转换成更便于人类阅读的格式。C#语言中的相关API可以追溯到几十年前所诞生的C语言,但是这些老的习惯现在应该改变,因为C#6.0提供了内插字符串(Interpolated String)这项新的功能可以用来更好地设置字符串的格式。

与设置字符串格式所用的旧办法相比,这项新功能有很多好处。开发者可以用它写出更容易阅读的代码,编译器也可以用它实现出更为完备的静态类型检查机制,从而降低程序出错的概率。此外,它还提供了更加丰富的语法,令你可以用更为合适的表达式来生成自己想要的字符串。

String.Format()函数虽然可以运作,但是会导致一些问题,开发者必须对生成的字符串进行测试及验证,才有可能发现这些问题。所有的替换操作都是根据格式字符串里面的序号来完成的,而编译器又不会去验证格式字符串后面的参数个数与有待替换的序号数量是否相等。如果两者不等,那么程序在运行的时候就会抛出异常。

还有一个更为隐晦的问题:格式字符串中的序号与params数组中的位置相对应,而阅读代码的人却不太容易看出来数组中的那些字符串是不是按照正确顺序排列的。必须运行代码,并仔细检查程序所生成的字符串,才能够确认这一点。

这些困难当然都是可以克服的,但会花费较多的时间,因此,不妨改用C#语言所提供的新特性来简化编写代码工作。这项新特性指的就是内插字符串。

内插字符串以$开头,它不像传统的格式字符串那样把序号放在一对花括号里面,并用其指代params数组中的对应元素,而是可以直接在花括号里面编写C#表达式。这使得代码更便于阅读,因为开发者可以直接在字符串里面看到这些有待替换的内容分别对应于什么样的表达式。采用这种办法来生成字符串是很容易验证其结果的。由于表达式直接出现在字符串中而不用单独写在字符串后面,因此,每一个有待替换的部分都能与替换该部分所用的那条表达式对应起来,不会出现双方的总数量不相符的情况。此外,这种写法也使得开发者不太会把表达式之间的顺序写错。

这样的语法糖(syntactic sugar)是很好的。将这种新特性融入日常的编程工作之后,你就会看到内插字符串是多么强大了。

首先,还是谈谈可以嵌入花括号里的那些表达式在写法上有什么样的限制。

之所以把花括号里的代码叫作表达式而不泛称为语句,是因为不能使用if/else或while等控制流语句来做替换。如果需要根据控制流做替换,那么必须把这些逻辑写成方法,然后在内插字符串里面嵌入该方法的调用结果。

字符串内插机制是通过库代码来完成的,那些代码与当前的string.Format()类似(至于如何实现国际化,请参见本章第5条)。内插字符串会在必要的时候把变量从其他类型转为string类型。比方说,下面这个内插字符串就是如此:

由字符串内插操作所生成的代码会调用一个参数为params对象数组的格式化方法。Math.PI是double类型,而double是值类型,因此,必须将其自动转为Object才可以。这种转换需要执行装箱操作,如果刚才那行代码运行得很频繁,或是需要在短小的循环中反复执行,那么就会严重影响性能(关于这个问题,请参见本章第9条)。这种情况下,开发者应该自己去把它转换成字符串,这样就不用给表达式中的数值装箱了:

如果ToString()直接返回的文本不符合你的要求,那么可以修改其参数,以创建你想要的文本:

制作字符串的时候,可能还需要对该字符串做一些处理,或是把表达式所返回的对象加以格式化。下面来看看怎样在内插字符串里面使用标准的格式说明符(也就是C#语言内建的说明符)来调整字符串的格式。要实现该功能,只需在大括号中的表达式后面加上冒号,并将格式说明符写在右侧。

警觉的读者可能会发现,由于条件表达式也使用冒号,因此,如果在内插字符串里面用冒号,那么C#可能会把它理解成格式说明符的前导字符,而不将其视为条件表达式的一部分。比方说,下面这行代码可能无法编译:

这个问题很好解决,只需迫使编译器将其理解为条件表达式即可。将整个内容括起来之后,编译器就不会再把冒号视为格式字符串的前一个字符了:

字符串内插机制为C#语言带来了很多强大的功能。只要是有效的C#表达式,就可以出现在这种字符串里面。刚才大家看到了怎样把变量和条件表达式放进去,其实这只是其中的一小部分功能,除此之外,还可以通过null合并运算符(null-coalescing operator)与null条件运算符(null-conditional operator,也称为null propagation operator(null传播运算符))来更为清晰地处理那些可能缺失的值:

通过这个例子可以看到,花括号里面还可以嵌入字符串,凡是位于{和}之间的字符,就都会被当成这条表达式中的C#代码加以解析。(冒号例外,它用来表示其右侧的内容是格式说明符。)

这是个很好的特性,深入研究之后,你就会发现它实在是太奇妙了。例如在内插字符串里面还可以继续编写内插字符串。合理运用这种写法可以极大地简化编程工作。比方说,下面这种写法就能够在可以找到记录的情况下把这条记录中的信息显示出来,并在找不到记录的情况下打印出与之相应的序号:

如果要找的这条记录不存在,那么就会执行条件表达式的false部分,从而令那个小的内插字符串生效,该字符串会返回一条消息,并在其中指出要查的是哪个位置上的记录。

在内插字符串里面,还可以使用LINQ查询操作来创建内容,而且这种查询操作本身也可以利用内插字符串来调整查询结果所具备的格式:

上面这种写法可能不太会用在正式的产品代码中,但是由此可以看出,内插字符串与C#语言之间结合得相当密切。ASP.NET MVC框架中的Razor View引擎也支持内插字符串,这使得开发者在编写Web应用程序时能够更便捷地以HTML的形式来输出信息。默认的MVC应用程序本身就演示了怎样在Razor View中使用内插字符串。下面这个例子节选自controller部分,它可以显示当前登入的用户名:

构建应用程序中的其他HTML页面时,也可以采用这个技巧来更为精确地表达你想要输出的内容。

上面这些例子展示了内插字符串所具备的强大功能,这些功能虽然也可以用传统的格式化字符串来实现,但是却比较麻烦。值得注意的地方在于,内插字符串本身其实也会解析成一条普通的字符串,因为把其中有待填写的那些部分填好之后,它就和其他字符串没有区别了。如果某个字符串是用来创建SQL命令的,那么尤其要注意这一点,因为内插字符串并不会创建出参数化的SQL查询(parameterized SQL query),而只会形成一个普通的string对象,那些参数值全都已经写入该string中了。由此可见,用内插字符串创建SQL命令是极其危险的。其实不只是SQL命令,凡是需要留到运行的时候再去解读的信息就都有这个风险,开发者需要特别小心才是。

把计算机内部所用的表示形式转换成便于我们阅读的形式,这在很多年前就已经是程序开发中的常见任务了,而当前的许多编程语言里面依然留有C语言诞生时所引入的那套旧方法,那些方法会导致很多潜在的错误,而内插字符串这项新的特性则不太会出现这种错误。因此,在当前的编程工作中,应该多用这种功能强大且简单易行的写法。