clsStringBuilder
类拼凑在一起。考虑到了VBA可能在“背后”执行的意外复制行为,我可以避免这种行为,或者对编码样式进行了纠正(或没有纠正)。Option Compare Database
Option Explicit
'******
'* v2 *
'******
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal dst As Long, ByVal src As Long, ByVal Length As Long)
Private Const DEFAULT_CAPACITY As Long = &H10
Private m_currLen As Long
Private m_stringBuffer() As Byte
Private Sub Class_Initialize()
ReDim m_stringBuffer(0 To (DEFAULT_CAPACITY * 2) - 1) 'Each unicode character is 2 bytes
End Sub
Public Function Append(strString As String) As clsStringBuilder
On Error GoTo derp
If m_currLen + LenB(strString) < UBound(m_stringBuffer) Then
CopyMemory VarPtr(m_stringBuffer(m_currLen)), StrPtr(strString), LenB(strString)
Else
If m_currLen + LenB(strString) < UBound(m_stringBuffer) * 2 Then
Expand
Else
Expand m_currLen + LenB(strString)
End If
CopyMemory VarPtr(m_stringBuffer(m_currLen)), StrPtr(strString), LenB(strString)
End If
m_currLen = m_currLen + LenB(strString)
Set Append = Me
Exit Function
derp:
Stop
Resume
End Function
Public Property Get Length() As Long
Length = m_currLen / 2
End Property
Public Property Get Capacity() As Long
Capacity = UBound(m_stringBuffer)
End Property
Private Sub Expand(Optional newSize As Long = 0)
If newSize <> 0 Then
ReDim Preserve m_stringBuffer(0 To newSize - 1)
Else
ReDim Preserve m_stringBuffer(0 To (UBound(m_stringBuffer) * 2) + 1)
End If
End Sub
Public Function toString() As String
toString = Mid(m_stringBuffer, 1, m_currLen / 2)
End Function
这里是一个测试:
Public Sub Main()
Dim sb As clsStringBuilder
Set sb = New clsStringBuilder
Dim strString As String
Dim i As Long
Dim StartTime As Double
'VBA String
StartTime = MicroTimer()
For i = 0 To 100000
strString = strString + "Hello World;"
Next
Debug.Print "The VBA String took: " & Round(MicroTimer - StartTime, 3) & " seconds"
'StringBuilder
StartTime = MicroTimer()
For i = 0 To 100000
sb.Append "Hello World;"
Next
Debug.Print "The Stringbuilder took: " & Round(MicroTimer - StartTime, 3) & " seconds"
'Are the strings the same?
Debug.Print StrComp(strString, sb.toString, vbBinaryCompare)
End Sub
这是Microsoft的
MicroTimer
函数,可以在这里找到:Private Declare Function getFrequency Lib "kernel32" _
Alias "QueryPerformanceFrequency" (cyFrequency As Currency) As Long
Private Declare Function getTickCount Lib "kernel32" _
Alias "QueryPerformanceCounter" (cyTickCount As Currency) As Long
Function MicroTimer() As Double
'
' Returns seconds.
Dim cyTicks1 As Currency
Static cyFrequency As Currency
'
MicroTimer = 0
' Get frequency.
If cyFrequency = 0 Then getFrequency cyFrequency
' Get ticks.
getTickCount cyTicks1
' Seconds
If cyFrequency Then MicroTimer = cyTicks1 / cyFrequency
End Function
#1 楼
我很喜欢这个棒极了。像.NETString
对象一样,VBA字符串是不可变的,这意味着在.NET中,当"the quick brown fox"
附加"jumps over"
然后"the lazy dog"
时,已经生成了4个字符串,因此第一个被复制了3次。因此,绝对欢迎任何VBA工具包都欢迎使用VBA StringBuilder
类!这是您在这里获得的一些严肃的代码。让我们看一下。
因此,您已将其称为
clsStringBuilder
类。我知道您来自哪里,但是没有“ cls”匈牙利前缀的真正原因-我将其删除,并将其称为StringBuilder
类。 > 不必理会。我知道版本控制本来是用VBA几乎不可能的,但是仍然不需要在注释中“版本化”代码。您实际上维护版本号吗?何必呢?只需将其删除,它就变得毫无用处。
十六进制
16
对吗?我认为使用十进制表示法会更清楚。实际上,这种容量可能会造成混淆,尤其是考虑到十六进制表示法时。 10
的字节数是多少?是字符吗?'******
'* v2 *
'******
啊,那么字符。调用常数
16
怎么样?不,太久了..我个人不喜欢YELLCASE,我只称它为&H10
,但是我看到其他人使用ALL CAPS作为常量-只要您一致,它就可以工作:) br /> 顺便说一句,您在这里的评论不错,但我不会麻烦指定“ unicode”字符;当IDE本身仅支持ANSI字符串时,它会造成混乱! br />实际上,由于
DEFAULT_CHARACTER_CAPACITY
是以字节为单位,因此我将其称为InitialCharacterCapacity
,以避免出现问号:Private Const DEFAULT_CAPACITY As Long = &H10
ReDim m_stringBuffer(0 To (DEFAULT_CAPACITY * 2) - 1) 'Each unicode character is 2 bytes
m_currLen
,真的吗?我没有比这更多的匈牙利语了!您还应该知道默认情况下会传递参数currentLength
-我将签名更改为:Public Property Get Length() As Long
Length = m_currLen / 2
End Property
错误处理不是最佳的-如果发生异常,您将为最终用户提供IDE,以帮助他们调试和调试代码!那不是生产准备就绪的: br />
Public Function Append(strString As String) As clsStringBuilder
您可能还想确保
m_stringBuffer
不会实际缩小缓冲区。我认为。 ;)最后,我不确定我为什么理解
stringBufffer
不遵循约定并像所有公共方法一样在buffer
中进行命名-currentLength
看起来更好。 >辛苦了!评论
\ $ \ begingroup \ $
感谢您的反馈-现在修复命名约定。对于成员变量,我经常遇到需要将它们设为私有并通过Get / Set / Let属性提供访问权限的情况。在这种情况下,我使用“ m_”阻止VBA大喊“检测到歧义名称:membername”。在这些情况下的最佳做法是什么?
\ $ \ endgroup \ $
–黑鹰
2014-10-22 20:36
\ $ \ begingroup \ $
我喜欢将我所有的私有字段填充为私有类型-这里是私有类型TStringBuilder,其中包含所有成员。然后该类只有1个字段,我将其称为,例如,将其私有化为TStringBuilder-这样就消除了名称冲突,并且我喜欢看到this.MemberName =公共属性中的值Let MemberName(ByVal value As Whatever)块;)
\ $ \ endgroup \ $
– Mathieu Guindon♦
14-10-22在20:39
\ $ \ begingroup \ $
有趣的...我喜欢这个:D我必须仔细考虑一下含义...
\ $ \ endgroup \ $
–黑鹰
14-10-22在20:54
\ $ \ begingroup \ $
也许只是我,但我喜欢YELLCASE常量...
\ $ \ endgroup \ $
–RubberDuck
2014年10月23日,1:13
#2 楼
您的StringBuilder
非常令人印象深刻:) ++符合Mat的建议,除了我在RubberDuck一边的YELLCASE;)我想我已经确定了潜在的内存溢出(
out of memory
)。可能几乎没有任何人会发生,但是,嘿...如果用另一个循环包装循环,则VBA运行时似乎无法赶上计数和释放引用的速度。 例如:
For j = 0 To 1000
Dim csb As New clsStringBuilder
StartTime = MicroTimer()
For i = 0 To 100000
csb.Append "Hello World;"
Next
Next
它将在某个时候停止在
StringBuilder
中,并导致derp
... AFAIC,您无能为力。 ..除了不允许像我这样的人测试您的代码; P jk! 我还有其他一些小事情: />
□多次计算得出相同的数字效率低下。如果需要在一个if-else / select情况下获得5次
out of memory
的值,请考虑将此数字存储在变量中。 □
Select Case
(在If-Else
中)应该比Ubound(arr)
快一点。 (您的Mid$()
)通过我的改进,总体速度似乎只快了一点-太微妙了吗? ;)
每个测试1000次
好吧,我刚刚将名称更改为
ToString()
,这就是我所做的: /> Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" _
(ByVal dst As Long, ByVal src As Long, ByVal Length As Long)
Private Const DEFAULT_CAPACITY As Long = 16
Private m_currLen As Long
Private m_stringBuffer() As Byte
Private Sub Class_Initialize()
ReDim m_stringBuffer(0 To (DEFAULT_CAPACITY + DEFAULT_CAPACITY) - 1) 'Each unicode character is 2 bytes
End Sub
Public Function Append(strString As String) As StringBuilder
On Error GoTo derp
Dim uBuffer As Long
uBuffer = UBound(m_stringBuffer)
Dim lengthB As Long
lengthB = LenB(strString)
Dim sPtr As Long
sPtr = StrPtr(strString)
Dim currLen As Long
currLen = m_currLen + lengthB
Select Case currLen
Case Is < uBuffer
CopyMemory VarPtr(m_stringBuffer(m_currLen)), sPtr, lengthB
Case Is < (uBuffer + uBuffer)
Expand
CopyMemory VarPtr(m_stringBuffer(m_currLen)), sPtr, lengthB
Case Else
Expand currLen
CopyMemory VarPtr(m_stringBuffer(m_currLen)), sPtr, lengthB
End Select
m_currLen = currLen
Set Append = Me
Exit Function
derp:
Stop
Resume
End Function
Public Property Get Length() As Long
Length = m_currLen * 0.5
End Property
Public Property Get Capacity() As Long
Capacity = UBound(m_stringBuffer)
End Property
Private Sub Expand(Optional newSize As Long = 0)
Select Case newSize
Case Is = 0
ReDim Preserve m_stringBuffer(0 To (UBound(m_stringBuffer) + UBound(m_stringBuffer)) + 1)
Case Else
ReDim Preserve m_stringBuffer(0 To newSize - 1)
End Select
End Sub
Public Function ToString() As String
ToString = Mid$(m_stringBuffer, 1, m_currLen * 0.5)
End Function
您可以使用
Mid()
玩更多一些,但我把它留在对...感到满意的状态。&
应该是+
,因此无需存储在变量IMO 评论
\ $ \ begingroup \ $
谢谢!现在解决这个问题...您使用什么来生成花式图表?
\ $ \ endgroup \ $
–黑鹰
14-10-23在17:43
\ $ \ begingroup \ $
你不能认真对待你的问题吗? :P Excel 2010 ...
\ $ \ endgroup \ $
–user28366
14-10-23在17:59
\ $ \ begingroup \ $
当然,是因为这些颜色不是“办公室粉彩”罐头而引起的
\ $ \ endgroup \ $
–黑鹰
14-10-23在18:01
\ $ \ begingroup \ $
@Blackhawk不用担心;)我敢猜想自己,我敢打赌我们都有我们最喜欢的RGB;)
\ $ \ endgroup \ $
–user28366
14-10-23在22:14
\ $ \ begingroup \ $
@ user28366您已经成为VBA的As New语法的受害者。在循环中使用Dim csb作为New StringBuilder不会在每次迭代中创建新实例,因此,您只能使用第一个实例,并且可以有效地添加“ Hello World;”。仅100m次。这是一个超过2GB的字符串(2,402,424,024字节),或者在内存用尽之前出现了长时间溢出。溢出问题与StringBuilder类无关,而与您的过度使用有关。开源VBE外接程序Rubberduck VBA(我为之贡献)会发现这种疏忽。
\ $ \ endgroup \ $
– ThunderFrame
17 Mar 29 '17 at 22:22
#3 楼
实际上不需要使用CopyMemory
,您可以使用数组简单地实现相同的目的。代码不仅更短,而且更快。Dim MyBuffer() As String
Dim MyCurrentIndex As Long
Dim MyMaxIndex As Long
Private Sub Class_Initialize()
MyCurrentIndex = 0
MyMaxIndex = 16
ReDim MyBuffer(1 To MyMaxIndex)
End Sub
'Appends the given Text to this StringBuilder
Public Sub Append(Text As String)
MyCurrentIndex = MyCurrentIndex + 1
If MyCurrentIndex > MyMaxIndex Then
MyMaxIndex = 2 * MyMaxIndex
ReDim Preserve MyBuffer(1 To MyMaxIndex)
End If
MyBuffer(MyCurrentIndex) = Text
End Sub
'Returns the text in this StringBuilder
'Optional Parameter: Separator (default vbNullString) used in joining components
Public Function ToString(Optional Separator As String = vbNullString) As String
If MyCurrentIndex > 0 Then
ReDim Preserve MyBuffer(1 To MyCurrentIndex)
MyMaxIndex = MyCurrentIndex
ToString = Join(MyBuffer, Separator)
End If
End Function
评论
\ $ \ begingroup \ $
您可能想添加一些解释,以及为什么和为什么这比OP发布的要好。还要注意,这个问题已经很老了(这里我们不在乎规范的答案,而是代码的实际审查),因此OP可能已经发展了。
\ $ \ endgroup \ $
–地狱
17年2月8日在10:32
\ $ \ begingroup \ $
@ Martin.Roller仅看代码,似乎MyCurrentIndex需要增加。是这样吗?? myCurrentIndex = myCurrentIndex + Len(文本)-1
\ $ \ endgroup \ $
– donPablo
17年5月2日在4:26
\ $ \ begingroup \ $
@donPablo:再看一遍或尝试单步执行代码。文本的长度无关紧要。
\ $ \ endgroup \ $
– Martin.Roller
17年5月15日在10:23
\ $ \ begingroup \ $
@Blackhawk:请注意,此处的速度仅取决于附加的数量,而不取决于附加文本的大小。
\ $ \ endgroup \ $
– Martin.Roller
17年5月15日在10:28
\ $ \ begingroup \ $
这显然是要使用的版本,只有一个没有32/64位可移植性问题。
\ $ \ endgroup \ $
–Patrick Honorez
18-11-15在13:53
#4 楼
我知道这是一个古老的问题,但是发现它的人可能会发现这个有价值的答案。我已经使用Excel 2013测试了StringBuilder的4个版本,最快的版本似乎是Blackhawk代码的优化版本(下面列出了优化代码)。下面的列表显示了一个示例,说明每个版本进行Blackhawk的测试所需的时间为1000次(总共100,000,000次附加)。新的优化版本标记为“新”,耗时略长于34秒。 Martin.Roller的基于数组的代码被标记为“ Ary”,耗时超过36秒,并且几乎与Blackhawk的代码相关。我已经运行了几次测试,Blackhawk的代码确实领先于Martin.Roller的代码。 Blackhawk的代码被标记为“旧”,耗时超过36秒。 “ Mid”版本是最后一个版本,它是我使用VBA的MID语句创建的一个版本,用于替换字符串的内容。在下面的代码中删除“ On Error”应该可以进一步提高速度,但是请确保调用代码所生成的字符串永远不会超过VBA可以处理的长度。新的StringBuilder需要: 34.396秒
Ary StringBuilder花费了:36.467秒
老StringBuilder花费了:36.605秒
Mid StringBuilder花费了:40.141秒
Blackhawk的StringBuilder的新优化版本:
Option Compare Text
Option Explicit
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal dst As Long, ByVal src As Long, ByVal Length As Long)
Private Const InitialCharCount As Long = 16
' Define members
Private mUBound As Long
Private mString() As Byte
Private Sub Class_Initialize()
Clear
End Sub
Public Sub Clear()
mUBound = -1
ReDim mString(0 To InitialCharCount * 2 - 1) 'Each unicode character is 2 bytes
End Sub
Public Function Append(value As String) As StringBuilder
Dim NewUBound As Long
Dim CapacityUBound As Long
On Error GoTo Failed
NewUBound = mUBound + LenB(value)
If NewUBound > UBound(mString) Then
CapacityUBound = UBound(mString) * 2 + 1
If NewUBound > CapacityUBound Then CapacityUBound = NewUBound * 2 + 1
ReDim Preserve mString(0 To CapacityUBound)
End If
CopyMemory VarPtr(mString(mUBound + 1)), StrPtr(value), LenB(value)
mUBound = NewUBound
Set Append = Me
Exit Function
Failed:
Stop
Resume
End Function
Public Property Get Length() As Long
Length = (mUBound + 1) / 2
End Property
Public Function ToString() As String
ToString = Mid(mString, 1, Length)
End Function
评论
\ $ \ begingroup \ $
感谢您这样做!本着问题的精神,您可以添加对所做优化的描述吗?这将帮助任何关注此答案的人了解什么变化导致加速。我不得不说,我最喜欢VBA的一件事是缺少标准库-您将学习如何将自己的所有东西都投放出去:
\ $ \ endgroup \ $
–黑鹰
18年8月20日在14:41
评论
嘿黑鹰(Blackhawk)-我已经对代码进行了一些调整,并将其上传到GitHub上,我不知道您是否看到了蒂姆·霍尔(Tim Hall)的VBA-Blocks,但我认为通过类似nuget的StringBuilder类来使用真棒VBA的软件包管理器(Rubberduck最终可能会为其设计UI,因此可以在VBE中添加/更新/维护项目的软件包)。如果您可以联系Tim同意我对您的代码所做的事情(或者如果您愿意,我可以删除该存储库),那就太好了!@MathieuGuindon太棒了!如果您需要我戳别人,请让我知道,否则请将此评论视为批准的印章:)
@TonyDallimore添加了更多说明,标准ReDim Preserve最昂贵的部分是副本。当您有一块内存并且想要仅将其“扩展”一个字符时,实际发生的情况是操作系统必须找到一个全新的内存块(即新请求的大小),然后必须从将旧块转换为新块。随着数据的增长,这花费的时间越来越长。想象一下,建立一个2GB的阵列,一次扩展1个字节-您最终将制作约20亿个副本,总计约20亿GB!
@Blackhawk感谢您的评论。我了解字符串连接和ReDim Preserve如何在后台工作的原理。我看到ReDim Preserve Arr(1到UBound(Arr)+ 1)常常使我没有足够仔细地研究您的代码。一次,MathieuGuindon指出,每个ReDim都会使数组的大小增加一倍,而不仅仅是为下一个追加添加足够的数组,因此代码如此之快的原因显而易见。
@Mathieu:您部分正确;使用上面的测试:VBA字符串花费了68107秒; MatsMugStringbuilder花费了:0,883秒; BlackhawkStringbuilder用了0,396秒;并且两个StringBuilder版本都避免了字符串空间不足...在10K串联下没什么大不了的。