配列インデックスの呪縛からの解放:`Stack(Of T)` と `Queue(Of T)` で描く堅牢な業務アプリ設計
業務システムの開発現場を見渡すと、いまだに `List(Of T)` の末尾を `RemoveAt` で削ったり、`Insert(0, …)` で無理やり先頭に割り込ませたりしているコードに出くわす。
「動けばいい」というフェーズを過ぎ、数万件のトランザクションや複雑な状態遷移を扱う中級者へのステップとして、この設計は直ちに改めるべきだ。
配列のインデックス計算や、要素のシフトに伴うメモリコピーのオーバーヘッドに悩まされるのはもう終わりにする。
今回は、LIFO(後入れ先出し)の `Stack(Of T)` と、FIFO(先入れ先出し)の `Queue(Of T)` を用い、「操作履歴の管理(Undo/Redo)」と「非同期タスクのバッファリング」を極限までエレガントかつ堅牢に実装する手法を伝授する。
—
1. なぜ「配列やList」での履歴・タスク管理は破綻するのか?
業務アプリでよくある要件として「入力データの状態管理(Undo/Redo)」や「受信した電文の順次処理」がある。これらを `List(Of T)` で実装すると、以下の致命的な問題に直面する。
- パフォーマンスの劣化: 先頭への要素追加(`Insert(0)`)は、既存の全要素をメモリ上で後ろにシフトさせるため、$O(N)$ のコストがかかり、データ量が増えるとUIがフリーズする。
- バグの温床: インデックスの指し示す位置(`currentIndex` など)の管理を誤ると、配列の境界外エラー(`ArgumentOutOfRangeException`)や、意図しないデータの上書きが発生する。
- 意図の曖昧さ: コードを読む他人が「このリストはスタックとして使いたいのか、ランダムアクセスしたいのか」が判別できず、保守性が著しく低下する。
データ構造には、それぞれの目的に最適化された形がある。「どうアクセスするか」が明確であれば、それを強制するコレクション(抽象データ型)を使うべきだ。それがプロフェッショナルの設計思想である。
—
2. 【LIFO構造】`Stack(Of T)` による洗練された Undo/Redo 実装
LIFO(Last-In, First-Out)の原則に従う `Stack(Of T)` は、「直前の状態に戻す」Undo機能の 구현において無類の強さを発揮する。
実務で通用する Undo/Redo マネージャーの設計
単にデータを積むだけでなく、メモリリークを防ぐための「最大履歴数(Capacity)」の制御と、Redo(やり直し)スタックのクリアロジックを組み込むのが実務的コードだ。
以下のコードは、テキストエディタやパラメータ設定画面の変更履歴を安全に管理するクラスの実装例である。
Imports System.Collections.Generic
Public Class StateManager(Of T)
‘ 操作履歴を保持するスタック (Undo用)
Private ReadOnly _undoStack As New Stack(Of T)()
‘ やり直し用のスタック (Redo用)
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 PushState(currentState As T)
_undoStack.Push(currentState)
‘ 新しい操作が行われたら、Redoスタックはクリアするのが定石
_redoStack.Clear()
‘ 最大数を超えた場合、古い履歴をパージする(Stackには直接容量制限がないため下層を再構築)
If _undoStack.Count > _maxHistorySize Then
TrimStack()
End If
End Sub
”’
”’
Public Function Undo(currentState As T, ByRef restoredState As T) As Boolean
If _undoStack.Count = 0 Then Return False
‘ 現在の状態を Redo スタック退避
_redoStack.Push(currentState)
‘ ひとつ前の状態を取り出す
restoredState = _undoStack.Pop()
Return True
End Function
”’
”’
Public Function Redo(currentState As T, ByRef restoredState As T) As Boolean
If _redoStack.Count = 0 Then Return False
‘ 現在の状態を Undo スタックに戻す
_undoStack.Push(currentState)
‘ やり直す状態を取り出す
restoredState = _redoStack.Pop()
Return True
End Function
”’
”’
Private Sub TrimStack()
‘ Stackは直接下から削除できないため、一度配列に退避して再構築する
Dim tempArray = _undoStack.ToArray()
_undoStack.Clear()
‘ 逆順に詰めていく(最新のものが上になるように)
For i As Integer = _maxHistorySize – 1 To 0 Step -1
_undoStack.Push(tempArray(i))
Next
End Sub
End Class
—
3. 【FIFO構造】`Queue(Of T)` による非同期タスクバッファリング
次に、FIFO(First-In, First-Out)の `Queue(Of T)` だ。
外部APIとの通信、大量のファイル一括処理、DBへのバルクインサート待ち行列など、「受け付けた順番通りに処理しなければならない」シーンでは `Queue(Of T)` が唯一無二の解となる。
排他制御を意識した堅牢なタスクワーカーの構築
業務アプリでは、バックグラウンドスレッドから安全にキューを操作し、デッドロックや競合を防ぐ設計が求められる。ここではスレッドセーフに動作するタスクバッファの骨組みを示す。
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks
Public Class TaskBufferManager
‘ 処理待ちタスクを保持するキュー
Private ReadOnly _taskQueue As New Queue(Of Func(Of Task))()
‘ スレッド間の競合を防ぐためのロックオブジェクト
Private ReadOnly _lockObj As New Object()
‘ 処理実行中フラグ
Private _isProcessing As Boolean = False
”’
”’
Public Sub EnqueueTask(taskItem As Func(Of Task))
SyncLock _lockObj
_taskQueue.Enqueue(taskItem)
End SyncLock
‘ 非同期で処理ループをキックする(多重起動防止)
Task.Run(Sub() ProcessQueueAsync())
End Sub
”’
”’
Private Async Sub ProcessQueueAsync()
SyncLock _lockObj
If _isProcessing Then Return
_isProcessing = True
End SyncLock
Try
Dim currentTask As Func(Of Task) = Nothing
While True
SyncLock _lockObj
If _taskQueue.Count = 0 Then
_isProcessing = False
Exit While
End If
‘ 先頭から取り出す(FIFOの真骨頂)
currentTask = _taskQueue.Dequeue()
End SyncLock
‘ タスクの実行(例外発生時も後続を止めないようハンドリング)
Try
Await currentTask.Invoke()
Catch ex As Exception
‘ 業務ログ出力やエラーハンドリングをここに記述
Console.WriteLine($”タスク処理エラー: {ex.Message}”)
End Try
End While
Catch ex As Exception
SyncLock _lockObj
_isProcessing = False
End SyncLock
End Try
End Sub
End Class
—
4. ファイル・データベース連携における実務上の注意点
`Stack` や `Queue` は非常に強力だが、メモリ上にデータを保持する性質上、以下のインフラ連携の罠に注意しなければならない。
1. アプリケーション異常終了時のデータロスト:
メモリ上の `Stack` や `Queue` に積まれたままの未処理タスクや操作履歴は、アプリがクラッシュした瞬間に消滅する。
- 対策: 重要な業務キューは、DBのテーブルやローカルのSQLite、または永続化ログ(WAL形式など)へ即時シリアライズして退避させる「永続キュー(Persistent Queue)」のパターンを検討すること。
2. メモリ肥大化(OutOfMemoryException):
無限に `Enqueue` や `Push` を許可すると、ユーザーが意図しない大量データ入力時にメモリを食いつぶす。
- 対策: 前述のコードのように上限値を設けるか、メモリ上に置くのはIDやキー(参照)のみにとどめ、実体データは都度DBからフェッチする設計に倒す。
—
5. まとめ
配列のインデックス操作や `List` の無理な使い回しは、コードを読みにくくし、パフォーマンスのボトルネックを生む。
- 「戻る・やり直し」が必要な場面には、状態の積層を美しく管理できる `Stack(Of T)` を使う。
- 「順序を守った処理待ち・バッファリング」が必要な場面には、先入れ先出しを担保する `Queue(Of T)` を使う。
これらを適切に選択・配置できるようになるだけで、あなたの書くVB.NETコードの品質は一段と洗練され、プロフェッショナルとしての説得力を帯びるはずだ。
明日からの実装で、ぜひ「配列思考」から脱却し、目的駆動型のコレクション選択を実践してほしい。
