【テクニカル・上級編】初心者向け:VB.NETのDo…LoopとWhile…End Whileの使い分け:無限ループを絶対に回避する条件判定と可読性向上テクニック – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

VB.NETにおけるDo…LoopとWhile…End While:無限ループを狩るための鉄則と、シニアエンジニアの視点

長年、VBAからVB6、そして.NET Framework、さらには.NET Coreへと、業務システムの世界を渡り歩いてきた。その中で幾度となく遭遇し、その都度、己の技術で克服してきたのが「無限ループ」という名の悪夢だ。特に、VB.NETで繰り返し処理を設計する際、`Do…Loop` と `While…End While` のどちらを選ぶべきか、そしてその条件判定をどう設計するかは、システム全体の安定稼働を左右する極めて重要な問題である。

本稿では、単なる構文の解説に終始するのではなく、我々のようなレガシーシステム保守やWindows API連携を熟知したエンジニア、あるいは社内システム管理者が直面するであろう、より実践的で、より深いレベルでの「繰り返し処理」の真髄を、魂を込めて紐解いていく。

1. 繰り返し処理の二大巨頭:Do…LoopとWhile…End Whileの動作特性

まず、両者の基本的な動作特性を、その「癖」まで含めて理解することが、無限ループ回避の第一歩である。

1.1. `While…End While`:条件が真の間、実行を繰り返す

`While…End While` は、その名の通り、指定した条件が `True` である間、ブロック内の処理を繰り返し実行する。

.net
Dim counter As Integer = 0

While counter < 5 ' ここでの処理 Console.WriteLine($"Whileループ: counter = {counter}") ' カウンタをインクリメント。これを忘れると無限ループ! counter += 1 End While ここでの注意点:

  • 条件判定のタイミング: ループに入る前に一度、ループを抜けた後に一度、条件判定が行われる。つまり、ループ本体の処理が実行される前に条件が満たされなくなれば、一度も実行されずに終了する。
  • ループ本体での状態変化: ループが継続するかどうかは、ループ本体で `counter` のような条件判定に使われる変数の値が変化することに依存する。この変化を設計段階で確実に見通せなければ、無限ループの温床となる。

1.2. `Do…Loop`:実行後に条件判定、または実行前に条件判定

`Do…Loop` は、さらに柔軟な制御が可能だが、その分、理解を誤ると危険も増す。主に以下の2つの形式がある。

1.2.1. `Do While…Loop` (または `Do…Loop While`):ループ本体実行後の条件判定

.net
Dim counter As Integer = 0

Do
‘ ここでの処理
Console.WriteLine($”Do Whileループ (実行後判定): counter = {counter}”)

‘ カウンタをインクリメント。
counter += 1
Loop While counter < 5 ここでの注意点:

  • 最低1回の実行保証: `Loop While` の形式では、条件判定がループの最後に置かれるため、条件が最初から満たされていなくても、ループ本体は最低1回は必ず実行される。これは、初期値に関わらず一度は処理を行いたい場合に有効だが、意図しない動作につながる可能性もある。
  • 条件判定のタイミング: ループ本体の処理が実行された「後」に条件判定が行われる。

1.2.2. `Do Until…Loop` (または `Do…Loop Until`):条件が満たされるまで実行

`Until` は `Not While` と同義と考えると分かりやすい。

.net
Dim counter As Integer = 0

Do Until counter >= 5
‘ ここでの処理
Console.WriteLine($”Do Untilループ: counter = {counter}”)

‘ カウンタをインクリメント。
counter += 1
Loop

ここでの注意点:

  • `Do Until` (実行前判定): `While…End While` と同様に、ループに入る前に一度条件判定が行われる。
  • `Do…Loop Until` (実行後判定): `Do While…Loop` と同様に、ループ本体は最低1回は必ず実行される。

2. 無限ループを狩るための鉄則:条件判定の設計思想

無限ループの発生原因は、ほとんどの場合、ループの継続条件が永続的に満たされ続ける、あるいは、ループ本体でその条件が変化しないことにある。これを回避するための設計思想を、実務的な観点から解説しよう。

2.1. 「出口」を明確に設計せよ

ループは、必ず「いつかは終わる」という出口が設計されていなければならない。

  • カウンタベース: `counter < 5` のように、明確な終了条件となるカウンタ変数を用意する。
  • フラグベース: `While isProcessing` のように、外部イベントや処理の完了を示すフラグ変数を用意する。このフラグは、ループ本体で必ず `False` になるように設計する。
  • データ量ベース: ファイルの終端、配列の終端、データベースのレコード終端など、処理対象のデータ量が尽きることを条件とする。

2.2. ループ本体での「状態変化」を徹底管理せよ

ループが継続するための条件変数は、ループ本体で必ず「変化」しなければならない。

  • カウンタのインクリメント/デクリメント: 常に忘れない。
  • フラグの変更: 処理が完了したら、必ずフラグを `False` にする。
  • データポインタの移動: ファイル読み込みなら次の行へ、配列なら次の要素へ、ポインタを進める。

2.3. 「最小実行回数」を意識した選択

  • 最低1回は実行したい場合: `Do…Loop While` または `Do…Loop Until` が適している。例えば、初期設定処理を一度だけ行い、その後条件を評価したい場合など。
  • 条件を満たさない場合は実行したくない場合: `While…End While` または `Do While…Loop` (実行前判定) が適している。例えば、特定のデータが存在する場合のみ処理を実行したい場合など。

2.4. Windows API呼び出しにおける注意点

Windows APIの呼び出しを伴う場合、ループの設計はさらに複雑になる。

  • 非同期処理: `WaitForSingleObject` のようなAPIは、指定したオブジェクトの状態が変化するまでスレッドをブロックする。これは一種のループだが、API側で適切にタイムアウトや状態監視が行われているため、通常は無限ループの心配はない。しかし、APIの仕様を正確に理解していないと、予期せぬ遅延やハングアップの原因となる。
  • メッセージループ: GUIアプリケーションでは、メッセージループ (`Application.Run` や `GetMessage`/`TranslateMessage`/`DispatchMessage` の組み合わせ) が存在する。これは、ユーザー操作やシステムイベントを待ち受けるための無限ループ(`WM_QUIT` メッセージで終了)であり、これを不用意に中断したり、ループ内で重い処理を長時間実行したりすると、UIの応答性が著しく低下する。
  • リソースの解放: API呼び出しでは、ハンドルやメモリなどのリソースを明示的に解放する必要がある場合が多い。ループ内でこれらのリソースを確保し、解放し忘れると、メモリリークやリソース枯渇を招き、間接的にシステム全体の不安定化につながる。

.net
‘ 例:Windows API (CreateFile) を使用し、ファイルアクセスを繰り返す場合
‘ ファイルハンドルはループの外で初期化し、ループ内で使用、ループ終了後に解放する
Dim fileHandle As IntPtr = IntPtr.Zero

Try
‘ ファイルを開く (ループに入る前に一度だけ)
fileHandle = CreateFile(“path\to\your\file.txt”, GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero)

If fileHandle <> INVALID_HANDLE_VALUE Then
Dim buffer(1023) As Byte
Dim bytesRead As Integer = 0

‘ ファイルの内容を読み込むループ
‘ ReadFile API は、読み込んだバイト数を bytesRead に返す。
‘ ファイルの終端に達すると 0 を返す。
While ReadFile(fileHandle, buffer, buffer.Length, bytesRead, IntPtr.Zero) AndAlso bytesRead > 0
‘ 読み込んだデータを処理する
Console.WriteLine($”Read {bytesRead} bytes.”)

‘ ここでバッファの内容を解析するなど
‘ …

‘ 次の読み込みのために bytesRead をリセットする (ReadFile が自動で設定してくれるが、念のため)
bytesRead = 0
End While
Else
‘ ファイルオープン失敗時のエラー処理
Console.WriteLine(“Failed to open file.”)
End If

Catch ex As Exception
‘ 例外処理
Console.WriteLine($”An error occurred: {ex.Message}”)
Finally
‘ ファイルハンドルを必ず解放する
If fileHandle <> IntPtr.Zero Then
CloseHandle(fileHandle)
fileHandle = IntPtr.Zero ‘ 解放後は無効な値にする
End If
End Try

‘ —– Windows API宣言 (抜粋) —–
Private Declare Function CreateFile Lib “kernel32.dll” Alias “CreateFileA” ( _
ByVal lpFileName As String, _
ByVal dwDesiredAccess As Integer, _
ByVal dwShareMode As Integer, _
ByVal lpSecurityAttributes As IntPtr, _
ByVal dwCreationDisposition As Integer, _
ByVal dwFlagsAndAttributes As Integer, _
ByVal hTemplateFile As IntPtr _
) As IntPtr

Private Declare Function ReadFile Lib “kernel32.dll” ( _
ByVal hFile As IntPtr, _
ByVal lpBuffer As Byte(), _
ByVal nNumberOfBytesToRead As Integer, _
ByRef lpNumberOfBytesRead As Integer, _
ByVal lpOverlapped As IntPtr _
) As Boolean

Private Declare Function CloseHandle Lib “kernel32.dll” ( _
ByVal hObject As IntPtr _
) As Boolean

Private Const INVALID_HANDLE_VALUE As Integer = -1
Private Const GENERIC_READ As Integer = &H80000000L
Private Const FILE_SHARE_READ As Integer = &H1
Private Const OPEN_EXISTING As Integer = 3
Private Const FILE_ATTRIBUTE_NORMAL As Integer = &H80

このコード例のポイント:

  • `Try…Finally`: リソース解放の確実性を高めるために必須。
  • `CloseHandle`: APIで取得したハンドルは、使い終わったら必ず解放する。`fileHandle` が `IntPtr.Zero` でないことを確認してから解放する。
  • `ReadFile` の戻り値と `bytesRead`: `ReadFile` は成功した場合に `True` を返し、読み込んだバイト数を `bytesRead` に格納する。ファイルの終端に達した場合は `True` を返すが、`bytesRead` が `0` になる。この `bytesRead > 0` という条件が、ループを抜けるための出口となる。

2.5. メモリ最適化とオブジェクトのライフサイクル

VB.NETでは、`For Each` や `For` ループなどでコレクションを走査する際に、一時的なイテレータオブジェクトが生成されることがある。また、ループ内で大量のオブジェクトを生成・破棄する場合、GC (Garbage Collector) の負荷が増大する。

  • オブジェクトの明示的解放: `IDisposable` インターフェースを実装したオブジェクト(特にファイルストリームやデータベース接続など、アンマネージドリソースを扱うもの)は、`Using` ステートメントを使って確実に解放することが推奨される。`Using` ステートメントは、ブロックを抜ける際に自動的に `Dispose` メソッドを呼び出すため、`Try…Finally` と同様、あるいはそれ以上の確実性でリソースを解放できる。

.net
‘ ファイルストリームを安全に扱う例
Dim filePath As String = “data.txt”

‘ Using ステートメントは、ブロック終了時に自動的に Dispose を呼び出す
Using fs As New System.IO.FileStream(filePath, System.IO.FileMode.OpenOrCreate)
Using sr As New System.IO.StreamReader(fs)
Dim line As String
‘ ファイルの終端まで1行ずつ読み込む
While (line = sr.ReadLine()) IsNot Nothing
Console.WriteLine(line)
‘ ここで line を処理する
End While
End Using ‘ sr.Dispose() が自動的に呼ばれる
End Using ‘ fs.Dispose() が自動的に呼ばれる

  • 不要なオブジェクトの早期破棄: ループ内で一時的にしか使用しないオブジェクトは、スコープを限定するか、明示的に `Nothing` を代入してGCに回収を促すことも有効だが、過剰な `Nothing` 代入は可読性を損なう可能性もある。基本的には、スコープを適切に管理すればGCが効率的に処理してくれる。

3. 可読性向上のためのテクニック

無限ループ回避と並行して、コードの可読性を高めることは、保守性を著しく向上させる。

3.1. 命名規則の徹底

ループカウンタやフラグ変数には、その役割が明確に分かる名前を付ける。

  • `i`, `j`, `k`:単純なカウンタには許容される場合もあるが、より複雑なループでは `itemIndex`, `retryCount`, `isFinished` のように具体的にする。
  • `processDataFlag`:処理が進行中かどうかを示すフラグ。
  • `maxRetries`:再試行の上限回数。

3.2. コメントによる「意図」の明示

なぜそのループが必要で、どのような条件で終了するのかをコメントで明記する。これは、後でコードを読んだ自分自身のためでもある。

.net
‘ 外部システムからの応答を最大 30 秒間、5 秒間隔でポーリングする
Dim startTime As DateTime = DateTime.Now
Dim timeoutSeconds As Integer = 30
Dim pollIntervalMilliseconds As Integer = 5000
Dim responseReceived As Boolean = False

While Not responseReceived AndAlso (DateTime.Now.Subtract(startTime).TotalSeconds < timeoutSeconds) ' 外部システムにポーリングリクエストを送信 SendPollingRequest() ' 応答を確認 If IsResponseAvailable() Then responseReceived = True ' 応答データを処理 ProcessResponse() Else ' 応答がない場合、指定間隔待機 System.Threading.Thread.Sleep(pollIntervalMilliseconds) End If End While ' タイムアウトしたか、応答があったかの判断 If Not responseReceived Then Console.WriteLine("Polling timed out.") ' タイムアウト時のエラー処理 Else Console.WriteLine("Response received.") End If この例では、`While` の条件式自体がタイムアウト処理を含んでいるが、コメントでその意図を明確にしている。

3.3. 処理の責務を分離する

ループ本体が複雑になりすぎる場合は、その処理を別のメソッドに切り出す。これにより、ループ構造自体はシンプルに保たれ、条件判定とループ本体のロジックが混在するのを防ぐ。

4. レガシー環境との連携における極限の知見

長年、VB6やCOMコンポーネント、あるいはC++で書かれたDLLなど、レガシーな資産と連携する機会は多い。

  • COMオブジェクトのライフサイクル管理: COMオブジェクトは、通常 `_Release` メソッド(または `Marshal.ReleaseComObject`)で参照カウントをデクリメントする必要がある。ループ内でCOMオブジェクトを繰り返し生成・使用する場合、参照カウントが正しく管理されないと、メモリリークやリソースの残留を引き起こす。`Marshal.ReleaseComObject` は、COMオブジェクトがまだ使用されている可能性がある場合でも、強制的に参照カウントをデクリメントするため、慎重な使用が求められる。
  • VB6互換モードでの罠: .NET Frameworkでは、VB6との互換性を保つための機能が存在するが、その挙動を完全に理解せずループ処理を実装すると、予期せぬパフォーマンス低下やエラーを招くことがある。特に、オブジェクトの型変換やメソッド呼び出しにおいては、VB6の暗黙的な型変換と.NETの厳密な型チェックの違いに注意が必要だ。

5. システム間連携における極限の知見

データベース、Web API、メッセージキューなど、複数のシステムと連携する際、ループ処理は非同期処理やエラーハンドリングと密接に関わる。

  • タイムアウトとリトライ: 外部システムへのリクエストが失敗した場合、一定回数のリトライを設けることは一般的だ。このリトライ処理をループで実装する際には、リトライ間隔の指数関数的な増加(Exponential Backoff)などを考慮し、過度な負荷をかけないように設計する必要がある。
  • トランザクション管理: データベースへの大量データ書き込みなどでループを使用する場合、トランザクションの範囲を適切に区切ることが重要だ。ループ全体を一つのトランザクションにすると、長時間ロックが維持されたり、メモリ使用量が増大したりする可能性がある。一定件数ごとにコミットする、といったバッチ処理的なアプローチが有効である。

.net
‘ データベースへのバッチ書き込み例
Dim batchSize As Integer = 1000
Dim recordsProcessed As Integer = 0
Dim totalRecords As Integer = GetTotalRecordCount() ‘ 総レコード数を取得

Using connection As New SqlConnection(“YourConnectionString”)
connection.Open()

Dim command As New SqlCommand()
command.Connection = connection

‘ SQL文を準備 (例: INSERT 文)
command.CommandText = “INSERT INTO YourTable (Column1, Column2) VALUES (@Param1, @Param2)”
command.Parameters.Add(“@Param1”, SqlDbType.VarChar, 50)
command.Parameters.Add(“@Param2”, SqlDbType.Int)

Dim transaction As SqlTransaction = connection.BeginTransaction()
command.Transaction = transaction

Try
‘ 全てのレコードを処理するループ
While recordsProcessed < totalRecords Dim recordsInBatch As Integer = 0 ' バッチサイズ分、または残りのレコードを処理 While recordsInBatch < batchSize AndAlso recordsProcessed < totalRecords ' データの取得 (例) Dim data1 As String = GetNextData1() Dim data2 As Integer = GetNextData2() ' パラメータの設定 command.Parameters("@Param1").Value = data1 command.Parameters("@Param2").Value = data2 ' SQL実行 command.ExecuteNonQuery() recordsProcessed += 1 recordsInBatch += 1 End While ' バッチコミット transaction.Commit() Console.WriteLine($"Committed batch of {recordsInBatch} records. Total processed: {recordsProcessed}") ' 次のトランザクションを開始 transaction.Dispose() ' 前のトランザクションを破棄 transaction = connection.BeginTransaction() command.Transaction = transaction End While ' 最後のトランザクションをコミット(もしあれば) If transaction IsNot Nothing Then transaction.Commit() End If Catch ex As Exception ' エラー発生時はロールバック If transaction IsNot Nothing Then Try transaction.Rollback() Console.WriteLine($"Transaction rolled back due to error: {ex.Message}") Catch tex As Exception Console.WriteLine($"Error during rollback: {tex.Message}") End Try End If ' エラーを上位に投げるか、適切に処理する Throw ex Finally ' トランザクションオブジェクトの解放 If transaction IsNot Nothing Then transaction.Dispose() End If End Try End Using

まとめ

`Do…Loop` と `While…End While` の選択は、単なる構文上の好みではなく、「いつ条件判定が行われ、最低何回実行されるのか」という動作特性を理解した上での、システム要件に基づいた設計判断である。

我々が日々向き合う業務システム、特にレガシー環境や複雑なシステム連携においては、潜在的なリスクを排除し、堅牢なコードを書き上げることが最優先事項となる。無限ループは、そのリスクの代表格だ。

今回解説した、条件判定の設計思想、API連携時の注意点、リソース管理、そして可読性向上テクニックは、単なる「書き方」ではなく、「システムを安定稼働させるための哲学」である。これらの知見を血肉とし、日々のコーディングに活かしていくことこそが、我々エンジニアに課せられた使命であると、私は信じている。

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