【実務・中級編】実務中級者向け:VB.NETにおける「Stack(Of T)」と「Queue(Of T)」の業務活用:LIFO/FIFO構造による履歴管理とタスク処理 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

【VB.NET実務】Stack(Of T)とQueue(Of T)を極める:LIFO/FIFO構造で実現する堅牢な履歴管理とタスク処理

こんにちは。開発プロジェクトを率いるチーフアーキテクトの私だ。

日々の業務システム開発で、こんな悩みに直面したことはないだろうか?

  • 「ユーザーの操作履歴を保持して『元に戻す(Undo)』機能を実装したいが、配列やリストのインデックス管理が煩雑でバグの温床になっている」
  • 「非同期で飛んできたタスクやリクエストを順番通り(FIFO)に処理したいのに、スレッドセーフティや排他制御で頭を悩ませている」

これらを素朴な `List(Of T)` や場当たり的なフラグ管理で実装しようとする者は、総じて二流のプログラマだ。リストの途中で要素を挿入・削除するたびにメモリコピーが発生し、パフォーマンスは地に落ち、インデックスのズレによるNullReferenceExceptionの地雷を踏むことになる。

.NETが誇るジェネリックコレクションである `Stack(Of T)`(LIFO: 後入先出)`Queue(Of T)`(FIFO: 先入先出)。この2つのデータ構造のライフサイクルと特性を骨の髄まで理解していれば、これらの問題はスマートに、かつ極めて高速に解決できる。

今回は、実務の現場で即座に使える、堅牢で保守性の高いプロダクションコードと共に、その極意を伝授しよう。

1. なぜ「リスト」ではなく「Stack」と「Queue」なのか?

大前提として、データ構造の選択はシステムの寿命を左右する。

| コレクション | 構造 | 特徴 | 主な実務用途 |
| :— | :— | :— | :— |
| `List(Of T)` | インデックスアクセス | 任意の場所の参照・変更が得意。末尾以外の追加・削除はO(N)の負荷。 | マスタデータの保持、単純な一覧表示 |
| `Stack(Of T)` | LIFO (Last-In, First-Out) | 最後に積んだものが最初に取れる。追加・削除が常にO(1)の爆速。 | Undo/Redo機能、構文解析、深さ優先探索 |
| `Queue(Of T)` | FIFO (First-In, First-Out) | 最初に入れたものが最初に取れる。追加・削除が常にO(1)の爆速。 | タスクキューイング、バッチ処理、幅優先探索 |

`Stack`とライバル関係にあるのは `List` だが、末尾以外への頻繁な挿入・削除が発生するアルゴリズムで `List` を使うのは、バグを自ら買いに行っているようなものだ。
目的が「順序の制御」であるならば、迷わず `Stack` または `Queue` を選ぶべきだ。

2. 【実践】`Stack(Of T)` による「高度なUndo/Redo(元に戻す/やり直し)」管理

業務アプリケーションで「操作の履歴管理(Undo)」を実装する際、`List` と現在地を指すポインタ変数(`currentIndex`)で管理していないか? その実装、状態の分岐や最大履歴数の制限(トリミング)を実装する段階でコードがスパゲッティ化する悪寒がしないか。

ここで紹介するのは、「メインのUndoスタック」と「やり直し用のRedoスタック」の2つを組み合わせた、最も美しく堅牢な設計パターンの実例だ。

プロダクションコード例(VB.NET)

Imports System.Collections.Generic

Namespace Enterprise.Patterns

‘ 業務データ(例として画面の入力状態やトランザクション情報を想定)
Public Class DocumentState
Public Property TextContent As String
Public Property CursorPosition As Integer

Public Sub New(text As String, position As Integer)
Me.TextContent = text
Me.CursorPosition = position
End Sub
End Class

‘ 堅牢なUndo/Redoマネージャー
Public NotInheritable Class UndoRedoManager(Of T)
Private ReadOnly _undoStack As New Stack(Of T)()
Private ReadOnly _redoStack As New Stack(Of T)()
Private ReadOnly _maxHistorySize As Integer

”’

”’ コンストラクタ
”’

”’ 保持する最大履歴数(メモリリーク防止のため必ず制限を設けること) Public Sub New(Optional maxHistorySize As Integer = 50)
_maxHistorySize = maxHistorySize
End Sub

”’

”’ 新しい操作状態を記録する
”’

Public Sub RecordState(state As T)
_undoStack.Push(state)

‘ 新規アクションが発生した瞬間、Redoスタックはクリアするのが定石
_redoStack.Clear()

‘ 最大履歴数を超える場合のトリミング(古いものから捨てる)
‘ Stack(Of T)には容量制限がないため、溢れた分を底から切り捨てる処理が必要
If _undoStack.Count > _maxHistorySize Then
TrimOldestState()
End If
End Sub

”’

”’ 元に戻す (Undo)
”’

Public Function Undo(currentState As T) As T
If Not CanUndo() Then
Throw New InvalidOperationException(“これ以上戻すことはできません。”)
End If

‘ 現在の状態をRedo側へ退避
_redoStack.Push(currentState)

‘ Undoスタックから直前の状態を取り出す
Return _undoStack.Pop()
End Function

”’

”’ やり直す (Redo)
”’

Public Function Redo(currentState As T) As T
If Not CanRedo() Then
Throw New InvalidOperationException(“これ以上やり直すことはできません。”)
End If

‘ 現在の状態をUndo側へ戻す
_undoStack.Push(currentState)

‘ Redoスタックから状態を取り出す
Return _redoStack.Pop()
End Function

Public Function CanUndo() As Boolean
Return _undoStack.Count > 0
End Function

Public Function CanRedo() As Boolean
Return _redoStack.Count > 0
End Function

”’

”’ Stackの底にある古い要素を削除するための内部メソッド
”’ (Stackは直接インデックス指定で削除できないため、一度配列に逃がして再構築する)
”’

Private Sub TrimOldestState()
Dim tempArray(_maxHistorySize – 1) As T
‘ 上位(新しい順)から指定数だけコピー
For i As Integer = 0 To _maxHistorySize – 1
tempArray(i) = _undoStack.Pop()
Next

_undoStack.Clear()

‘ 古い要素(配列の最後尾にいたもの)を除外しつつ、再度プッシュする
For i As Integer = _maxHistorySize – 1 To 0 Step -1
_undoStack.Push(tempArray(i))
Next
End Sub

End Class
End Namespace

設計上のキモ

1. メモリリークの防止 (`_maxHistorySize`):
`Stack` は要素を追加し続けると際限なくメモリを消費する。業務アプリでは必ず上限を設け、古いデータを捨てる設計(トリミング)を組み込むこと。
2. Redoスタックのクリア:
ユーザーが「Undo」した後に「新たな操作」を行った場合、未来の履歴(Redo)は消去しなければならない。`RecordState` 内での `_redoStack.Clear()` は絶対の鉄則だ。

3. 【実践】`Queue(Of T)` による「非同期タスク・ログの順次処理(FIFO)」

次に、バッチ処理やファイルインポート、データベースへの非同期バルクインサートなどで頻繁に要求される「タスクの順次処理」だ。
「同時にリクエストが来るが、データベースのロック競合を防ぐために1件ずつ順番に処理したい」という要件には `Queue(Of T)` が完璧にフィットする。

プロダクションコード例(VB.NET:スレッドセーフなタスクワーカー)

実務ではマルチスレッド環境下(バックグラウンドワーカーなど)からキューにタスクが追加されることが多いため、排他制御(`SyncLock`)を考慮した実装が必須となる。

Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks

Namespace Enterprise.Patterns

‘ 処理対象のタスクデータ構造
Public Class ImportTask
Public Property TaskId As String
Public Property FilePath As String
End Class

‘ スレッドセーフなFIFOタスクプロセッサ
Public NotInheritable Class SequentialTaskQueue
Implements IDisposable

Private ReadOnly _queue As New Queue(Of ImportTask)()
Private ReadOnly _lockObj As New Object()
Private ReadOnly _cancellationTokenSource As New CancellationTokenSource()
Private _processingTask As Task
Private _isDisposed As Boolean = False

Public Sub New()
‘ バックグラウンドで常時キューを監視するコンシューマタスクを起動
_processingTask = Task.Run(Address C T ProcessQueueLoop)
End Sub

”’

”’ キューに新しいタスクをエンキュー(追加)する
”’

Public Sub EnqueueTask(task As ImportTask)
If _isDisposed Then Throw New ObjectDisposedException(NameOf(SequentialTaskQueue))

SyncLock _lockObj
_queue.Enqueue(task)
‘ 待機しているスレッドがあれば起こす(Monitor.Pulseでも良いが、シンプルにQueue管理を維持)
End SyncLock
End Sub

”’

”’ バックグラウンドでのキュー監視・順次処理ループ
”’

Private Sub ProcessQueueLoop()
While Not _cancellationTokenSource.Token.IsCancellationRequested
Dim currentTask As ImportTask = Nothing

SyncLock _lockObj
If _queue.Count > 0 Then
‘ 先頭の要素を取り出す (FIFO)
currentTask = _queue.Dequeue()
End If
End SyncLock

If currentTask IsNot Nothing Then
‘ キューから取り出したタスクを実行(実際の業務ロジック)
ExecuteTask(currentTask)
Else
‘ タスクがない場合はCPUを無駄に占有しないよう少しスリープ
Thread.Sleep(100)
End If
End While
End Sub

Private Sub ExecuteTask(task As ImportTask)
Try
Console.WriteLine($”[開始] タスクID: {task.TaskId}, ファイル: {task.FilePath}”)

‘ ここに実際のファイル読み込みやDB登録処理を記述
Thread.Sleep(1000) ‘ 処理のシミュレーション

Console.WriteLine($”[完了] タスクID: {task.TaskId}”)
Catch ex As Exception
‘ 実務ではここでログ出力と例外の握りつぶし(またはリトライキューへの再投入)を行う
Console.Error.WriteLine($”[エラー] タスク失敗: {task.TaskId} – {ex.Message}”)
End Try
End Sub

”’

”’ 終了時のクリーンアップ
”’

Public Sub Dispose() Implements IDisposable.Dispose
If _isDisposed Then Return
_isDisposed = True

_cancellationTokenSource.Cancel()
Try
‘ 実行中のワーカータスクの終了を最大5秒まで待機
_processingTask.Wait(5000)
Catch
‘ タイムアウト等は無視
End Try
_cancellationTokenSource.Dispose()
End Sub
End Class
End Namespace

設計上のキモ

1. スレッドセーフティの確保 (`SyncLock`):
複数スレッドから同時に `Enqueue` や `Dequeue` が走ると、内部配列のポインタが破損し致命的な例外(`InvalidOperationException` など)を引き起こす。必ず `SyncLock` でクリティカルセクションを保護すること。
2. ビジーウェイト(CPU空転)の防止:
キューが空のときに無限ループでCPUを100%食いつぶさないよう、`Thread.Sleep` や `SemaphoreSlim` などの非同期ウェイト機構を組み合わせるのがプロの技だ。.NET 4.0以降であれば `BlockingCollection(Of T)` を使うという選択肢もあるが、明示的に順序制御やカスタムロジックを入れたい場合は `Queue(Of T)` + `SyncLock` の組み合わせが最も直感的でバグを生みにくい。

4. チーフアーキテクトからの実践的アドバイス

1. 「foreach」での走査順序に注意せよ

  • `Stack(Of T)` を `For Each` で回すと、「新しい順(上から下)」の順序で列挙される。
  • `Queue(Of T)` を `For Each` で回すと、「古い順(先頭から末尾)」の順序で列挙される。
  • これらを意図せずに混同すると、デバッグが極めて困難なロジックバグの温床になる。

2. パフォーマンスへの過信は禁物

  • `Stack` も `Queue` も内部は実体として配列(Array)を持っている。容量が初期サイズを超えると、内部でメモリの再割りAllocation(リロケーション)とコピーが発生する。
  • もしあらかじめ最大要素数が予測できるのであれば、コンストラクタで初期キャパシティを指定せよ(例: `New Queue(Of ImportTask)(1000)`)。これだけで実務での高負荷時のガベージコレクション(GC)発生頻度を劇的に劇減させることができる。

まとめ

VB.NETにおける `Stack(Of T)` と `Queue(Of T)` は、単なる「便利な入れ物」ではない。
「データのライフサイクルと処理順序をコンパイラレベルで担保し、バグの入り込む隙を完全に塞ぐための最強の武器」である。

場当たり的な `List` のインデックス操作やフラグ管理から脱却し、LIFO/FIFOの構造美を取り入れた堅牢なアーキテクチャを構築してほしい。君たちの書くコードの品質が一段階も二段階も跳ね上がることを約束しよう。

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