正确操作C#字符串

本文是《编写高质量代码改善C#程序的157个建议》第一章基本语言要素之建议1正确操作字符串,喜欢本书请到各大商城购买原书,支持正版。

第一章序

如何操作字符串?如何进行转型?什么是克隆?什么是相等型?为什么需要HashCode?当我们真正开始使用一门语言进行编程时,就会遇到这些问题。这些问题看起来简单,可是我们是否想过:为什么要这样处理,这样做是最好的吗?本章的内容将以这些最基础的要素开篇,给出C#编码中的一些最佳实践,让我们在C#进阶学习的过程中始终朝着正确的方向前进。

建议1:正确操作字符串

字符串应该是所有编程语言中使用最频繁的一种基础数据类型。如果使用不慎,我们就会为一次字符串的操作所带来的额外性能开销而付出代价。本条建议将从两个方面来探讨如何规避这类性能开销:

  • 确保尽量少的装箱
  • 避免分配额外的内存空间

先来介绍第一个方面,请看下面的两行代码:

String str1 = "str1" + 9;
String str2 = "str2" + 9.ToString();

为了清楚这行代码的执行情况,我们来比较两者生成的IL代码。

这里先说明下怎么看IL代码,应该有人不知道怎么看:

查看IL代码

  1. 打开vs 2019命令行

开始菜单打开vs 2019命令行,截图如下:

打开命令行
打开命令行
  1. 打开IL DASM窗口
打开IL DASM窗口
打开IL DASM窗口
  1. 查看IL代码

这里有两种方式,一是菜单打开目标程序:文件=》打开,二是直接将文件拖入IL DASM窗口

拖入目标程序,查看IL代码
拖入目标程序,查看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代码对比

原书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:使用默认转型方法

原文出处:微信公众号【小市民在西河】

原文链接:https://mp.weixin.qq.com/s/60DYqKhGVINZ27rAv_cD1w

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论