ほりすてぃっくうぃずだむ

アーカイブだったりレポートだったり駄文だったり思ったことをなんでもてきとーに書く

システムエラー &H800401A8(-2147221080)のお話

たまにはプログラマっぽい話でも。

今の職務上、ゴリゴリ開発するってわけじゃないんですが、VBA組んだりは日常的にしております。Javaに触ることはめっきり減っちゃいましたが。やっててよかったVBA

んで、以前自分で開発していたツールで、こんなエラーがでて困っていたわけです。

f:id:isk_holistic:20200122094231p:plain
システムエラーです:&H800401A8(-2147221080)

ほげぇぇぇぇ!

H800401A8をダブルクォートで囲って(でないと似たようなエラーコードの記事が引っかかっちゃうからね)検索してみて出てきたStackOverFlowの記事がこちら。

excel - Issues with Multiple Runtime errors: updated code - still having issues - Stack Overflow

どうもWorksheetに触ろうとしたときのエラーだと。
んで、その解決に、「ファイル名をいじった」と。
よくわからぬ…。

なんかうまく再現させることもできなかったので、対症処置で何とかなることもあって放置したんですが、ひょんなことから理由がわかりました。
エラーコードググればわかりますが、あんまり情報もないみたいなので、ここは別にtechblogではないですが、ソースコードを一部公開しておきます。

Dim filePath As String
filePath = "C:\hogehoge_NoXXXX.xlsx"

' ファイルオープン時のエラーは処理しない
On Error Resume Next
Workbooks.Open Filename:=filePath

Dim wb As Workbook
Set wb = Workbooks("hogehoge_NoXXXX.xlsx")

' ファイルが開けているかチェック
If (wb Is Nothing) Then
    Msg "ファイルが開けていません。"
    Exit Sub
End If

Dim ws As WorkSheet
Set ws = wb.Worksheet("ぴよぴよ")

On Error GoTo 0

' ここでおちる
ws.Select

これは一連のループの中の処理で、NoXXXXには何らかの番号が入り、動的に生成されます。
ほんで、あるものについては問題なくWorksheetをSelect出来るのに、できないことがあった、と。

で、これなぜ起きていたかというと、

  1. まず、そもそもファイルが存在しない場合がある
  2. 存在しないファイルをWorkbooks.Openした場合、エラーになるが、On Error Resume Nextしているので、処理自体が無視される
  3. 開いていないブックをSetしようとすると、エラーになるが、以下略
  4. 結果、変数wbは、初期化されていない。「初期化されていない変数」は「変数なし」状態であり、これはNothingではない*1
  5. よって、If(wb Is Nothing) Thenは常にFalseである(エラー処理になっていない)
  6. 以下wsについても同様
  7. 変数なしの状態のwsを触ろうとするため、エラーとなる

Javaとか慣れてると、Object obj = new Object();とか呼吸だし、たとえしなかったとしてもnullであるので、IsNullすればいいと思っちゃうけど、VBAでは宣言と初期化は同時に出来ないし、初期化しなかった(ここでは結果的にできていなかった)モノは「変数なし」というよく分からない状況になる*2し、しかもそれはNothingではないし、というようなところで、「あるぇ」ってなってたということのようです。
まあ、VBAでも一応だいたいの値は「ちゃんと存在している値で」Setしますし、でなければCreateObjectしたり、クラスモジュールであればNewしたりしますから、やっぱりこういうのはちょっと意識から漏れたりするのかしら。

なので、修正はこうです。

Dim filePath As String
filePath = "C:\hogehoge_NoXXXX.xlsx"

'' FIX:ファイル存在チェック
If Not (Dir(filePath)) Then
  Msg "ファイルが存在しません。"
  Exit Sub
End If

' 後略 ---------------------

おさぼりはいけませんねえ、というお話しでした。
しかし、「変数なし(初期化されていない)」変数へのアクセスによるエラー、なんて初心者がやっちまいそうなエラーですが*3、「H800401A8」で出てこなかったのはなんでなんでしょうね。ふしぎ。

ちなみに、IsNothingで「変数なし」を引っかけられないのなら、何だったら引っかけられるんだ?という話なんですが、簡単に調べてみた限りでは引っかける方法が見つからんかったっていう。これはこれで謎です。まあ、変数管理気を付けましょうねというお話しでした。

ご参考:
B.WH |AC : Object型変数の値が <変数なし> となる現象

追記:
うわーなにこれ。なんでこんなことになってんだろう。
一番最初のソースコードは、これはこれであってますね。記事書いてて、これが間違っているなんておかしいと思ったんですよ。

Dim wb As Workbook

If (wb Is Nothing) Then
  Msg "このメッセージは表示されます"
End If

つまり、「SetされていないObjectの初期値はNothing」です。コンソールで値ウォッチしていてもそうなっています。こんな記事もあります。
Excel VBAでシートの存在有無を判定するのにループはいらない。On Errorステートメントを使おう - mk_55's diary

しかし、以下には問題があります。

Dim fileIndex As Integer
fileIndex = 1
Do
LOOP_CONTINUE:

  Dim wb As Workbook
  
  On Error Resume Next

  Workbooks.Open Filename:="C:\ファイル名_" + fileIndex + ".xlsx"
  Set wb = Workbooks(filePath)

  Dim ws As WorkSheet
  ws = wb.WorkSheet("worksheet")

  On Error GoTo 0

  If (wb Is Nothing) Or (ws Is Nothing) Then
    ' ファイルを開くのに失敗したり開いたファイルにworksheetワークシートがない
    wb.Close
    GoTo LOOP_CONTINUE
  End If

  ws.Select

  ' 問題児コイツ
  wb.Close

  ' 開いたworksheetからデータ拾って条件満たしたら終了ってことにしてください
  If (なにかの条件) Then
    Exit Do
  End If

  fileIndex = fileIndex + 1
Loop

このループの初回、変数wbもwsも、初期値はNothingです。なので、Is Nothingで問題なくチェックできます。
が、「問題児コイツ」と書いたwb.Closeを実行した時点で、wbもwsも「変数なし」となり、Loop先頭に戻った時点でもNothingになりません。
これが今回の根本原因でした。

ということで、「初期値をSetしない値のチェックについて、Is Nothingについては、ループ構文内で使わないならば、問題ない」が、「ループ内で同じ変数を使いまわすのであれば要注意」です*4
なので、こうするのが良さそう。

Dim wb As Workbook, ws As WorkSheet

Dim fileIndex As Integer
fileIndex = 1


Do
LOOP_CONTINUE:

  Set wb = Nothing
  Set ws = Nothing

  On Error Resume Next
  Workbooks.Open Filename:="C:\ファイル名_" + fileIndex + ".xlsx"
  Set wb = Workbooks(filePath)

  ws = wb.WorkSheet("worksheet")
  On Error GoTo 0

  If (wb Is Nothing) Or (ws Is Nothing) Then
    ' ファイルを開くのに失敗したり開いたファイルにworksheetワークシートがない
    wb.Close
    GoTo LOOP_CONTINUE
  End If

  ws.Select

  wb.Close

  ' 開いたworksheetからデータ拾って条件満たしたら終了ってことにしてください
  If (なにかの条件) Then
    Exit Do
  End If

  fileIndex = fileIndex + 1
Loop

なんでこんな仕様なんだ…。
以上追記まで。

*1:この理解は(現象としてはこんな感じのことが起きてはいますが)誤りです。詳しくは追記で。

*2:だからこの理解は以下略

*3:これ書いて「お前何年目やねん」と悲しくなったりはしていません。…していません。

*4:こんなもん気付くかっ!(逆ギレ)