【テクニカル・上級編】VB.NETのYieldキーワードを用いたイテレータ(Iterator)の実装:大規模データをメモリ効率よく遅延評価で返すカスタム列挙の作り方 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

VB.NET Yieldの極意:大規模データをメモリ枯渇から救う遅延評価イテレータの実装

レガシーなVBAシステムから移行したエンジニアや、デスクトップアプリの極限のチューニングに挑む者たちが最初に直面する壁。それが「メモリの肥大化とガベージコレクション(GC)の圧力」だ。

数百万件のレコードをデータベースから一括取得したり、巨大なCSVファイルをメモリ上に丸ごと読み込んだりするコードを見たことはないだろうか?
`Dim list As New List(Of DataRow)()` にデータを詰め込むその瞬間、アプリケーションのメモリ使用量は跳ね上がり、LOH(Large Object Heap)の断片化を引き起こし、最終的には `OutOfMemoryException` という名の死神が訪れる。

これを打破する唯一の鍵が、`Iterator` 関数と `Yield` ステートメントによる「遅延評価(Lazy Evaluation)」だ。
今回は、VB.NETにおいてこの機能を極限まで使いこなし、システムを省メモリ化するための実践的知見を叩き込む。

1. なぜ「一括取得」が悪なのか?(メモリとライフサイクルの真実)

通常のコレクション返却処理(`List(Of T)` や配列)では、すべての要素をメモリ上に生成・保持した状態で呼び出し元に制御を返す。

.net
‘ 【悪夢のパターン】全件をメモリにロード
Public Function GetLargeData() As List(Of MyRecord)
Dim results As New List(Of MyRecord)
‘ DBから100万件取得…
While reader.Read()
results.Add(New MyRecord(reader))
End While
Return results ‘ ここで100万件分のオブジェクトがメモリに鎮座する
End Function

このアプローチは、データ量が数千件程度であれば問題ない。しかし、業務システムで扱う100万件超のデータの前では無力だ。オブジェクトの生成コスト、参照の維持、そしてそれを処理し終えるまでの間、CLR(Common Language Runtime)のメモリを圧迫し続ける。

遅延評価(Yield)のパラダイムシフト

`Yield` を使ったイテレータは、「データが必要になったその瞬間(1件ずつ)」に生成と返却を行う。呼び出し元が `For Each` で次の要素を要求したときだけ処理が動き、1件を返したら一旦処理が中断(サスペンド)される。

メモリ上に存在するデータは、常に「たった1件分」だ。

2. VB.NETにおける Iterator Function の実装パターン

VB.NETで遅延評価イテレータを実装するには、メソッドのシグネチャに `Iterator` キーワードを付与し、内部で `Yield` を使用する。

以下のコードは、巨大なCSVファイルやストリームを1行ずつパースし、メモリをほとんど消費せずに呼び出し元へ返却する実用的なパターンの実装例である。

.net
Imports System.IO
Imports System.Collections.Generic

Public Class CsvReaderIterator

‘ 大規模CSVを遅延評価で1行ずつオブジェクトに変換して返す
Public Iterator Function ReadLargeCsv(filePath As String) As IEnumerable(Of TransactionRecord)

‘ Usingブロックでストリームとリーダーを確実に管理し、スコープ外で即座にリソースを解放する
Using fs As New FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite),
sr As New StreamReader(fs, System.Text.Encoding.UTF8)

‘ ヘッダーのスキップ
Dim header As String = sr.ReadLine()
If header Is Nothing Then Exit Function

Dim lineCount As Long = 0

‘ 1行ずつ読み込み、メモリ上に全展開しない
While Not sr.EndOfStream
Dim line As String = sr.ReadLine()
lineCount += 1

If String.IsNullOrWhiteSpace(line) Then Continue While

‘ パース処理
Dim columns As String() = line.Split(“,”c)

‘ 必要なオブジェクトをその場で生成
Dim record As New TransactionRecord(lineCount, columns)

‘ ★ Yieldによる遅延返却:ここで処理が呼び出し元に一時的に渡る
Yield record

‘ ※ 次のループが回るとき、前回の record オブジェクトは
‘ 参照が外れていれば速やかにGCの対象となる(LOHを汚染しない)
End While
End Using

End Function

End Class

Public Class TransactionRecord
Public Property Id As Long
Public Property DateValue As DateTime
Public Property Amount As Decimal

Public Sub New(id As Long, cols As String())
Me.Id = id
Me.DateValue = DateTime.Parse(cols(0))
Me.Amount = Decimal.Parse(cols(1))
End Sub
End Class

秘匿されたアーキテクチャ:コンパイラの裏側の仕事

VB.NETのコンパイラは、`Iterator` キーワードがついたメソッドを見つけると、裏側で自動的に「ステートマシン(状態機械)」を表す内部クラス(`IEnumerable(Of T)` と `IEnumerator(Of T)` を実装したクラス)を生成する。

開発者が手動で複雑なステート管理クラスを書く必要はなく、`Yield` を書くだけでコンパイラがその魔法を代行してくれるのだ。

3. シニアエンジニアが押さえるべき「Yield」の罠とベストプラクティス

`Yield` は強力だが、その動作原理(遅延評価とステートマシンの非同期・中断処理)を理解していないと、現場で致命的なバグやパフォーマンス低下を引き起こす。

① 例外処理の境界線に注意する

`Yield` を含むイテレータブロック内では、`Try…Catch` の記述に制限がある。特に `Yield` ステートメントを `Try…Catch` の `Try` ブロック内に直接置くことは、VB.NET(C#も同様)では言語仕様上エラーになる場合がある(`Finally` ブロックとの競合を防ぐため)。

リソースの確実な解放には、前述のコードのように `Using` ステートメントをイテレータのスコープの最外殻に配置するのが鉄則である。

② 「何回も列挙するな」(遅延評価の再実行コスト)

`IEnumerable(Of T)` を返すイテレータは、`For Each` を回すたびに最初から処理が再実行される
もしデータベースのクエリ結果やファイル読み込みを `Yield` でラップしている場合、同じコレクションに対して何度も `For Each` や `.Count()` を呼ぶと、その都度I/Oが発生してシステムが破滅する。

.net
‘ 【危険なコード】
Dim dataSeq = reader.ReadLargeCsv(“huge.csv”)

‘ 1回目の走査(ここでファイルが読み込まれる)
Dim totalCount As Integer = dataSeq.Count()

‘ 2回目の走査(なんと!もう一度ファイルを最初から読み直している!)
For Each item In dataSeq
Process(item)
Next

対策: 一度走査した結果をメモリに保持して再利用したい場合は、遅延評価の恩恵を捨てて `.ToList()` や `.ToArray()` で一度だけ実体化(Materialization)させること。

.net
‘ 安全にメモリにキャッシュする場合
Dim dataList = reader.ReadLargeCsv(“huge.csv”).ToList()

4. Windows API連携や外部システム連携への応用

この遅延評価イテレータの真価は、Win32 APIやプロセス列挙などの非マネージリソースと組み合わせたときに発揮される。

例えば、実行中のプロセス一覧や特定のウィンドウハンドル(HWND)を監視し、条件に一致するものだけを順次処理するシステム間連携バッチにおいて、無駄なメモリ消費を抑えながらリアルタイムにストリーム処理を行うことが可能になる。

.net
‘ 例:システム上の特定条件を満たす要素を無限/大規模シーケンスとして流すパイプライン
Public Iterator Function MonitorSystemStream() As IEnumerable(Of SystemState)
While True
‘ 何らかの外部ポーリングやAPI呼び出し
Dim currentState As SystemState = FetchApiData()

‘ 評価にヒットしたものだけを流す
If currentState.IsCritical Then
Yield currentState
End If

System.Threading.Thread.Sleep(1000) ‘ 1秒ウェイト
End While
End Function

総括

VB.NETの `Iterator` と `Yield` は、単なる「コードを簡潔にするための糖衣構文(シンタックスシュガー)」ではない。
これは、「メモリ効率の限界を突破し、リソースのライフサイクルを完全に制御下に置くためのチートコード」である。

レガシーシステムの改修や、膨大なデータを扱う基幹バッチ処理において、「とりあえず `List` に突っ込む」という思考停止のプログラミングとは今日で決別せよ。適切な遅延評価の設計こそが、安定稼働する真のエンタープライズシステムの基盤となるのだ。

タイトルとURLをコピーしました