Excel VBAでStringBuilderを使う
VB.NET には StringBuilder
というクラスが実装されています。このクラスは文字列連結に特化しており、ループ内での文字列連結など処理中に文字列の連結が何度も出現する場合にその力を発揮します。VB.NET は VBA のベースとなっている VB6 の後継にあたるのですが、残念ながらこのクラスは VB6 には実装されていません。当然、VBAにも実装されていません。
なので作っちゃいました。
あった方が便利ですもんね。
StringBuilder for Excel VBA
とりあえずコードです。基本的な機能のみ実装しています。
'************************************************************************ '*----------------------------------------------------------------------- '* Name: StringBuilder (Class Module) '*----------------------------------------------------------------------- '* Descriptioin:StringBuilder for VBA '*----------------------------------------------------------------------- '* Copyright: HAYs https://dev-clips.com , 2015 All Rights Reserved. '*----------------------------------------------------------------------- '* <Update> '* Date Version Author Memo '*----------------------------------------------------------------------- '* 2015.11.25 1.00 HAYs New Release '************************************************************************ ' option Option Explicit '************************************************************************ '* variable '************************************************************************ Private pCapacity As Long Private pLength As Long Private mBuffer As String '************************************************************************ '* class event '************************************************************************ '*----------------------------------------------------------------------- '* constructor '*----------------------------------------------------------------------- Private Sub Class_Initialize() pCapacity = 1023 Me.Clear End Sub '*----------------------------------------------------------------------- '* destructor '*----------------------------------------------------------------------- Private Sub Class_Terminate() 'clean up mBuffer = vbNullString End Sub '************************************************************************ '* property '************************************************************************ '*----------------------------------------------------------------------- '* Capacity '*----------------------------------------------------------------------- Friend Property Let Capacity(ByVal NewValue As Long) 'ignore smaller NewValue If NewValue > pCapacity Then 're-allocate mBuffer = mBuffer & String(NewValue - pCapacity, vbNullChar) 'save new value pCapacity = NewValue End If End Property Friend Property Get Capacity() As Long Capacity = pCapacity End Property '*----------------------------------------------------------------------- '* Length '*----------------------------------------------------------------------- Friend Property Let Length(ByVal NewValue As Long) If NewValue < pLength Then Mid(mBuffer, NewValue + 1, pLength - NewValue) = _ String$(pLength - NewValue, vbNullChar) End If pLength = NewValue End Property Friend Property Get Length() As Long Length = pLength End Property '************************************************************************ '* method '************************************************************************ '*----------------------------------------------------------------------- '* clear '*----------------------------------------------------------------------- Friend Function Clear() As StringBuilder 'initialize length pLength = 0 'allocate memory mBuffer = String$(pCapacity, vbNullChar) 'return me Set Clear = Me End Function '*----------------------------------------------------------------------- '* append '*----------------------------------------------------------------------- Friend Function Append(ByRef StringValue As String) As StringBuilder Dim pos As Long Dim tmpCap As Long 'set position pos = pLength + 1 'add new length pLength = pLength + Len(StringValue) 'check overflow If pLength > pCapacity Then 'expand capacity *doubles up tmpCap = pCapacity Do While tmpCap < pLength tmpCap = tmpCap * 2 Loop 'save new capacity Me.Capacity = tmpCap End If 'append Mid(mBuffer, pos) = StringValue 'retrun me Set Append = Me End Function '*----------------------------------------------------------------------- '* insert '*----------------------------------------------------------------------- Friend Function Insert(ByRef StringValue As String, _ ByVal position As Long) As StringBuilder Dim tmpCap As Long Dim tmpLen As Long 'check position Select Case position Case 1 To pLength Case Is < 1: position = 1 Case Else Set Insert = Append(StringValue) Exit Function End Select 'save length tmpLen = pLength 'add new length pLength = pLength + Len(StringValue) 'check overflow If pLength > pCapacity Then 'expand Capacity *doubles up tmpCap = pCapacity Do While tmpCap < pLength tmpCap = tmpCap * 2 Loop 'save new capacity Me.Capacity = tmpCap End If 'slide Mid(mBuffer, position + Len(StringValue)) _ = Mid$(mBuffer, position, tmpLen) 'insert Mid(mBuffer, position) = StringValue 'retrun me Set Insert = Me End Function '*----------------------------------------------------------------------- '* string value '*----------------------------------------------------------------------- Friend Function ToString() As String ToString = Left$(mBuffer, pLength) End Function
StringBuilder For Excel VBA のダウンロード
各機能の使用説明
Class_Initializeイベント - コンストラクタ
クラスのインスタンス作成時に実行されるメソッド「Class_Initialize」で初期処理を行います。容量を保持する変数pCapacity
に初期値(ここでは1023)を設定しClear
メソッドを呼び出し初期化しています。
Class_Terminateイベント - デストラクタ
文字列バッファ変数mBuffer
をvbNullString
(値0を持つ文字列)でクリアしています。あまり必要のない処理ですが、大きな容量を保持するバッファですので明示的にクリア処理を搭載しました。
Clearメソッド - 初期化
現在の文字列長を保持する変数pLength
を0
でクリアし、文字列バッファ変数のmBuffer
を指定された容量(Capacity
プロパティ)で初期化します。この際、String
関数を使用して容量分のNull文字(vbNullChar
)で文字列を保存する領域を確保します。
String関数のリファレンスです。
機能:指定した文字を指定した数分繰り返した文字列を返します。
構文:String(number, character)
引数 | 指定 | 内容 |
number | 必須 | 文字列の繰り返し回数を長整数型 (Long)で指定します。 |
character | 必須 | 繰り返す文字列式を指定します。指定した文字列式が2文字以上の場合は先頭の文字を繰り返した値を返します。 |
String
関数でCapacity
分のメモリを確保しているのですが、この「あらかじめ容量を確保しておく」ってのが高速化のポイントです。なぜこれで高速処理が可能なのかは後程説明します。
Appendメソッド - 文字列追加
String
型の引数StringValue
で渡された文字列をバッファに追加します。追加後の文字列が容量(pCapacity
)を超える場合は、現在確保されている容量を2倍に拡張します。また、文字列長を保持する変数pLength
に引数StringValue
の長さを加算します。
文字列の追加はClear
メソッドで確保したバッファに対して行います。厳密にいうと、追加するというよりは追加されるべき場所の文字列(String
関数で追加したNull文字=vbNullChar
)と追加する文字列をMid
ステートメントを使用して置き換えています。
Mid
ステートメントの詳細は以下の記事を参照してください。
Mid関数とMidステートメント - https://dev-clips.com/clip/vba/mid-keyword/
Insertメソッド - 文字列挿入
基本的には文字列追加のAppend
メソッドと同じですが、Append
メソッドがバッファの最後に文字列を追加するのに対し、Insert
メソッドは引数position
で指定した位置に文字列を挿入します。引数position
が0
以下の場合は1文字目の位置に挿入します。現在の文字列長を超える場合はAppend
メソッドを呼び出して文字列を最後に追加します。
Clear
メソッド、Append
メソッド、Insert
メソッドは処理が終了したら、自分自身(Me=StringBuffer
クラス)を返します。ToStringメソッド - 連結文字列
Append
メソッド、Insert
メソッドで連結した文字列を返します。
Capacityプロパティ - 文字列バッファ容量
文字列バッファの容量を保存します。値の設定と取得が可能です。新しい容量を設定する際、設定済みの容量よりも大きい場合のみプロパティの値を更新し、文字列バッファを拡張します。
Lengthプロパティ - 文字列長
現在の文字列長を保持します。値の設定と取得が可能です。新しい値を設定する際、現在の文字列長より短い長さを指定した場合は指定した長さを超過した部分をNull文字(vbNullChar
)でクリアします。
StringBuilderが高速な理由
文字列の不変性
文字列は不変です。いきなり何言ってんだとか思われるかもしれませんが、StringBuilder
が高速な理由(というか文字列連結が低速な理由)を知るには文字列の不変性について理解しとかなければなりません。
MSDNにある Visual Basic のString型に関する記事では以下のように明記されています。
文字列は不変です。つまり、その値は作成後に変更することができません。
String Basics in Visual Basic
- https://msdn.microsoft.com/ja-jp/library/ms234766.aspx
(え?String型の変数は"ABC"って文字列を入れた後に"DEF"って文字列格納できるじゃん...)
とか思われて何言ってんだコイツ状態の方もいらっしゃるかもしれませんので、String型の特性についてちょっとだけおさらいしときましょう。
String型は参照型です。String型の変数に何らかの文字列を代入すると、(文字列長×2)バイト + 6バイトをメモリ上のヒープ領域に確保します。
※正確には、というかメモリレイアウト的には 4バイト + (文字列長×2)バイト + 2バイト です。
String型の変数は実際に代入した文字列が格納されるわけではなく、確保したメモリのアドレスを参照します。
例えば、String型の変数Sに文字列"ABC"を代入した場合は以下のようになります。ここで検証のためにString型の確保したメモリのアドレスを返す関数 StrPtr
関数を使用しています。
Dim S As String S = "ABC" Debug.Print StrPtr(S)
僕の環境ではStrPtr
関数は 9257852 という値を返しました。因みにこのStrPtr
関数が返す値は環境によって異なりますし、同じ環境でも実行する度に異なります。(もちろん、同じ値になる場合もありますよ)
StrPtr
関数のリファレンスです。
機能:String型変数が参照するメモリアドレス(ポインタ)をLongPtr型で返す。
構文:StrPtr(Ptr as String) As LongPtr
引数 | 指定 | 内容 |
Ptr | 必須 | String型の変数を指定します。 |
※LongPtr型は64bit環境ではLongLong型(8バイト)となり、32bit環境ではLong型(4バイト)となります。これは VBA が自動で判別するのでユーザーが意識する必要はありません。
ここで、変数S
に別の文字列"DEF"を代入し、StrPtr
関数でメモリアドレスを表示させてみます。
S = "DEF" Debug.Print StrPtr(S)
ここではStrPtr
関数は 108956956 を返しました。初めに変数S
に”ABC”という文字列を代入したときは 9257852 を返したので、この時点で参照するメモリアドレスが異なっています。この事からわかるように、一度文字列を代入した変数に新たに文字列を代入すると、メモリ上に新しい領域が確保され、変数S
は新しく確保した”DEF”のアドレスを参照します。"ABC"と"DEF"という同じ3文字(3バイト)でもその領域は再利用されることはありません。そして今まで確保していた”ABC”の領域は(適当なタイミングで)破棄されてしまいます。この仕組みが上述のMSDNの引用にある文字列の不変性です。
Midステートメントを使用した文字列操作
「文字列の不変性」については理解して頂けたでしょうか?String型の変数は異なる文字列を代入する度に新しいメモリ領域を確保します。つまり、今まで使用した領域を再利用することはありません。このため、文字列の代入には文字列の長さに比例したオーバーヘッドが発生します。
ただし、これは代入演算子(=)で文字列を代入した場合の話です。
既にString型の変数に文字列が代入されている場合、Mid
ステートメントを使用すると新しい領域を確保することなく、つまり既に確保した領域を再利用して文字列を変更することが可能になります。Mid
ステートメントは文字列の一部を置き換えます。文字列の一部を取り出すMid関数と同じ名前で紛らわしいですが、2つは異なります。
例として、先ほどの変数SにMidステートメントを用いて文字列"GHI"を代入してみます。ここでも検証のためにStrPtr
関数を使用します。
Mid(S, 1, 3) = "GHI" Debug.Print StrPtr(S)
StrPtr
関数は 108956956 を返しました。この値は"GHI"代入前と同じ値です。つまり、変数Sは"DEF"を格納した時と同じメモリアドレスを参照し、一度確保した領域を再利用している事を意味します。この場合、メモリ領域を新たに確保するコストがかからないためパフォーマンスの向上が期待できます。
サンプルで使用した3文字程度の文字列ならメモリ割り当てのオーバーヘッドも小さく体感できるほどのパフォーマンスの差はありませんが、繰り返し処理の中や大きい文字列の連結などの処理でMid
ステートメントを使用した文字列の代入(正確には置換)は大きな効力を発揮します。
冒頭のStringBuilder
クラスでは、インスタンス作成時に大きな容量をあらかじめ確保しています。そして、Append
メソッドやInsert
メソッドで文字列が追加されると確保した領域の一部をMid
ステートメントを使用して置換します。先述の通り、この方法ではあらかじめ確保していた領域を再利用するためメモリ割り当てのコストがかかりません。
これがStringBuilder
クラスが高速な理由です。
パフォーマンスの検証
StringBuilder
が高速に処理をするのは既に説明しましたが、一体どれだけのパフォーマンス向上が見込めるのでしょうか。
以下のサンプルで検証してみます。まずは、普通に&
演算子で変数S
に文字列"ABC"を100,000回連結します。実行時間の計測には午前0時からの秒数をSingle型で返すTimer
関数を使用します。
Sub Ampersand_Benchmark() Dim start As Single Dim i As Long Dim S As String '計測開始 start = Timer '"ABC"を100000回連結する For i = 1 To 100000 S = S & "ABC" Next '計測終了 Debug.Print Timer - start End Sub
次に同条件でStringBuilder
を利用した文字列連結の検証です。コンストラクタ(初期処理)での容量確保もパフォーマンスに影響するはずなので、実行時間計測はループに入る前のインスタンス作成時点から始めます。
Sub StringBuilder_Benchmark() Dim start As Single Dim i As Long Dim S As String Dim sb As StringBuilder '計測開始 start = Timer 'インスタンス作成 Set sb = New StringBuilder '"ABC"を100000回連結する For i = 1 To 100000 sb.Append "ABC" Next '計測終了 Debug.Print Timer - start End Sub
それぞれ5回計測した結果は以下の表のとおりです。
※計測時間の単位は秒です。
実行回数 | &演算子 | StringBuilder |
1回目 | 5.94604500 | 0.06005859 |
2回目 | 6.22595200 | 0.05200195 |
3回目 | 6.33190900 | 0.05407715 |
4回目 | 6.20996100 | 0.05200195 |
5回目 | 6.18493700 | 0.04003906 |
平均 | 6.17976080 | 0.05163574 |
&演算子での連結が平均で6秒以上かかっているのに対し、StringBuilder
での連結はわずか50ミリ秒程です。StringBuilder
で連結した方が断然高速に処理できることがわかります。
まとめ。
開発していると必ずと言っていいほど文字列連結処理ってありますよね。小さい文字列で連結回数が少なければいいんですが、ループ内での連結などがあるとパフォーマンスに大きく影響します。そんな時はこのStringBuilder
クラスを使ってみてください。文字列連結にかかるコストをカットしているのでかなりのパフォーマンスアップが期待できますよ:)
何かご質問等ございましたらお気軽にお問合せくださいまし。
HAYs
コメントを残す