【追記】この現象はC#のDictionaryでも起きる様子(記事末尾参照)
なんだかんだDictionaryって便利だよね
ExcelのVBAを使う際、(C#でいうところの)Listに相当するものとしてDictionaryをよく使う。
古いプログラムでは配列を使っていることもあるが、項目をAddしたりFor Eachで回したりCountを取ったりをできるだけListに近い感覚で使うことができるDictionaryを使いたい。
Collectionでもいいのだが、機能的にもDictionaryのほうが優れていることもあって積極的な理由がない限りはCollectionは使わないことが多い。
この辺は好みなのでどちらでも構わないのだが、実はDictionaryには大きな落とし穴があり今回見事にはまってしまったのでご紹介しておく。
勝手に項目が追加される問題
Dictionaryを使って開発・デバッグしていると、急に想定しないエラーが出るようになった。
具体的には、Dictionaryを宣言した直後、
Dim dict As Scripting.Dictionary
Set dict = New Scripting.Dictionary
(↑の段階で)まだ何もAddしていないのにdict.Items(0)が存在している。Valueは空。
いやいや何でやねん。
何故!?ありえない話し!!
追加していないものが勝手に追加されるなんて普通に考えればあり得ない話。
「バグか?」と思ったが、どうもありえる話らしい。
原因は一言でいえば、「デバッグのウォッチ式にdict.Item(key)を追加したままだったから」。
確かに直前にデバッグ実行したときにこのウォッチ式は追加したが、何が悪いというのだろうか。
Item(key)をウォッチ式に追加したら何が悪いのか
まず、ItemプロパティはDictionaryオブジェクトのアクセサのようなものだが、実はgetterだけでなくsetterとしても作用する。
Item プロパティ
Dictionary オブジェクト内の指定した_キー_の_アイテム_を設定または取得します。 コレクションについては、指定した key に基づいて item を取得します。 値の取得と設定が可能です。
Item プロパティ (Dictionary オブジェクト) | Microsoft Docs
つまりどういうことかといえば、dict.Item("AA")でアクセスした際、
- dict内にKey:"AA"が存在していれば(当然ながら)Key:"AA"に対応するItemの内容を返してくれるが、
- dict内にKey:"AA"が存在していない場合、dictに新たな項目(Key:"AA"、Item:"")を作成する
ということらしいのだ。
注釈
item を変更するときに key がない場合は、指定した newitem で新しい key が作成されます。 既存の項目を返そうとしたときに key がない場合は、新しい key が作成され、対応する項目は空のままになります。
Item プロパティ (Dictionary オブジェクト) | Microsoft Docs
ウォッチ式にdict.Item("AA")がずっと残っていたことにより、VBAコードを実行するたびにウォッチ式を表示するためdict.Item("AA")へアクセスが発生する。
でもまだdictは空なのでKey:"AA"は見つからず、結果として空のItem("AA")が追加されてしまっていた、ということになる。
何でこんな仕様にしたの?なんで?
クソデカ落とし穴マジでよして
VBAは常に開発で使うものではなく、時々出てくる(しかもしばしば重要度が低い...わりに工数が想定以上にかかるためいつも苦労する)ものの開発に使われるため、なかなか知識のアップデートが起きにくい。
ただ古いやり方しか知らないと、Excelマクロにありがちな「動くには動くけど遅すぎる」などの問題にぶち当たるため、新しい方法を模索しないわけにもいかない。
そして、必要に駆られてやむなく慣れないやり方を採用するとこういう落とし穴にはまるわけだ。南無三。
VBAを触る際は「当然こう動くはずだろう」を捨てないといけないのは経験上分かっていたのだが、まさかこのレベルで注意が必要だとは思わなかった。まだまだ精進が足りない。
【追記】この現象はC#のDictionaryでも起きる様子
コメントで教えていただきました。
確かにMicrosoftの資料を見るとC#のDictionary.Itemにもgetterとsetterがある。
public TValue this[TKey key] { get; set; }
説明にもVBA同様の記載がある。
You can also use the Item[] property to add new elements by setting the value of a key that does not exist in the Dictionary<TKey,TValue>. When you set the property value, if the key is in the Dictionary<TKey,TValue>, the value associated with that key is replaced by the assigned value. If the key is not in the Dictionary<TKey,TValue>, the key and value are added to the dictionary. In contrast, the Add method does not modify existing elements.
(機械翻訳が酷いので英語ページを参照した)
Dictionaryに存在しないKeyをItem[]に設定することで要素を追加することが可能。既にキーが存在した場合は上書きし、存在しなければ追加する。
一方でAddは既存の要素を上書きしない。既に存在するキーを指定するとArgumentExceptionをスローする。とのこと。
まとり 返信
わたしはUnity C#で、Dictionaryで同様の問題にぶつかり、
こちらの記事を参考にさせて頂いて、調整したところ、
無事なおりました。
いや~..、Dictinaryのワナですね。笑
ありがとうございます。ほんとに助かりました。
コメントありがとうございます。
まさかC#でも同じだとは思いませんでした。。。笑
お力になれてよかったです。本文にも追記させていただきます!
名無しのVBAer 返信
先日業務でコードを書いていたときにまさに同じ問題にぶち当たり「なんでそんな摩訶不思議なことが」と調べていたときに、こちらのブログに助けられました。。ウォッチ式が罠だったなんて……。
本当に、なんでこんな仕様にしたんですかね?
ともかくありがとうございました。大変助かりました、、、。
コメントありがとうございます。
私も罠にはまって「なぜそんなことが…」の思いでこの記事を書きましたので、お力になれて幸いです。
もうちょっと分かり易くしてほしいものですが、より新しいC#(と言っても、C#ももう20周年ですが)でも同様なのでMicrosoftの設計思想なんですかね…?