【VB.NET】文字列連結でアプリを殺すな。「String vs StringBuilder」の境界線と最適解
業務自動化ツールを開発していると、必ずぶち当たる壁がある。「数万行のCSV出力」や「巨大なログの生成」だ。
「とりあえず `str &= line` でいいだろう」
もしあなたがそう考えているなら、今すぐそのコードを修正すべきだ。なぜなら、その書き方がメモリの断片化(フラグメンテーション)を誘発し、GC(ガベージコレクション)を暴走させ、最終的にアプリケーションをフリーズさせる主犯だからだ。
今日は、VB.NETにおける文字列操作の「真実」と、プロフェッショナルが採用すべき実装術を伝授する。
—
1. なぜ `String &=` は「悪」なのか?
VB.NETの `String` 型はイミュータブル(不変)だ。一度生成されたら、そのメモリ領域の内容は一切書き換えられない。
`str &= line` を実行するたびに、裏側では以下のことが起きている。
1. 現在の `str` の長さと `line` の長さを足した、新しい巨大なメモリ領域が確保される。
2. 古い `str` と `line` の内容が、新しい領域へコピーされる。
3. 古い `str` は「ゴミ」としてGCの回収対象になる。
数万回この操作を繰り返すとどうなるか? メモリ上には無数の「使い終わった古い文字列」が散乱し、CLR(共通言語ランタイム)はメモリの整理(GC)に追われ、処理は指数関数的に遅延していく。これがフリーズの正体だ。
—
2. StringBuilder:メモリの賢者
一方で `StringBuilder` は、あらかじめ大きなメモリバッファを確保し、その中で文字列を継ぎ足していく。メモリの再確保が最小限で済むため、連結操作が極めて高速だ。
性能比較の目安
- 100回程度の連結: `String` でも許容範囲内。
- 1,000回以上の連結: ここが分岐点。迷わず `StringBuilder` を選べ。
- 数万行のCSV出力: `StringBuilder` 一択。これ以外は「バグ」と見なすべきだ。
—
3. 実務でそのまま使える!高効率CSV出力コード
保守性が高く、かつパフォーマンスを最大化したプロダクションコードのテンプレートを公開する。
Imports System.Text
Imports System.IO
Public Sub ExportCsvLargeData(dataList As List(Of MyData), filePath As String)
‘ 1. バッファサイズを想定してStringBuilderを初期化
‘ 初期容量を指定することで、メモリ拡張コストを抑える(推測でOK)
Dim sb As New StringBuilder(1024 1024)
Try
‘ 2. ループ処理
For Each item In dataList
‘ AppendFormatを使うと可読性が上がる
‘ カンマ区切りの連結にはAppendを連鎖させるのが最速
sb.Append(item.Id).Append(“,”)
sb.Append(item.Name).Append(“,”)
sb.Append(item.Timestamp).AppendLine()
‘ メモリ枯渇を防ぐための工夫:
‘ 極端に巨大な場合、定期的にファイルへ書き出してsbをクリアする手法もある
Next
‘ 3. 一気にファイルへ書き込む
‘ File.WriteAllTextは内部でストリームを閉じるため堅牢
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8)
Catch ex As IOException
‘ ファイルロック等のエラーを適切にハンドリングする
Throw New Exception(“ファイル書き込みエラー: ” & ex.Message)
End Try
End Sub
この実装のポイント
- 初期容量の指定: `New StringBuilder(1024 1024)` とすることで、初期状態で1MBのメモリを確保し、連結時の再確保回数を劇的に減らしている。
- `AppendLine` の活用: OSごとの改行コードを意識する必要がない。
- Encodingの指定: CSV生成時は文字化けを防ぐため、必ず `Encoding.UTF8` などを明示的に指定する。
—
4. プロの設計判断:いつ「ストリーム」に切り替えるべきか?
もし、メモリに乗せきれないレベルのデータ(例:数GBのログファイル)を扱う場合、`StringBuilder` でさえメモリ不足(OutOfMemoryException)を引き起こす。
その場合は、`StringBuilder` を捨てて `StreamWriter` を使うのが正解だ。
Using writer As New StreamWriter(filePath, False, Encoding.UTF8)
For Each item In dataList
‘ メモリに溜め込まず、直接ディスクへ流し込む
writer.WriteLine($”{item.Id},{item.Name},{item.Timestamp}”)
Next
End Using
「メモリに溜める(StringBuilder)」か「直接流す(StreamWriter)」か。 この判断こそが、ツールを止めることなく最後まで完走させるエンジニアの胆力である。
—
最後に:コードは「資産」であれ
「動けばいい」というコードは、半年後の自分を殺す。
今回紹介した `StringBuilder` への切り替えは、単なる高速化ではない。メモリ効率という「システムの健康状態」を制御する技術だ。
自動化ツールは、誰かの時間を奪うものではなく、誰かの時間を創造するためのもの。その基盤となるコードが、最も堅牢で、洗練されているべきだ。
明日からの開発で、ぜひこの視点を組み込んでみてほしい。あなたのコードは、もっと速く、もっと美しくなれる。
