大規模データ処理の「メモリ爆死」を回避せよ:VB.NET `Yield`による遅延評価の実践
業務自動化の現場で、開発者が最も陥りやすい罠がある。それは、「とりあえず全件を`List(Of T)`に格納する」という安易な実装だ。
数万件程度のCSVやDBレコードなら問題ない。だが、数百万件規模のデータパイプラインを組んだ瞬間、アプリケーションはメモリを食いつぶし、ガベージコレクション(GC)が頻発してシステムは停止する。あるいは、`OutOfMemoryException`という名の死神が訪れる。
プロフェッショナルであれば、「必要な時に、必要な分だけ」データを渡す設計をすべきだ。VB.NETにおけるその解が、`Yield`キーワードを用いたイテレータ実装である。
—
1. なぜ「全件ロード」がNGなのか
多くの開発者は、データを処理する際に以下のようなコードを書く。
.net
‘ 【アンチパターン】メモリを浪費する実装
Public Function GetBigData() As List(Of DataRow)
Dim result As New List(Of DataRow)
‘ ファイルやDBから全件取得してリストに詰める
‘ この時点でメモリ上に全データが展開され、GCへの負荷が最大化する
Return result
End Function
この実装の罪は深い。巨大なデータセットを扱う際、戻り値が確定するまでメインスレッドはブロックされ、メモリ消費量はデータサイズに比例して肥大化する。後続の処理が1件目のデータしか必要としていなくても、全件がメモリに乗るまで待機しなければならないのだ。
—
2. Yieldキーワードによる「遅延評価」の魔法
`Yield`は、メソッドを「値の供給源(イテレータ)」へと変貌させる。メソッドが実行されても、実際には中身のループは回らず、呼び出し元が `For Each` で要素を要求した瞬間に初めて1件ずつ計算・返却が行われる。
実践:巨大なCSVを1行ずつメモリ効率よく読み込む
以下は、数GBの巨大ファイルを1行ずつ読み込み、条件に合うものだけをパイプラインに流す堅牢な設計の実装例だ。
.net
Imports System.IO
Public Class DataProcessor
”’
”’
Public Iterator Function StreamLargeFile(filePath As String) As IEnumerable(Of String)
‘ Usingブロックでリソースを確実に解放する
Using reader As New StreamReader(filePath)
While Not reader.EndOfStream
Dim line As String = reader.ReadLine()
‘ ここでフィルタリングを行うことで、不要な行をメモリに乗せない
If Not String.IsNullOrWhiteSpace(line) AndAlso line.Contains(“TargetKeyword”) Then
‘ Yieldが呼ばれた瞬間に呼び出し元へ1件返却し、メソッドの実行は一時停止する
Yield line
End If
End While
End Using
‘ メソッドが終了するとイテレータは自動的に終了する
End Function
End Class
—
3. 開発者が守るべき「3つの鉄則」
この手法を実務で採用する際、以下の3点を徹底しなければならない。
① 状態の保持に注意する
`Yield`メソッドは、呼び出されるたびにメソッドのローカル変数が保持される。複雑な再帰処理や、クラスのメンバ変数を書き換えるような処理をイテレータ内で行うと、意図しない副作用を生む可能性がある。イテレータは「純粋なデータ供給源」に徹するのがベストプラクティスだ。
② リソース解放は確実に
`Yield`を使用するメソッド内では、必ず `Using` ブロックを活用すること。イテレータが途中で中断(`Break`等)された場合でも、`Using`は適切にリソース(ファイルハンドルやDBコネクション)をクリーンアップしてくれる。
③ デバッグの難易度を理解する
イテレータはステップ実行が直感的ではない。メソッド内にブレークポイントを置いても、`For Each`側で次の要素を要求するまで処理が止まらないからだ。この特性を理解し、ログ出力によるトレースを併用することを推奨する。
—
4. 結論:パイプライン思考への転換
`Yield`を使うことは、単なるコーディングテクニックではない。それは、「システム全体をストリームとして捉える」というパイプライン思考への転換だ。
データベースから抽出して、変換し、ファイルに出力する。この一連の流れを `IEnumerable(Of T)` で繋ぐことができれば、扱うデータが100件だろうが1億件だろうが、メモリ消費量は常に一定(定数オーダー)に収まる。
これが、世界最高峰の自動化エンジニアが到達する領域だ。
次に君が書くコードでは、メモリを「確保」するのではなく、データが「流れる」設計を意識してほしい。その時、君のアプリケーションは、どんな巨大なデータセットに対しても静かに、かつ確実にタスクを遂行するようになるはずだ。
