Visual Basic で StringBuilder を使うべき場合とその利点

こんにちは、じゅんじゅんです。先日、業務の中で .NET Framework(Visual Basic) での改修を行っているとき、 SQL 文の生成を String で行っている部分と StringBuilder で行っている部分があることが気になったので、 kenzaurosさんに 2つの違いを教えていただきました。

ということで今回は String と StringBuilder の違いどういうときに StringBuilder を使うのかについてお話します。

String と StringBuilder の違い

String の特徴

String クラスのオブジェクトは読み取り専用になっています。作成したら変更ができません

以下のように元の文字列 (myName) に別の文字列を連結したものを代入する場合、変更を行っているように見えますが、実際にはその変更を含む新しいオブジェクトが作成され、その参照が myName に代入されています

Dim myName As String = "ねじま"
myName = myName & "はじま"

Console.WriteLine(myName) ' ねじまはじま

このとき、変更前の文字列のオブジェクト (ねじま) は使われないオブジェクトとして残ります。といってもシステムが自動で廃棄を行いメモリを解放するので少しなら問題ありませんが、文字列に対して何度も変更を実行する必要がある場合、パフォーマンスを低下させる恐れがあります。

StringBuilder の特徴

一方、 StringBuilder の場合、文字列の内容を直接変更することができます。 StringBuilder クラスのオブジェクトは文字列の変更に対応するためのメモリのバッファをあらかじめ用意しています。文字列がバッファから溢れたら新しく 2 倍のバッファが割り当てられ、そこに今までのデータをコピーします。

以下のコードでは、上記の String と同様 myName に “ねじまはじま” という文字列が入りますが、こちらでは直接 myName というオブジェクトの内容を変更しており、 String のように新しいオブジェクトが作られることはありません

Dim myName As New System.Text.StringBuilder("ねじま")
myName.Append("はじま")

Console.WriteLine(myName) ' ねじまはじま

StringBuilder は動的に文字列内の文字数を拡張できるようにするオブジェクトですが、かといってバッファの更新が頻繁に起こってしまうとパフォーマンスが低下してしまいます。それを防ぐため、 StringBuilder.Capacity を設定しておくことで、あらかじめメモリに格納できる文字数を決めておくことができます。ちなみに Capacity の初期値は 16 文字です。

Dim myName As New System.Text.StringBuilder
myName.Capacity = 200

また、 Capacity プロパティは StringBuilder のコンストラクタの引数でも指定ができます。

Dim myName As New System.Text.StringBuilder(200)

' 文字列の初期値がある場合
Dim yourName As New System.Text.StringBuilder("ねじまはじま", 200)

このように先にどれぐらいの文字数になるかが予測できる場合、 StringBuilder.Capacity で大きめの値を設定しておけばバッファの更新の頻度を抑えることができます。

StringBuilder はどういうときに使うのか

では具体的にどのような場合に StringBuilder を使うべきか考えたとき、文字列の変更をたくさん行う Forで使うと恩恵が大きそうですね。実際はどうなのか、 For 文での文字列の連結を、 String で行った場合と StringBuilder で行った場合で実行速度の差を計って確認してみましょう。

Dim stopwatch As New Stopwatch()

' String で 100000 回文字列を連結
Dim str As String = ""
stopwatch.Start()
For i As Integer = 0 To 100000
    str = str & "a"
Next
stopwatch.Stop()
Console.WriteLine($"String:        {stopwatch.Elapsed} 秒")

stopwatch.Reset()

' StringBuilder で 100000 回文字列を連結
Dim strBd As New System.Text.StringBuilder("")
stopwatch.Start()
For i As Integer = 0 To 100000
    strBd = strBd.Append("a")
Next
stopwatch.Stop()
Console.WriteLine($"StringBuilder: {stopwatch.Elapsed} 秒")

' 結果
' String:        00:00:01.7203247 秒
' StringBuilder: 00:00:00.0015878 秒

結果、圧倒的に StringBuilder が速いという結果になりました。

String を使う方がいいかもしれない場合

逆に StringBuilder よりも String を使う方がパフォーマンスが良くなる可能性がある場合も紹介しておきます。

  • 文字列に対しての変更の回数が少ない場合

特にループもなく文字列の変更も少ない場合、 StringBuilder を使うことでパフォーマンスがわずかに低下する可能性があります。

  • 文字列の広範な検索操作を実行する必要がある場合

StringBuilder には、 String.ContainsString.IndexOfString.StartsWith などの文字列を検索するメソッドがありません。一度 StringBuilder を String に変換する必要があるため、結果的に StringBuilder のパフォーマンス上の利点が損なわれる可能性があります。

感想

.NET Framework に初めて触れて何か月か経ちますが、ずっと String と StringBuilder の違いをよく知らないまま使ってしまっていました。これは調べることが面倒だったからではなく、これらの違いを知る必要があるという意識が薄かったからです。別の言い方をすると**「自分が知らない」ということを知らない**状態でした。

「他の人が書いてたから」「動いてるからいいや」ではなく、細かいところにもしっかり疑問を持ち、自分がまだ知らないことはないかを常に意識しておこうと思います。

参考

junya-gera