和風スパゲティのレシピ

日本語でコーディングするExcelVBA

Dictionary.Keys(i)/Items(i)と書いてはいけない

知らずに落ちると抜け出せなくなるVBAの落とし穴です。

  • Dictionaryを使ったのに処理が遅い
  • 遅延(実行時)バインディングにしたら動かなくなった
  • 「Property Let プロシージャが定義されておらず、Property Get プロシージャからオブジェクトが返されませんでした。」という謎のエラーに遭遇した

あたりにお悩みの方は、この落とし穴に落ちていないかご確認ください。

Dictionary.Keys/Itemsは引数を持たない

DictionaryのKeys/Itemsメソッドは、
すべてのKey/Itemを1次元配列にして返すメソッドです。


引数はなくて、返り値が1次元Arrayのメソッド
というのが正しい解釈です。


つまりKeys(i)というコードは、

  • DictionaryのKeysメソッドの「引数i」ではなく
  • DictionaryのKeysメソッドが返す配列の「添字i」

と読み解かなくてはなりません。

「i」は配列さんの持ち物で、Dictionaryさんのものではないということですね。


つまりDictionaryは要素番号(Index)を持っていません。

CollectionがKeyとIndexで動くため誤認しやすいのですが、
Dictionaryは「何番目の要素」という概念はないのです。


まずはこれを理解した上で、以下のコードを見てみましょう。

Dim i As Long
For i = 0 To Dic商品リスト.Count - 1
    Debug.Print Dic商品リスト.Keys(i)
Next

たまに見るこんなコード、一見正しいように見えますが、
このコードは、なんと配列をループの回数だけ作っているコードです。

1万回ループした場合は、要素1万の配列を1万個作ります。


「Keysメソッドは全keyを1次元配列にして返すメソッド」
なので、よく考えるとこれは至極当然の動きなのですが、
これではせっかくのDictionaryが遅くなってしまいます。

一見正しそうなだけに、恐ろしい罠ですね。

  • DictionaryはIndex(要素番号)を持たない
  • 代わりに1次元配列を返すメソッドKeys/Itemsが用意されている
  • ただしこれらは配列を「都度作って」返すメソッド

であることをまずは理解しておきましょう。

解決策

キーの一覧を取得する場合に、

Dim i As Long
For i = 0 To Dic商品リスト.Count - 1
    Debug.Print Dic商品リスト.Keys(i)
Next

こう書いてしまっていたコードは、以下のいずれかに修正します。


◇ Keysが出力する配列を変数に受け取る

Dim Arr商品リスト
Arr商品リスト = Dic商品リスト.Keys

Dim i As Long
For i = 0 To UBound(Arr商品リスト)
    Debug.Print Arr商品リスト(i)
Next


◇ Keysの中身をFor Eachでループする

Dim key
For Each key in Dic商品リスト.Keys
    Debug.Print key
Next


これで配列の生成を「最初の1回だけ」にすることができます。


先ほど「DictionaryはIndexを持たない」と説明しましたが、
Keysメソッドが出力する配列はちゃんと登録順になっていますので、
結果的に「要素番号順に処理」をすることが可能です。


なおItemを順に処理したい場合には、Itemsを扱う方法に加えて、

Dim key
For Each key in Dic商品リスト.Keys
    Debug.Print Dic商品リスト.Item(key)
Next

と、「すべてのkeyをループしてItem(key)で各Itemを取得する」方法もあります。


KeyとItemをセットで処理する場合は、
ForEach文1つでいいこの書き方は書くのも楽で読みやすいです。

この書き方もしっかり覚えておきましょう。

遅延(実行時)バインディングでのエラー

Keys(i)という書き方には速度以外にもう一つ弊害がります。

それが「遅延バインディングでは使えない」という点です。


Dictionaryを使用するためには、

Dim Dic商品リスト As New Dictionary
' または
Dim Dic商品リスト As Object
Set Dic商品リスト = CreateObject("Scripting.Dictionary")

このどちらかのコードを書く必要があり、
前者で書くには「Microsoft Scripting Runtime」の参照が必要です。


この前者を「事前バインディング(事前に参照設定するから)」
後者を「遅延(実行時)バインディング」と呼びます。


そしてこの「遅延バインディング」を用いた場合は、

Dic商品リスト.Keys(i)

このコードは

Property Let プロシージャが定義されておらず、Property Get プロシージャからオブジェクトが返されませんでした。

という謎のエラーメッセージで停止してしまいます。


これはなぜかというと、VBAさんは特に断りがなければ、
Keys(i)をKeysの引数「i」だと思ってしまうからです。


この「特に断りがなければ」というのが、

As New Dictionary
As Object

という部分にあたります。


As Dictionaryと「この変数はディクショナリですよ」と断っていれば、
Keys(i)を「Keysが返すArrayの添え字」と読んでくれますが、

As Objectと「この変数は何かのオブジェクトですよ」としか伝えてないと、
Keys(i)というコードを読むことが出来なくなっちゃうんですね。


※ よって正確には「事前/遅延の差」ではありません。
事前バインディングをしていたとしても、
変数の型がObjectやVariantだとこのエラーが発生します。



この解決法は、もちろん事前バインディングで書くのがベストです。


そもそも事前バインディングでないと、

Dictionaryのインテリセンス

この選択肢が出ません。

この選択肢を使ってサクサク書けるのもDictionaryの大事なメリットですので、
せっかくDictionaryを使うなら事前バインディングで書きましょう。



ただし、一応遅延バインディングでもこれを回避する方法があり、
ひとつは、

Dim Arr商品リスト
Arr商品リスト = Dic商品リスト.Keys

Dim i As Long
For i = 0 To UBound(Arr商品リスト)
    Debug.Print Arr商品リスト(i)
Next

と、一旦配列変数に入れることです。これでエラーになりません。
(Keys(i)と書くのをやめているので当然と言えば当然です)


そしてもう一つ、裏技的になりますが、

Dic商品リスト.Keys()(i)

と、Keysの()を明示してから(i)を書いても解決します。

これでも
「Keysが引数を持たないメソッドで、その返り値にiを渡す」
を伝えることが出来ていますからね。


あまりおすすめはしませんが、すでに書かれたマクロがあり、
バインディング部分を直すのが面倒ならこれで解決してもいいかもしれません。

おまけ:引数を持たないプロパティの返り値に渡すi

今回の原因は「引数を持たないプロパティの返り値に渡すi」によって起きていました。

なんとなくレアケースっぽく見えますが、実はこれ、よく使っています。


そのコードとは「Worksheets(i)」です。


Worksheets(i)の(i)は、Worksheetsプロパティの引数ではありません。

Worksheetsプロパティが返す「全シート」というコレクションの、
省略時のデフォルトプロパティItemにi
を渡しています。

' 正確にはこれ
Worksheets.Item(i)


Worksheets("○○").
のあと入力選択肢が出ないのはこれが原因です。


ということで、一見このコード↓も今回のエラーが出そうに見えます。

Dim x As Object ' ←ブックとは言わないでおく
Set x = Workbooks("Book1.xlsx")

x.Worksheets(i).Name = "○○"

今回やった「引数を持たないプロパティの返り値にiを渡す」をやってますからね。


ですがこれではエラーは起きません。

なぜかというと、
プロパティに引数が無かったら.Item(i)と解釈する
ということはVBAさんがやってくれるからです。


つまりxが「何かのオブジェクト」だったとしても、
最低限「x(i)」を「x.Item(i)」と読み替えてはくれるんですね。


よって今回の原因を正確に記述すると、

  • 「引数を持たないプロパティが返すコレクションのItemに渡すi」はOK
  • 「引数を持たないプロパティが返す配列に渡すi」はNG

ということになります。


NGの場合のエラーメッセージ

Property Let プロシージャが定義されておらず、Property Get プロシージャからオブジェクトが返されませんでした。

が謎だったのも、

  • Keys(i)に対する「Keysに引数はないよー」警告ではなく、
  • Keys.Item(i)に対する「コレクションがないよー」警告だった

からだと思われます。
(にしてもまだ謎なエラー内容なので、なにか分かったら追記します)


これは自作クラスでも再現でき、

' クラスの中身
Property Get x() As Sheets
    Set x = Worksheets
End Property

Property Get y()
    y = Array("1", "2", "3")
End Property

' 実行コード
Dim cls As Object ' ← クラスの型はVBAさんに教えない
Set cls = New Class1

Debug.Print cls.x(1).Name ' ←通る
Debug.Print cls.y(1) ' ←「Property Let プロシージャが定義されて~~エラー」

こうなります。



まあそんなこと知らなくても、

  • せっかくDictionary使うなら事前バインドしろ
  • 変数の型は可能な限り指定しろ

で済む話かもしれませんけどね。

Worksheets(i)が引数ではないことは知っておいてもいいかもしれませんし、
やっぱり知らなくてもいいかもしれません。


まあそんなこともあるんだなと思っておいてください。