正确操作C#字符串
正确操作C#字符串
本文是《编写高质量代码改善C#程序的157个建议》
第一章基本语言要素
之建议1正确操作字符串
,喜欢本书请到各大商城购买原书,支持正版。
❝第一章序
❝如何操作字符串?如何进行转型?什么是克隆?什么是相等型?为什么需要HashCode?当我们真正开始使用一门语言进行编程时,就会遇到这些问题。这些问题看起来简单,可是我们是否想过:为什么要这样处理,这样做是最好的吗?本章的内容将以这些最基础的要素开篇,给出C#编码中的一些最佳实践,让我们在C#进阶学习的过程中始终朝着正确的方向前进。
建议1:正确操作字符串
字符串应该是所有编程语言中使用最频繁的一种基础数据类型。如果使用不慎,我们就会为一次字符串的操作所带来的额外性能开销而付出代价。本条建议将从两个方面来探讨如何规避这类性能开销:
- 确保尽量少的装箱
- 避免分配额外的内存空间
先来介绍第一个方面,请看下面的两行代码:
String str1 = "str1" + 9;
String str2 = "str2" + 9.ToString();
为了清楚这行代码的执行情况,我们来比较两者生成的IL代码。
这里先说明下怎么看IL代码,应该有人不知道怎么看:
查看IL代码
- 打开vs 2019命令行
开始菜单打开vs 2019
命令行,截图如下:
- 打开IL DASM窗口
- 查看IL代码
这里有两种方式,一是菜单打开目标程序:文件=》打开,二是直接将文件拖入IL DASM窗口
拖入目标程序,查看IL代码将上面的IL代码贴出来分析哈。
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 44 (0x2c)
.maxstack 2
.locals init (string V_0,
string V_1,
int32 V_2)
IL_0000: nop
IL_0001: ldstr "str1"
IL_0006: ldc.i4.s 9
IL_0008: stloc.2
IL_0009: ldloca.s V_2
IL_000b: call instance string [System.Runtime]System.Int32::ToString()
IL_0010: call string [System.Runtime]System.String::Concat(string,
string)
IL_0015: stloc.0
IL_0016: ldstr "str2"
IL_001b: ldc.i4.s 9
IL_001d: stloc.2
IL_001e: ldloca.s V_2
IL_0020: call instance string [System.Runtime]System.Int32::ToString()
IL_0025: call string [System.Runtime]System.String::Concat(string,
string)
IL_002a: stloc.1
IL_002b: ret
} // end of method Program::Main
两行代码生成的IL代码几乎一模一样,我是用.NET 5生成了,我们使用.NET Framework生成试试:
.NET Framework 2.0|3.0|3.5|4.0|4.5
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 44 (0x2c)
.maxstack 2
.locals init ([0] string str1,
[1] string str2,
[2] int32 V_2)
IL_0000: nop
IL_0001: ldstr "str1"
IL_0006: ldc.i4.s 9
IL_0008: stloc.2
IL_0009: ldloca.s V_2
IL_000b: call instance string [mscorlib]System.Int32::ToString()
IL_0010: call string [mscorlib]System.String::Concat(string,
string)
IL_0015: stloc.0
IL_0016: ldstr "str2"
IL_001b: ldc.i4.s 9
IL_001d: stloc.2
IL_001e: ldloca.s V_2
IL_0020: call instance string [mscorlib]System.Int32::ToString()
IL_0025: call string [mscorlib]System.String::Concat(string,
string)
IL_002a: stloc.1
IL_002b: ret
} // end of method Program::Main
也是一样,怎么回事?和书上不一致呢?
原书IL代码(第2页末、第3页)
原书IL代码对比原书IL代码说明(第3页)
书中说明什么原因造成自己的IL代码和书中不同呢?
本号主猜测,未验证:
❝号主使用的vs 2019编译的程序,编译器可能对程序做了足够的优化;而原书是10年出版,当时应该是vs 2010(或者vs 2008)编译的,旧编译器优化工作没有2021年现在的编译器这么优秀...
这个建议我们暂时跳过,但本建议最后原作者推荐大家使用StringBuilder
,号主非常赞同:
❝
StringBuilder
并不会重新创建一个string
对象,它的效率源于预先以非托管的方式
分配内存。如果StringBuilder
没有先定义长度,则默认分配的长度为16
。当StringBuilder
字符长度小于等于16
时,StringBuilder
不会重新分配内存;当StringBuilder
字符长度大于16小于32时,StringBuilder
又会重新分配内存,使之成为16的倍数
。如果预先判断字符串的长度将大于16,则可以为其设定一个更加合适的长度(如32)。StringBuilder
重新分配内存时是按照上次的容量加倍
进行分配的。当然,我们需要注意,StringBuilder
指定的长度要合适,太小了,需要频繁分配内存;太大了,浪费空间。
微软还提供了另外一个方法来简化StringBuilder
的操作,即使用string.Format
方法。string.Format
方法在内部使用StringBuilder
进行字符串的格式化,如下面的代码所示:
private static void NewMethod11()
{
// 为了演示的需要,定义了4个变量
string a = "t";
string b = "e";
string c = "s";
string d = "t";
string e = string.Format("{0}{1}{2}{3}", a, b, c, d);
}
当然现在还有更简化的方式:string e = $"{a}{b}{c}{d}"
。
下一篇,我们读建议2:使用默认转型方法
。