miauのブログ

はてなダイアリー「miauの避難所」をはてなブログに移行しました

格納順を保持するディクショナリクラスを作ってみた

今回「連想配列に値を格納して順番どおりに取り出す」という処理が多かったんですが、Scripting.Dictionary は値を取り出す際に格納した順番でとり出されることが保証されていません。(といっても、経験上は順序が崩れたことは一度もないのですが・・・。)

念のため、格納した順序で値を取り出すための OrderedDictionary というクラスを作りましたので、そのお話。今回作ったクラス自体よりも、こういったクラスの作り方といったところに焦点を当てています。

完成品

例によって gist に置いています。

いちおう Scripting.Dictionary と同様のインターフェイスは作っておいたので、Scripting.Dictionary をそのまま置き換えても使えると思います。
あと、利点が「順番を保持してくれる」だけでは寂しいので、内部データを文字列表現で返す Inspect() メソッドも作っておきました。デバッグに便利かもしれません。

使用例はこんな感じです。

Dim dicPrefs As New OrderedDictionary

dicPrefs.Add 1, "北海道"
dicPrefs.Add 2, "青森"
Debug.Print dicPrefs.Inspect    ' => {1: 北海道, 2: 青森}

Dim vPrefId As Variant
For Each vPrefId In dicPrefs
    Debug.Print CStr(vPrefId) & ":" & dicPrefs(vPrefId)
Next

内部的にはメインのデータを Scripting.Dictionary として保持していて、それとは別にキーの順序を保持するために Collection メンバも保持しています。Remove() が O(n) の処理になっているので、もし頻繁に Remove する処理であれば、実装を変更したほうがいいかもしれません。

コレクションクラスの作り方

上記でさらっと「Scripting.Dictionary と同様のインターフェイスを備えている」と書きましたが、実は For Each の In 〜 部分で使えるクラスを VBA で作る方法については、かなり情報が少ないです。VB6 でも OOP を意識していない人は結構知らなかったりする部分ですし、VBA でそこまでクリーンなコードにこだわる方も少ないので、仕方ないとは思うのですが・・・。

とりあえず VB6 での方法は VBA でも使えるのかな?ということで調べてみると・・・いちおう使えるみたいですね。

OrderedDictionary もここに載っていたひな形を元に作成しています。

裏技的な位置づけなのかな?と思ったんですが、ちゃんと紹介してる本もあるみたいです。

この本はググってる最中に Google ブックスで見かけたもので、

で必要部分は読めちゃいました。この本を参考にしつつ、ポイントを説明してみます。

For Each で利用するためのポイント

VBA 内部では、For Each の対象となるオブジェクトに対して _NewEnum() 関数が呼ばれており、そのラッパである NewEnum() 関数を

  • 戻り値の型: IUnknown
  • Procedure ID: -4

として定義する必要があります。

VB であれば GUI で Procedure ID が指定できるんですが、VBA ではそのようなインターフェイスはありませんので、一度エクスポートして、

Public Function NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
    Set NewEnum = m_Keys.[_NewEnum]
End Function

のように Attribute 指定を追加した上で、再度インポートする必要があります。(VBA のエディタ部に書く→警告を無視して保存→ファイルを開き直す、という流れでも OK な気がします。)

上記の本には「F2 でオブジェクトブラウザを開いて適当なところで右クリック→[非表示のメンバーを表示] にチェックを入れておくと、_NewEnum なんかも補完できるし勝手に [] で囲んでくれるし便利だよ」とかそういう tips も載ってました。

ちなみに gist に置いたソースでは、NewEnum を非表示のメンバに指定するための

Attribute NewEnum.VB_MemberFlags = "40"

も一緒に追加していますが、これは VBA では有効ではないようです。

デフォルト・プロパティの指定方法

デフォルト・プロパティというのは、dic.Item(key) のようによく使うプロパティを dic(key) のようにプロパティ名を指定せずに参照するための設定です。これも VBA では GUI で設定できませんので、一度エクスポート→Attribute 指定→インポートする必要があります。

Public Property Get Item(ByVal key As Variant) As Variant
Attribute Item.VB_UserMemId = 0
    On Error GoTo ERR_HAND
    :(省略)
End Property

という感じで、デフォルトプロパティとして使いたい Item プロパティの ID に 0 を指定すればよいです。

(2011-02-12 追記)

-4 とか 0 とかいうマジックナンバーが気持ち悪いという人もいるようなので、意味を一応。

オートメーション周りで定められた定数値で、-4 は DISPID_NEWENUM、0 は DISPID_VALUE ですね。定数値は下記 URL に載っているように他にもいくつかあるみたいなんですけど、定数値と意味がまとめて載ってるページは少なかったです。