はじめに
VBAでよく見かける「実行時エラー9(インデックスが有効範囲にありません)」は、配列やシート名、コレクション参照などで頻繁に遭遇するエラーの一つです。その原因を知っていてもなぜか同じようにこのエラーを発生させてしまう方がいます。
このエラーは単なる「配列の範囲外アクセス」という表面的な問題ではなく、より深い根本原因があります。この記事では、エラーの対処法だけでなく、そもそもなぜこのエラーが発生するのかという本質を解説します。
そして、再発を防ぐための書き方のコツも紹介します。
エラーの根本原因
原因1. エラーメッセージの意味が分からない、直感的でない
根本原因の1つはエラーメッセージを読んでも意味が分からないということです。それはエラーメッセージの日本語がおかしいためです。
「実行時エラー ‘9’: インデックスが有効範囲にありません。」
このエラーメッセージを初心者が見たときに「インデックスってなに?」と感じても不思議ではありません。配列やコレクションのエラーだと気付けるのはかなりプログラミングに慣れた人でしょう。
このメッセージは主語が間違っています。本当の主語は「インデックス」ではなく「配列」や「コレクション」です。
もしエラーメッセージが、
「配列やコレクションに対して、存在しない番号や名前を指定しています」
と書いてあればどうでしょうか。初めてそれを見ても理解できて、あとは自分で対応できる方は結構多いのではないかと思います。
原因2. 配列の最後の番号を誤解している(要素数から1引いた数であることを強く認識できていない)
もう1つの根本原因は配列の最後の番号が何になるのか分からないという点です。
通常は配列が0開始であるということは理解されていると思いますが、では配列の最後は?と聞かれたときにすぐにこたえられるかどうかです。
- 要素数が3の配列の最後のインデックスは? → 答え: 2
- 要素数が10の配列の最後のインデックスは? → 答え: 9
もし答えられないのであれば、配列の先頭が0である、ということと同じぐらいに、配列の最後は要素数マイナス1、と覚えておきましょう。
これを感覚的に理解できていないと、ループ処理の終了条件を間違えてしまいます。
視覚的に理解する
要素数:3
最後のインデックス:2(= 要素数 – 1)
インデックス | 値 |
0 | “aaa” |
1 | “bbb” |
2 | “ccc” |
※ 配列の「長さ」と「最後の番号」は別物です。インデックス番号は0から始まるため、「最後の番号 = 要素数 – 1」となります。
一度でいいので、このような配列の絵と各要素の番号の絵を書いてみることをお勧めします。そして、配列の最後の要素と配列の要素数の関係を確認しましょう。その一度が今後の配列の理解に大きく役立ちます。
原因3. UBound関数の戻り値を誤解している
VBAで配列を扱う場合、要素数を取得するためにUBound関数をよく使いますが、ここに罠があります。
1 2 3 4 5 6 7 |
Dim ar() Redim ar(2) '// 2を指定すると要素数=3の、要素0, 1, 2の領域を作成する ar(0) = "aaa" ar(1) = "bbb" ar(2) = "ccc" Debug.Print UBound(ar) '// 出力=2(要素数の3ではない!) |
UBound関数は「要素数」ではなく「最後のインデックス番号」を返します。
- 要素数3の配列 → UBoundは2を返す
- 要素数10の配列 → UBoundは9を返す
この仕様を知らないと、ループの終了条件を間違えやすくなります。
このコードの2行目には配列の要素数を2と指定し、3行目で配列番号2を指定しています。このコードがやりたいのは、要素を3つ持つ配列の最後の要素に”ccc”を代入したい、ということですが、要素数が3であることが一目で分かりません。
このように、コードだけを見ると要素数と最大要素番号がずれていることに気が付かないことがあります。
UBound関数は要素数ではなく要素数マイナス1を返します。
UBound関数についての詳細は「配列の要素数を取得する(UBound、LBound)」をご参照ください。
原因4. Redimによる動的配列の誤解
1 2 |
Dim ar() As String ReDim ar(2) ' これは「要素数2」ではなく「最大インデックス2」を意味する |
ReDimで指定する数字は「要素数」ではなく「最大インデックス番号」です。
つまり、ReDim ar(2)は要素数3の配列を作成します。
Redimで指定するのは要素数マイナス1です。
エラーが発生する具体的なケース
ケース1. 配列の範囲外へアクセスしてエラーが発生するケース
もっとも一般的なエラー9の発生パターンが、以下のような配列を扱うケースです。
例えば、日曜日から土曜日までの7日間の配列を用意する場合、配列の要素は7つの領域を用意することになります。ところが誤って6つしか領域が用意されていない状態の場合に、7つ目の領域にアクセスするとエラー9になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Sub Err9Sample() Dim weekArray() As String '// 日曜日から金曜日までの領域を確保 ReDim weekArray(5) weekArray(0) = "日曜日" weekArray(1) = "月曜日" weekArray(2) = "火曜日" weekArray(3) = "水曜日" weekArray(4) = "木曜日" weekArray(5) = "金曜日" weekArray(6) = "土曜日" '// ここでエラー End Sub |
上のコードは設定時のエラーの例ですが、配列の値を参照しようとした場合もエラーになります。
ケース2: ループの終了条件ミス
ループ終了条件を誤った場合にエラー9が発生するケースもよくあります。
「UBound(ar)」のように配列の要素数を取得する際に、UBound関数は要素数ではなく要素数マイナス1を返すことを忘れて、あえて「UBound(ar) + 1」のように終了条件を書くとエラーになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Sub ErrorExample2() Dim ar(2) As String Dim i As Long ar(0) = "a" ar(1) = "b" ar(2) = "c" '// ×間違い: UBound(ar)は2なので、i=3のとき範囲外 For i = 0 To UBound(ar) + 1 Debug.Print ar(i) Next End Sub |
ケース3: コレクションの存在しないキー
配列だけでなく、コレクションでも発生します。
Dictionaryクラスのキーに存在しないキーを渡すと、エラーになります。
Dictionaryの詳細については「VBAのDictionaryの使い方(全メソッドとプロパティ網羅)」をご参照ください。
1 2 3 4 5 6 7 8 9 |
Sub ErrorExample3() Dim dic As Object Set dic = CreateObject("Scripting.Dictionary") dic("key1") = "値1" dic("key2") = "値2" Debug.Print dic("key3") ' エラー! key3は存在しない End Sub |
ケース4: ワークシートの存在しない名前
ワークシートを参照する際に、存在しない見出しのワークシートを参照するとエラーになります。
1 2 3 4 |
Sub ErrorExample4() '// "データ"という名前のシートが存在しない場合 Worksheets("データ").Activate '// エラー! End Sub |
エラーの対応方法
エラーの対応方法は、配列設定時や参照時のループ終了条件を正しく設定することでほとんど解決します。
配列のループ処理であればForループとUBound関数やFor Each構文を使うのが分かりやすくていいと思います。
対応方法1. For + UBoundでの書き方(基本)
UBoundを正しく使えば安全です。
12行目で要素数を取得し、それを15行目のForループで使用する方法です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Sub Err9Test3() Dim ar(3) Dim i Dim iLen ar(0) = "a" ar(1) = "b" ar(2) = "c" ar(3) = "d" '// 要素数-1を取得 iLen = UBound(ar) '// 全要素ループ For i = 0 To iLen Debug.Print ar(i) Next End Sub |
対応方法2. For Eachでの書き方(最もオススメ)
12行目のFor Eachで配列の各要素値を取得する方法です。インデックスを意識する必要がないため、この方法が一番簡単です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Sub Err9Test5() Dim ar(3) Dim i Dim s ar(0) = "a" ar(1) = "b" ar(2) = "c" ar(3) = "d" '// 全要素ループ For Each s In ar Debug.Print s Next End Sub |
対応方法3. Doループなどの終了条件がないループ構文での書き方
Doループなどの終了条件がないループ構文を使う場合は、配列処理を行う前に終了条件を書くようにする、などのルールを決めておくとよいです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
Sub Err9Test5() Dim ar(3) Dim i Dim iLen ar(0) = "a" ar(1) = "b" ar(2) = "c" ar(3) = "d" '// 要素数-1を取得 iLen = UBound(ar) '// よくあるミス(ループカウンタ判定ミス) Do '// 配列操作前に終了条件 If i > iLen Then Exit Do End If Debug.Print ar(i) i = i + 1 Loop End Sub |
対応方法4. コレクションの存在確認
コレクションからデータを参照する前に、そのインデックスのデータが存在しているかを確認してから、データを参照するようにするとエラーを回避できます。
以下はDictionaryクラスにキーが存在しているかをExistsメソッドで事前に確認して、あればDictionaryクラスからデータを取得するようにして、エラーを回避しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
Sub CorrectExample4_Dictionary() Dim dic As Object Set dic = CreateObject("Scripting.Dictionary") dic("key1") = "値1" dic("key2") = "値2" ' Existsメソッドで事前チェック If dic.Exists("key3") Then Debug.Print dic("key3") Else Debug.Print "key3は存在しません" End If ' 存在しないキーに対する安全な処理 Dim keyToCheck As String keyToCheck = "key2" If dic.Exists(keyToCheck) Then Debug.Print keyToCheck & "の値: " & dic(keyToCheck) Else ' キーが存在しない場合は新規作成 dic(keyToCheck) = "新しい値" End If End Sub |
対応方法5. ワークシートの存在確認
ワークシートからデータを参照する前に、そのシート名やインデックスのワークシートが存在しているかを確認してから、ワークシートを参照するようにするとエラーを回避できます。
以下はワークシートが存在しているかを事前に確認して、あればsheetExistsをTrueに設定して、後続処理でsheetExists変数の値を判定した上でシート操作を行い、エラーを回避しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Sub CorrectExample4() ' ワークシートの存在確認 Dim ws As Worksheet Dim sheetExists As Boolean sheetExists = False For Each ws In Worksheets If ws.Name = "データ" Then sheetExists = True Exit For End If Next If sheetExists Then Worksheets("データ").Activate Else MsgBox "シート'データ'が見つかりません" End If End Sub |
まとめ
- For Eachを優先する
配列の全要素を処理する場合、可能な限りFor Eachを使いましょう。インデックスの管理が不要で、エラー9が発生しません。 - 終了条件の記述位置を統一する
Do LoopやWhile文を使う場合、終了条件を書く位置(先頭、途中、最後)を自分の中で統一しましょう。一貫性があればミスが減ります。 - UBoundの意味を正しく理解する
UBound = 最後のインデックス番号(要素数ではない)、ということを覚えておきましょう。
配列の処理は出来るだけFor EachやFor文で書いた方が確実です。しかし、どうしてもDoループなどを使う必要がある場合は、終了条件をどこに書くかを自分の中で決めておいた方がよいです。
終了条件をループの先頭に書いたり最後に書いたり途中に書いたり、とバラバラだと、都度終了判定が > なのか >= なのかなどを考えなければなりません。
私は上のサンプルのように「終了条件はループの先頭に書く」と決めています。
もちろん例外はありますが、基本的なルールを決めておいた方が本件エラーに悩まされることは少なくなります。