【テクニカル・上級編】実務中級者向け: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の完全制覇

レガシーな業務アプリケーションの保守、あるいはVBAからのステップアップの現場において、未だに「配列の再定義(`ReDim Preserve`)」や「`ArrayList`(ボックス化の悪夢)」、あるいは「`List(Of T)`の先頭インデックスを無理やり操作するO(N)の愚行」を見かけるたびに、私は深い絶望とエンジニアとしての使命感を覚える。

特に、業務システムにおける「操作の履歴管理(Undo/Redo)」や「大量データの非同期バッファリング(タスクキュー)」において、データ構造の選択を誤ることは、システム全体のパフォーマンスをドブに捨てることに等しい。

今回は、VB.NETの中級から上級へステップアップするために避けて通れない、`Stack(Of T)`(LIFO)`Queue(Of T)`(FIFO)の極限の活用法を、メモリ構造と実務的コンテキストの双方から叩き込む。

1. なぜ配列やList(Of T)ではダメなのか?(メモリと計算量の真実)

業務アプリケーションでよくあるアンチテーゼとして、`List(Of T)`の先頭要素に対して `List.Insert(0, item)` や `List.RemoveAt(0)` を繰り返し実行するコードが存在する。

これは、内部配列のメモリシフト(MemMove)を毎回発生させるため、要素数 $N$ に対して処理コストが $O(N)$ となり、データ量が増大するにつれてシステムは致命的なスローダウンを引き起こす。

これに対し、`Stack(Of T)` と `Queue(Of T)` は、内部構造として連続したメモリ領域(配列)を持ちつつも、ポインタ(またはインデックス)の操作のみで要素の出し入れを完結させる。これにより、追加・削除の計算量は常に $O(1)$(償却計算量)を維持する。

  • `Stack(Of T)` (Last In, First Out – 後入先出): 積み上げた皿の一番上から取る・置く。
  • `Queue(Of T)` (First In, First Out – 先入先出): 行列の最後尾に並び、先頭から処理する(リングバッファ構造)。

この特性を理解していれば、業務上の「順序制御」が必要な場面でどちらを使うべきかは自明である。

2. 【実務応用1】`Stack(Of T)` を用いた高度な Undo/Redo(操作履歴管理)

基幹業務システムの入力画面において、「直前の操作を戻す(Undo)」機能はもはや必須のUI要件である。
単に状態を全コピーしてスタックに積むアプローチは、メモリ消費の観点から愚策である。ここでは、「コマンドパターン(Command Pattern)」と `Stack(Of T)` を組み合わせ、状態の差分や実行オブジェクト自体をスタックで管理する極限の実装を示す。

Imports System
Imports System.Collections.Generic

Namespace Enterprise.Patterns

‘ コマンドインターフェース
Public Interface ICommand
Sub Execute()
Sub Undo()
End Interface

‘ 具体的な業務コマンド(例:単価変更トランザクション)
Public Class PriceChangeCommand
Implements ICommand

Private _target As Product
Private _oldPrice As Decimal
Private _newPrice As Decimal

Public Sub New(target As Product, newPrice As Decimal)
_target = target
_oldPrice = target.Price
_newPrice = newPrice
End Sub

Public Sub Execute() Implements ICommand.Execute
_target.Price = _newPrice
End Sub

Public Sub Undo() Implements ICommand.Undo
_target.Price = _oldPrice
End Sub
End Class

‘ 履歴マネージャー(Undo / Redo エンジン)
Public NotInheritable Class TransactionHistoryManager
Private ReadOnly _undoStack As New Stack(Of ICommand)()
Private ReadOnly _redoStack As New Stack(Of ICommand)()
Private Const MAX_STACK_SIZE As Integer = 50 ‘ メモリリーク防止の境界値

”’

”’ 新規コマンドを実行し、Undoスタックに積む。Redoスタックはクリアする。
”’

Public Sub ExecuteCommand(cmd As ICommand)
cmd.Execute()
_undoStack.Push(cmd)
_redoStack.Clear() ‘ 新しいアクションの発生により、Redo履歴は破棄

‘ スタック溢れ防止(メモリ管理の鉄則)
If _undoStack.Count > MAX_STACK_SIZE Then
TrimStack(_undoStack)
End If
End Sub

”’

”’ Undo(元に戻す)
”’

Public Sub Undo()
If _undoStack.Count = 0 Then Return

Dim cmd As ICommand = _undoStack.Pop()
cmd.Undo()
_redoStack.Push(cmd)
End Sub

”’

”’ Redo(やり直し)
”’

Public Sub Redo()
If _redoStack.Count = 0 Then Return

Dim cmd As ICommand = _redoStack.Pop()
cmd.Execute()
_undoStack.Push(cmd)
End Sub

”’

”’ スタックの下底を切り捨てるためのヘルパー(Stackには直接RemoveBottomがないため)
”’

Private Sub TrimStack(targetStack As Stack(Of ICommand))
Dim tempArray = targetStack.ToArray()
targetStack.Clear()
‘ 古い順から最大数手前までを入れ直す
For i As Integer = MAX_STACK_SIZE – 1 To 0 Step -1
targetStack.Push(tempArray(i))
Next
End Sub
End Class

Public Class Product
Public Property Name As String
Public Property Price As Decimal
End Class
End Namespace

チーフアーキテクトの視点:メモリ管理の罠

`Stack` の要素数が無限に増えると、長時間の稼働でガベージコレクション(GC)の負荷増大やメモリ枯渇を招く。上記のコードでは、上限を超えた際に古い履歴を間引くロジックを組み込んでいる。大規模なオブジェクトを保持する場合は、強参照によるメモリリークを防ぐため、不要になった段階で `Stack.Clear()` を明示的に叩く勇気を持て。

3. 【実務応用2】`Queue(Of T)` を用いた非同期タスクバッファリングとスレッドセーフ制御

次に、外部APIとの連携や、大量の帳票PDF一括生成など、「順番に処理しなければならないが、メインスレッドをブロックしたくない」という極めて実務的なシナリオを考える。

`Queue(Of T)` はデフォルトではスレッドセーフではない。複数スレッドから同時に `Enqueue` / `Dequeue` が発生する場合、独自のロック機構を設けるか、.NETが提供するスレッドセーフなラッパー、あるいは `System.Collections.Concurrent.ConcurrentQueue(Of T)` を使用するべきである。
しかし、今回は「処理順序の厳密な保証」「バッファ溢れ時の制御」を学ぶために、あえて通常の `Queue(Of T)` を排他制御(`SyncLock`)下で運用する堅牢な実装を示す。

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

Namespace Enterprise.Threading

Public Class PrintJob
Public Property JobId As String
Public Property DocumentData As Byte()
End Class

”’

”’ スレッドセーフな印刷タスク・バッファリング・エンジン
”’

Public NotInheritable Class PrintQueueProcessor
Implements IDisposable

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

Public Sub New()
‘ バックグラウンドでの非同期処理ループを開始
_processingTask = Task.Run(Sub() ProcessQueueLoop(_cancellationTokenSource.Token))
End Sub

”’

”’ ジョブをキューの末尾に追加 (FIFO)
”’

Public Sub EnqueueJob(job As PrintJob)
If _disposed Then Throw New ObjectDisposedException(NameOf(PrintQueueProcessor))

SyncLock _lockObj
_queue.Enqueue(job)
End SyncLock
End Sub

”’

”’ バックグラウンドワーカーによる順次処理ループ
”’

Private Sub ProcessQueueLoop(token As CancellationToken)
While Not token.IsCancellationRequested
Dim job As PrintJob = Nothing

SyncLock _lockObj
If _queue.Count > 0 Then
job = _queue.Dequeue()
End If
End SyncLock

If job IsNot Nothing Then
Try
‘ 重い外部リソース処理(例:プリンター出力やAPI送信)
ExecutePrinting(job)
Catch ex As Exception
‘ 業務ログへの記録(例外フック)
Console.WriteLine($”Error processing job {job.JobId}: {ex.Message}”)
End Sub
Else
‘ キューが空の場合はCPUを無駄に占有しないようスリープ
Thread.Sleep(100)
End If
End While
End Sub

Private Sub ExecutePrinting(job As PrintJob)
‘ 実処理のシミュレーション
Thread.Sleep(500)
Console.WriteLine($”[Success] Printed Job ID: {job.JobId}”)
End Sub

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

_cancellationTokenSource.Cancel()
Try
‘ バックグラウンドタスクの終了を最大3秒待機
_processingTask.Wait(3000)
Catch
‘ タイムアウト等は無視
End Try

_cancellationTokenSource.Dispose()
SyncLock _lockObj
_queue.Clear()
End SyncLock
End Sub
End Class
End Namespace

チーフアーキテクトの視点:リソース解放の美学

業務アプリにおいて、バックグラウンドスレッドやキューを持つクラスは必ず `IDisposable` を実装させよ。アプリケーション終了時や画面破棄時に `Dispose` を呼ばず放置すると、スレッドが宙ぶらりんになり、メモリリークやプロセス終了の遅延(ハングアップのような挙動)を引き起こす。`SyncLock` による排他制御と、`CancellationToken` による協調的キャンセルは、プロフェッショナルのコードの必須条件である。

4. レガシーシステム(VBA / 旧VB6)からの脱却と移行戦略

もしあなたが現在、VBAやVB6の古いコードをVB.NET(.NET 6/8)へモダナイゼーション(近代化)するプロジェクトにいるならば、以下のマインドセットを持たなければならない。

1. コレクションの再定義からの解放:
VBAの `Collection` や、動的配列の `ReDim` は捨て去れ。型安全かつ高速なジェネリクス(`Of T`)の世界に飛び込むことで、実行時エラーの9割はコンパイル時エラーとして事前に駆逐できる。
2. 「インデックス思考」から「抽象データ構造思考」へのシフト:
「何番目の要素か」に依存するコードを書くのをやめ、「次に取り出すべきものは何か(`Pop` / `Dequeue`)」という振る舞いに注目して設計せよ。これがコードの保守性を劇的に向上させる。

総括

`Stack(Of T)` と `Queue(Of T)` は、単なるデータ構造の引き出しではない。業務システムのロジックにおいて、「時間をいかに制御するか」を司る極めて重要な武器である。

配列のインデックス操作という泥臭い実装から脱却し、LIFOとFIFOのセマンティクスを正しくコードに落とし込むこと。それこそが、レガシーの呪縛を断ち切り、真に堅牢でスケーラブルなシステムを構築するための唯一の道である。

妥協なきコードを書け。それがプロフェッショナルだ。

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