善用StringBuilder提升字符串处理效率
我们必须开门见山地说,在相同字符串的许多操作上,使用StringBuilder类别会比使用一个String对象来得更有效率。
大家务必了解,System.String数据类型(或是说String对象)代表的是一种不变的字符串,也就是说,一旦您设定其值,您就不能更改它。如果您尝试要插入、删除或更改字符串的任何部分,唯一的方式就是去建立一个新的字符串。说得更专业点,每次字符串数据变更时,内存中该字符串原来的表示法就会被破坏掉,并建立内含新字符串数据的新表示法,此举会引发对内存的配置作业以及对内存的反配置作业。当然,这些作业都是在幕后完成的,因此真正的成本并不会立刻显现。配置与反配置内存会加重Common Language Runtime(CLR)里内存管理和内存回收的相关工作,所以绝对是要付出代价的,直接的影响,就是增了处理时间。这种情况在迅速地接连配置和反配置占有大块内存的字符串时尤其明显,就像在大量字符串串连时所发生的情况一样。虽然这种情形在单一用户环境里不会带来任何问题,但是在服务器的环境里使用时(比方说,在Web服务器上执行的ASP.NET® 应用程序中),却可能在效能与延展性上造成极为严重的问题。
以下面这一段程序代码而言(请参阅CH3_DemoForm017.vb之「使用String」按钮的Click事件处理例程),您知道在每一次的循环中,会发生多少次的字符串配置作业吗?答案是14。在此种写法中,「&」(或「+」)运算子会使得变量sXml所指向的字符串被破坏掉然后再重新建立。再次提醒您,字符串配置是很花时间的,而且随着字符串的增长,情况会越来越严重。这正是 .NET Framework为什么要提供StringBuilder类别的原因:
Dim nRep As Int32
Dim Reps As Int32 = Convert.ToInt32(numLoops.Value)
Dim Start, Finish As Double
Start = Microsoft.VisualBasic.DateAndTime.Timer
Dim sXml As String = ""
For nRep = 1 To Reps
sXml &= "<订单 订单号码=""" _
& nRep _
& """ 订货日期=""" _
& DateTime.Now.ToString() _
& """ 客户编号=""" _
& nRep _
& """ 产品编号=""" _
& nRep _
& """ 产品说明=""" _
& "此产品的 Id 是: " _
& nRep _
& """ 数量=""" _
& nRep _
& """/>"
Next nRep
sXml = "<订单 方法=""1"">" & sXml & "</订单>"
Finish = Microsoft.VisualBasic.DateAndTime.Timer
txtStringResult.Text = (Finish – Start).ToString
与System.String类别相较之下,System.Text.StringBuilder类别则会保留它自己的字符串缓冲区。在针对StringBuilder执行作业因而可能会改变字符串数据的长度时,StringBuilder会先检查缓冲区的大小是否足够容纳新的字符串数据。如果不够,则缓冲区的大小就会增加预先决定的数量。由于大幅降低内存配置操作的发生机率,因此自然能有效提升效能。
就以字符串串连作业而言,标准的串连方式会导致为每一项串连作业建立一个新字符串,但是StringBuilder却每次都使用相同的字符串缓冲区。基本上,当您只要修改字符串,但是不要建立新的字符串对象时,就应该使用System.Text命名空间中的StringBuilder类别。举例来说,当需要在循环中串连多个字符串时,使用StringBuilder类别将可具体提升效能。此外,StringBuilder类别还提供了非常有效率的Replace方法,它可以用来取代String.Replace。
基本上,StringBuilder方法在执行上会比标准串连方式优异的原因取决于几项因素,其中包括串连的数目、所要建置之字符串的大小、以及StringBuilder的缓冲区初始化参数是否设定得够好。请注意,在大多数的情况下,多估算一些缓冲区空间要比后来又不断加大来得更好。
以先前在循环中进行字符串串连作业的程序代码来说,如果使用StringBuilder将其改写如下(请参阅CH3_DemoForm017.vb之「使用StringBuilder」按钮的Click事件处理例程),将能够大幅提升执行速度,而且如果循环的数目愈多,提升的幅度愈明显:
Dim nRep As Int32
Dim Reps As Int32 = Convert.ToInt32(numLoops.Value)
Dim Start, Finish As Double
Start = Microsoft.VisualBasic.DateAndTime.Timer
Dim oSB As StringBuilder
' 确保StringBuilder的容量足以容纳最终的文字
oSB = New StringBuilder(Reps * 165)
oSB.Append("<订单 方法=""2"">")
For nRep = 1 To Reps
oSB.Append("<订单 订单号码=""")
oSB.Append(nRep)
oSB.Append(""" 订货日期=""")
oSB.Append(DateTime.Now.ToString())
oSB.Append(""" 客户编号=""")
oSB.Append(nRep)
oSB.Append(""" 产品编号=""")
oSB.Append(nRep)
oSB.Append(""" 产品说明=""")
oSB.Append("此产品的 Id 是: ")
oSB.Append(nRep)
oSB.Append(""" 数量=""")
oSB.Append(nRep)
oSB.Append("""/>")
Next nRep
oSB.Append("</订单>")
Finish = Microsoft.VisualBasic.DateAndTime.Timer
txtStringBuilderResult.Text = (Finish – Start).ToString
图表1
图表1所示者是程序范例CH3_DemoForm017.vb的执行画面,其主要目的在于让您比较先前所列之两种字符串串连作业写法(String vs StringBuilder)在执行速度上的差异。您可以先设定循环数,然后依序按一下「使用String」按钮与「使用StringBuilder」按钮,从「处理时间」文字方块中所列的数据可以发现,使用StringBuilder类别进行字符串串连作业的执行速度要比使用String类别快上好几十倍,可说是天壤之别。
请注意:
欲使用StringBuilder类别,请记得汇入命名空间System.Text。
不过我们也必须坦白地说,StringBuilder类别虽然好用,然而它也也非万能。事实上,并非所有的String类别方法都可以直接使用StringBuilder来完成,当您恰巧需要String类别所独有的方法时(例如:IndexOf),您必须将内容提取成一个文字符串,针对字符串执行您所需的操作,然后再继续使用StringBuilder。
图表2所示者是程序范例CH3_DemoForm018.vb的执行画面,它示范如何分别使用StringBuilder类别与String类别来完成同一项作业。图表3同时列出这两种写法的程序代码,以方便您比较之间的差异。本范例特别值得一提的地方是,由于我们要在单字“MVP”之后插入一个句点,因此必须先找到此单字,然后再于该单字之后插入一个句点。欲找到此单字,您必须使用IndexOf方法来加以搜寻,但是StringBuilder并未提供此方法,所以我们只好使用ToString方法来取得字符串,然后处理该字符串。
图表2
使用StringBuilder类别 |
使用String类别 |
' 示范如何使用StringBuilder类别。 |
' 同样的程序代码如果直接使用String物 |
图表3、String类别与StringBuilder类别使用比较
经由以上的说明,相信大家都对StringBuilder都已经抱持一个非常肯定的态度。
本文节录自《Visual Basic 2005程序开发与界面设计秘诀》一书