VB.NETにおける配列とList(Of T)の極意:メモリ構造から読み解くコレクション選択の鉄則
業務効率化ツールや社内システムを開発する中で、以下のようなコードを目にしたことはないでしょうか。
‘ 【アンチパターン】ループ内で ReDim Preserve を繰り返すコード
Dim lines() As String
Dim count As Integer = 0
Do While reader.Peek() >= 0
ReDim Preserve lines(count)
lines(count) = reader.ReadLine()
count += 1
Loop
「動くからこれでいい」――もしあなたがそう考えているなら、今すぐその認識を改める必要があります。このようなコードは、データ件数が数千、数万件に達した瞬間に急激なパフォーマンス低下を引き起こし、最終的にはメモリの枯渇(`OutOfMemoryException`)やガベージコレクション(GC)のスパイクを招きます。
本稿では、VB.NETにおける「配列(Array)」と「`List(Of T)`」の真の相違点を、メモリ構造(ヒープ割り当てとアドレス空間)の視点から徹底的に解剖します。なぜ特定の選択がバグや速度低下を生むのか、その論理的な理由と、実務で絶対に破ってはならない設計鉄則を伝授します。
—
1. メモリの裏側:固定長と可動長の本質
なぜ `ReDim Preserve` は「罪悪」なのか。それを理解するには、.NET Runtime のマネージドヒープ上で両者がどう扱われているかを知る必要があります。
配列(Array):連続した固定メモリブロック
配列は、メモリ上に指定された要素数分の連続した領域(Contiguous Memory Block)を単一のオブジェクトとして確保します。
[配列のメモリイメージ (要素数 3)]
+———+———+———+
| Index 0 | Index 1 | Index 2 |
+———+———+———+
(連続した単一のメモリ領域)
- 長所: 要素へのインデックスアクセスは、先頭アドレスからの単純なオフセット計算(`先頭 + (サイズ Index)`)で実行されるため、$O(1)$ の最速パフォーマンスを誇ります。
- 短所: 一度領域が決定すると、後からサイズを変更することは物理的に不可能です。
`ReDim Preserve` の正体
VB.NETの `ReDim Preserve` は、既存のメモリ領域を伸ばしているわけではありません。内部では以下の恐ろしい処理が自動実行されています。
1. 新しいサイズの配列を全く別のメモリ領域に新規確保する。
2. 旧配列の要素を新配列へ全件コピーする。
3. 旧配列の参照を破棄し、GC(ガベージコレクター)の回収対象にする。
これをループ処理の中で毎回行うと、$O(N^2)$ の再割り当てとメモリコピーが発生します。さらに、短寿命な大容量オブジェクトがヒープ上に大量生成されることで、LOH(Large Object Heap)の断片化とGCの頻発を引き起こし、システム全体のパフォーマンスを致命的に破壊します。
—
List(Of T):内部配列の自動隠蔽と動的拡張戦略
`List(Of T)` は、魔法の可変長配列ではありません。その実体は、「内部で固定長配列を抱え、必要に応じて賢く再構築するラッパークラス」です。
[List(Of T) の内部構造]
List(Of T) オブジェクト
├── Count (現在の要素数: 3)
├── Capacity (内部配列の容量: 4)
└── items() ───> [ Elem0 | Elem1 | Elem2 | (Empty) ]
`List(Of T)` に要素を追加(`.Add()`)した際、内部配列の容量(`Capacity`)が不足すると、`List(Of T)` は自動的に現在の容量を2倍に拡張した新しい内部配列を確保し、一括コピーします。
- 倍加戦略(Doubling Strategy): 要素を追加するたびではなく、「容量が満杯になった時だけ2倍にする」ことで、メモリ再割り当ての頻度を劇的に減らします(ならし解析で計算量は $O(1)$ に収束)。
—
2. プロフェッショナルが適用するコレクション選択の鉄則
実務設計において、どちらを使用すべきかの判断基準は極めて明快です。感性や慣習ではなく、以下の決定マトリクスに基づいて論理的に選択してください。
| 判断軸 | 配列 (`T()`) | `List(Of T)` |
| :— | :— | :— |
| 要素数 | 固定(初期化時に決定し、以降変動しない) | 可変(実行時に増減する) |
| メモリ構造 | 連続した単一ブロック(オーバーヘッド最小) | 内部配列+管理用メタデータ |
| 主な用途 | ・ルックアップテーブル
・固定長のバイナリデータ(`Byte()`)
・`Params` 引数
・API相互運用 | ・ファイル/DBからのデータロード
・ユーザー入力等の不確定データの保持
・動的なフィルタリングや加工処理 |
| 変更操作 | 不可(`ReDim` は禁止) | `Add`, `Remove`, `AddRange` などが自在 |
| 公開プロパティ | レイヤー境界での不変配列として参照 | 内部状態の隠蔽には不向き(`IReadOnlyList`を検討) |
鉄則1:要素数が確定しているなら「配列」一肢
CSVの1行を分割した結果(`String.Split`)や、固定の月次パラメータ(12ヶ月分)など、サイズが最初から確定している場合は配列を使用します。無駄なインスタンスオーバーヘッドや、将来的な容量拡張の無駄を排除するためです。
鉄則2:要素数が動的なら迷わず「`List(Of T)`」
データベースからの検索結果取得、外部APIのストリーム受信、条件に一致するオブジェクトの抽出など、件数が事前に読めない場合は絶対に `List(Of T)` を使用してください。
—
3. 実務で勝つプロダクションコード
ここからは、実務で頻繁に発生する「外部ファイル(CSV)からデータを読み込み、ビジネスロジックを適用してデータベースへ一括登録する」堅牢なコード例を示します。
バグの混入を防ぐ型安全な設計、リソース管理(`Using`)、メモリ効率を極限まで高めるテクニック(`Capacity`の事前割り当て)を網羅しています。
【実践例】高パフォーマンスなデータバッチ処理モジュール
Imports System.IO
Imports System.Collections.Generic
”’
”’
Public Class SalesRecord
Public Property CustomerId As String
Public Property Amount As Decimal
Public Property RecordDate As DateTime
Public Sub New(customerId As String, amount As Decimal, recordDate As DateTime)
Me.CustomerId = customerId
Me.Amount = amount
Me.RecordDate = recordDate
End Sub
End Class
Public Class SalesDataProcessor
”’
”’
”’ 対象CSVファイルのパス
”’
Public Function LoadSalesDataDataFromFile(filePath As String) As List(Of SalesRecord)
If Not File.Exists(filePath) Then
Throw New FileNotFoundException(“指定されたファイルが存在しません。”, filePath)
End If
‘ 【重要】概算の行数が読める場合は Capacity を明示指定し、内部再割り当てを防止する
‘ 例: 一般的なファイルサイズからおよその要素数を予測(指定しない場合はデフォルト値 4 からスタート)
Dim estimatedCapacity As Integer = 1000
Dim validRecords As New List(Of SalesRecord)(estimatedCapacity)
‘ ストリームリソースの確実な破棄を保証
Using reader As New StreamReader(filePath, System.Text.Encoding.UTF8)
Dim lineNumber As Long = 0
Do While Not reader.EndOfStream
lineNumber += 1
Dim line As String = reader.ReadLine()
‘ 空行のスキップ
If String.IsNullOrWhiteSpace(line) Then Continue Do
‘ 【配列の正しい使い所】Split結果のように要素数が固定される場合は配列で受ける
Dim fields As String() = line.Split(“,”c)
‘ データ形式のバリデーション(堅牢性の担保)
If fields.Length < 3 Then
' 実務ではログ出力等を行い、不正行をスキップまたは例外処理
Continue Do
End If
Dim customerId As String = fields(0).Trim()
Dim amount As Decimal
Dim recordDate As DateTime
' 型変換と安全なパース
If Decimal.TryParse(fields(1), amount) AndAlso
DateTime.TryParse(fields(2), recordDate) Then
' ドメインオブジェクトを生成してListに追加
' List(Of T) の Add は非常に高速 (計算量: 平均 O(1))
validRecords.Add(New SalesRecord(customerId, amount, recordDate))
Else
' ログ記録: $ "Line {lineNumber}: パースエラー"
End If
Loop
End Using
' 【プロの技】使用メモリの最適化
' 確保された Capacity が実際要素数より大幅に大きい場合、不要なメモリ領域を即座に解放する
validRecords.TrimExcess()
Return validRecords
End Function
'''
”’
Public Sub BulkInsertToDatabase(records As IReadOnlyList(Of SalesRecord))
‘ 呼び出し元でリストが変更されないよう IReadOnlyList(Of T) でインターフェースを固定する
If records Is Nothing OrElse records.Count = 0 Then Return
‘ データベースアクセスの擬似コード
‘ 外部APIや既存レガシーDBプロシージャが配列を要求する場合は .ToArray() で渡す
Dim recordArray As SalesRecord() = records.ToArray()
‘ ExecutedBulkInsert(recordArray) …
End Sub
End Class
—
4. チーフアーキテクトが教える高度な最適化テクニック
さらに一歩進んだ、システム全体の保守性と速度を飛躍させる知識を解説します。
1. `List(Of T)` の `Capacity` 事前設定(再割り当ての根絶)
`List(Of T)` をデフォルトコンストラクタ `New List(Of T)()` で生成すると、初期容量は `0`(または `4`)です。要素を追加していくと、`4 -> 8 -> 16 -> 32 -> 64 …` と内部配列のコピーが頻繁に発生します。
追加される要素数の概算が分かっている場合は、必ず初期容量を指定してください。
‘ 10,000件追加されることが予測できている場合
‘ 内部配列の再割り当てオーバーヘッドが完全にゼロになる
Dim orderList As New List(Of Order)(10000)
2. クラスの外部公開プロパティにおける設計原則
クラスのフィールドとして持つコレクションを外部に公開する場合、`List(Of T)` をそのまま `Public` プロパティとして晒してはいけません。外部から勝手に `.Clear()` や `.Add()` を呼ばれ、カプセル化が破壊されます。
Public Class OrderManager
‘ 内部状態は List(Of T) で保持(動的変更のため)
Private ReadOnly _orders As New List(Of Order)()
‘ 外部には変更不可な IReadOnlyList(Of T) として公開する
Public ReadOnly Property Orders As IReadOnlyList(Of Order)
Get
Return _orders
End Get
End Property
Public Sub AddOrder(newOrder As Order)
‘ バリデーション logic
_orders.Add(newOrder)
End Sub
End Class
3. LINQ 乱用による隠れパフォーマンスハザードの回避
LINQ の `.ToList()` や `.ToArray()` は極めて便利ですが、呼び出した瞬間に新しいメモリ領域を確保して全要素を評価・コピーします。
‘ 【非効率】不要な ToList() によって無駄な中間インスタンスがメモリに生成される
Dim result = dataList.Where(Function(x) x.IsActive).ToList().FirstOrDefault()
‘ 【最適】列挙を評価せず、条件に合う最初の要素だけを取得する
Dim result = dataList.FirstOrDefault(Function(x) x.IsActive)
—
まとめ
- 配列(`T()`)は「固定長・最速・最小メモリ」。サイズが決まっている静的データや、外部APIとの境界で使用する。`ReDim Preserve` はコードから排除せよ。
- `List(Of T)` は「可変長・高機能」。要素数が不確定な処理のファーストチョイス。ただし、可能であれば `Capacity` を初期指定して内部コピーを防ぐ。
- カプセル化の観点から、公開プロパティには `IReadOnlyList(Of T)` を検討し、堅牢なクラス設計を意識する。
仕組み(メモリ)を知り、コレクションの特性を正しく理解して選択する。この一歩を踏み出すことで、あなたの記述するVB.NETコードは「ただ動くコード」から「プロフェッショナルが唸る堅牢で美麗なコード」へと昇華します。開発現場のリーダーとして、チーム全体へこの選択鉄則を波及させてください。
