Dictionaryオブジェクトとは
Dictionaryはとても高速に動作するため大量のデータを使う場合には有用な手段の1つになります。
Dictionaryオブジェクトはキーと値で1セットとなるデータ形式を持ちます。他の言語であれば連想配列やハッシュマップなどの言い方をされます。
キーは値を検索するために使う「しおり」の役割を持ちます。食券式の店の場合、食券を買って料理を受けとりますが、このときの食券がキーで、料理が値になります。
同じDictionaryオブジェクトの中でキーと値のセットは複数持つことも可能ですが、同じキーの重複は出来ません。
以下のようなデータ構造になります。
キー | 値 |
---|---|
1 | 111 |
2 | 222 |
3 | 333 |
“1” | “111” |
“2” | “222” |
“a” | “aaaaa” |
“b” | “bbbb” |
“c” | “ccc” |
事前準備
Dictionaryオブジェクトを利用するには2通りの方法があります。1つはCreateObject関数を使う方法で、もう1つは参照設定を行う方法です。どちらでも動作は変わりませんが、参照設定を行うと多少高速なこととメソッドやプロパティが入力候補で表示されます。
個人的には参照設定で利用することをお勧めします。
CreateObject関数を使う方法
書き方は以下のようになります。
1 2 |
Dim dic As Object Set dic = CreateObject("Scripting.Dictionary") |
参照設定を行う方法
VBA画面のツールメニュー→参照設定を選び、参照設定ダイアログで「Microsoft Scripting Runtime」にチェックを付けます。
コードの書き方は以下のようになります。
1 2 |
Dim dic As Dictionary Set dic = New Dictionary |
なお、1行で書いても構いません。
1 |
Dim dic As New Dictionary |
厳密には1行で書かない方がいいのですが、普通に利用する分では問題は発生しませんので1行でも構いません。私自身も1行で書くことが多いです。
このことについての詳細は「VBAでクラス変数の宣言とNewを1行で書いてよいか」をご参照ください。
メソッドとプロパティ
Dictionaryオブジェクトには以下のメソッドとプロパティがあります。使い方は後述のサンプルコードに書いています。
それぞれの引数の「key」はキー、「Item」は値を指します。
メソッド
Sub Add(Key, Item) | 新しいキーと値のセットを追加します。
既に追加されているキーの上書きはエラーになります。 |
Function Exists(Key) As Boolean | 指定したキーが含まれているか確認します。
含まれていればTrue、そうでなければFalseを返します。 |
Function Items() | Dictionaryオブジェクトに含まれる全ての値を配列で返します。
Dictionaryオブジェクトはキーの重複は認められませんが、値の重複はありえるため、Itemsメソッドで取得した配列内で値が重複していることはありえますしエラーにもなりません。 |
Function Keys() | Dictionaryオブジェクトに含まれる全てのキーを配列で返します。 |
Sub Remove(Key) | 指定したキーの要素を削除します。 |
Sub RemoveAll() | Dictionaryオブジェクトに含まれる全ての要素を削除します。 |
プロパティ
Property CompareMode As CompareMethod | キーのあいまい検索を許可するかどうかをCompareMethod列挙型の定数で指定します。
Itemプロパティなどで使うキーがDictionaryオブジェクトに格納されているキーと厳密に同じでなければならない場合はBinaryCompareを指定し、大文字・小文字、ひらがな・カタカナ、全角・半角の区別せずに行う場合はTextCompareを指定します。 あくまでも検索する際のキーの条件のため、テキスト比較を設定してもキーの重複が許されるわけではありません。 Dictionaryオブジェクトにデータがセットされている状態ではエラーになります。 CompareMethod列挙型
|
|||||||||
Property Count As Long | キーと値のセットの数を返します。 | |||||||||
Property Item(Key) | 指定したキーに対応する値の設定および取得を行います。 | |||||||||
Property Key(Key) | キーを別のキーに変更します。
たとえばキー”1″を”2″に変更したい場合は「Dictionary.Key(“1”) = “2”」のように記述します。 |
一般的な使い方のサンプル
Dictionaryクラスの全てのメソッドとプロパティを使ったサンプルです。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
Sub DictionarySample() Dim dic As New Dictionary Dim arKeys Dim arItems Dim vKey Dim vItem Dim bExist As Boolean '// あいまい検索を許可(Addされてないときに行う) dic.CompareMode = TextCompare '// 追加 Call dic.Add(1, "1") Call dic.Add(2, "2") Call dic.Add(3, "3") Call dic.Add("aaa", Timer) '// 値の変更 dic.Item(1) = "111" '// キー1の値を"1"から"111"に変更 '// キーの変更 dic.key(2) = 20 '// キーを2から20に変更 '// キーの削除 Call dic.Remove(3) '// データ数 Debug.Print "Count=" & dic.Count '// 全キー取得 arKeys = dic.Keys '// 全キーを1つずつループ For Each vKey In arKeys Debug.Print "Item=" & dic.Item(vKey) Next '// 全ての値を取得 arItems = dic.Items '// 全ての値を1つずつループ For Each vItem In arItems Debug.Print "Item=" & vItem Next '// キー存在チェック bExist = dic.Exists(1) If (bExist = True) Then Debug.Print "キーがあります" Else Debug.Print "キーがありません" End If '// あいまい検索(CompareMode = TextCompareの確認) Debug.Print dic.Item("AAA") '// Timer値を出力 Debug.Print dic.Item("aaa") '// Timer値を出力 Debug.Print dic.Item("AAA") '// Timer値を出力 '// 全キー削除 Call dic.RemoveAll '// 削除後のデータ数 Debug.Print "Count=" & dic.Count End Sub |
配列よりDictionaryクラスを使う利点
Dictionaryクラスとよく比較対象とされるのが配列です。Dictionaryと配列との違いは、キーがあるかないかですが、それによりデータの格納方式にも違いがあります。配列は先頭から順にデータが並んだ状態でデータを格納しますが、Dictionaryにはデータの並び順は持っておらず、あくまでもキーが基準になります。そのため、並び順が重要な意味を持つ場合には後述するキーのソートを行うか、もしくは、Dictionaryクラスよりも配列を利用した方がよい場合があります。
また、配列はデータを格納するための領域をRedim構文やRedim Preserve構文で確保する必要がありますが、Dictionaryにはその必要がありません。Addメソッドを実行すると領域が拡張されてキーと値が追加されます。
Dictionaryは配列と違ってキーでデータを特定できるため、高速にデータの選別が可能であるという特性があります。そのため、多数のデータの中から特定のデータを抽出する、という処理を行う場合は配列よりも圧倒的にDictionaryクラスを使った方が高速に処理できます。
もし配列でのデータ抽出を繰り返し行うような処理で遅いと感じることがあるのであれば「VBAで配列を連想配列Dictionaryに変換する」をご参照ください。
キーと値に登録できるデータの種類
Dictionaryを使う際に、キーにとして扱うデータは数値や文字列が多いと思いますが、オブジェクトを指定することが可能で、値にはオブジェクト(例:Range(“A1”))の他にも配列や戻り値を返す関数(例:Timer)などを指定することができます。ただし、キーに配列をセットすることは出来ません。
オブジェクトをキーに設定できるため、以下のように、キーにRangeオブジェクト、値にワークシートや配列をセットすることが出来ます。
また、キーに使う数値と文字列は別物として扱われます。以下のコードにもありますが、1と”1″は重複せずにどちらもキーとして設定されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Sub DictionaryAddTest() Dim dic As Dictionary Dim ar() Set dic = New Dictionary ReDim ar(3) ar(0) = "1" ar(1) = "2" ar(2) = "3" ar(3) = "4" Call dic.Add("1", "222") Call dic.Add(1, 222) Call dic.Add(Range("A1"), ActiveSheet) Call dic.Add(4, ar) Call dic.Add(5, Timer) End Sub |
未登録のキーだけを追加したい場合
Dictionaryオブジェクトに未登録のキーだけを追加したい場合は、事前にExistsメソッドで対象のキーが追加済みかどうかを判定した上で、未追加であれば追加するようにします。
以下のような感じになります。
1 2 3 4 5 6 7 8 9 10 11 |
Sub DictionaryExistTest() Dim dic As New Dictionary '// 追加 Call dic.Add(1, "1") '// 存在チェック If (dic.Exists(2) = False) Then Call dic.Add(2, "2") End If End Sub |
キーの上書きをしたい場合
追加済みのキーに対してAddメソッドを実行するとエラーになります。他の言語のハッシュマップなどではAddメソッドのような追加メソッドで直接上書きが出来るものもありますが、VBAのDictionaryクラスではそれは出来ません。そこでVBAでは疑似的に別の方法で上書きを行うことになります。
上書きの方法には2通りあります。Itemプロパティを使って値の書き換えを行う方法と、Removeメソッドで削除してからAddメソッドで追加する方法です。どちらの方法でも構いませんがItemプロパティでの書き換えの方が手間が少ない分、処理速度も速いです。
以下のコード内でそれぞれの方法を紹介します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Sub DictionaryOverwriteTest() Dim dic As New Dictionary '// 追加 Call dic.Add(1, "1") Call dic.Add(2, "2") '// Itemプロパティで書き換え dic.Item(1) = "111" '// "1"から"111"に書き換え '// Remove + Addで書き換え dic.Remove (2) Call dic.Add(2, "222") End Sub |
関数の引数でDictionaryクラスを使う場合
Dictionaryオブジェクトを関数の引数としてやり取りを行う場合に、渡した関数側での処理が呼び出し元にも反映されます。
Integer等の数値型やStringの文字列型の場合は関数の引数にByVal指定をすると呼び出し元には関数内の編集が引き継がれませんが、DictionaryはByValを付けていても渡した関数での編集内容が呼び出し元にも反映されます。
以下のサンプルは呼び出し元で1と2を追加し、関数を呼び出してその関数内部で3を追加し、呼び出し元でDictionaryの全てのキーに対応する値を出力しています。
出力すると呼び出し先の関数で追加された3も一緒に出力され、関数間で引き継がれていることが分かります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
Sub DictionaryCallTest() Dim dic As New Dictionary Dim vKey '// 追加 Call dic.Add(1, "1") Call dic.Add(2, "2") '// 関数呼び出し Call DictionaryCalledTest(dic) '// 確認 For Each vKey In dic.Keys Debug.Print dic.Item(vKey) '// "1" "2" "3"の3つが出力される Next End Sub Sub DictionaryCalledTest(a_dic As Dictionary) '// 追加 Call a_dic.Add(3, "3") End Sub |
全キーをソートする方法
Dictionaryクラスのキーはソートの仕組みがありません。ただ、場合によってはキーがソートされていた方が都合がいい場合があります。そのままではソートはできませんので、Keysプロパティで全てのキーを配列として取得して、その全キー配列をソートする方法を行います。
ソートの方法にはいくつかあります。
よく紹介されるソート方法はワークシートの指定セル範囲を並べるSortメソッドを使う方法だと思いますが、個人的にはこの方法は避けた方がよいと思っています。理由は、ソートのためにワークシートやセルを用意しなければならず、使ったあとに削除も必要になりますし、セルへの値の設定や操作は処理が遅いため、せっかくのDictionaryクラスを使っている高速性の利点が犠牲になってしまうためです。
そこで、高速にソートを行うためには2通りの方法があります。1つはソート処理を自分で実装する方法で、もう1つは.NETのArrayListクラスのSortメソッドを利用する方法です。どちらを使ってもいいのですが、.NETの利用はあまりメジャーな方法ではないため、ここではクイックソートを実装する方法を紹介します。
クイックソートについての詳細は「VBAの配列をクイックソートで並べ替え」ご参照ください。ここでは利用しませんがArrayListクラスのSortメソッドについては「VBAの配列を.NETのArrayListのSortで並べ替え」をご参照ください。
コードで使っているクイックソートの関数は上記リンク先のコードをそのまま使っています。
実行すると、クイックソート後は”8″ “9” “10”の順になります。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
Sub DictionarySort_Quicksort() Dim dic As New Dictionary Dim vKey Dim arKeys() Call dic.Add(10, "10") Call dic.Add(9, "9") Call dic.Add(8, "8") arKeys = dic.Keys '// クイックソート前の配列arKeysで確認 For Each vKey In arKeys Debug.Print dic.Item(vKey) Next '// クイックソート Call quicksort(arKeys) '// クイックソート後の配列arKeysで確認 For Each vKey In arKeys Debug.Print dic.Item(vKey) Next End Sub '// クイックソート Sub quicksort(a_Ar(), Optional iFirst As Integer = 0, Optional iLast As Integer = -1) Dim iLeft As Integer '// 左ループカウンタ Dim iRight As Integer '// 右ループカウンタ Dim sMedian '// 中央値 Dim tmp '// 配列移動用バッファ '// ソート終了位置省略時は配列要素数を設定 If (iLast = -1) Then iLast = UBound(a_Ar) End If '// 中央値を取得 sMedian = a_Ar(Int((iFirst + iLast) / 2)) iLeft = iFirst iRight = iLast Do '// 中央値の左側をループ Do '// 配列の左側から中央値より大きい値を探す If (a_Ar(iLeft) >= sMedian) Then Exit Do End If '// 左側を1つ右にずらす iLeft = iLeft + 1 Loop '// 中央値の右側をループ Do '// 配列の右側から中央値より大きい値を探す If (sMedian >= a_Ar(iRight)) Then Exit Do End If '// 右側を1つ左にずらす iRight = iRight - 1 Loop '// 左側の方が大きければここで処理終了 If (iLeft >= iRight) Then Exit Do End If '// 右側の方が大きい場合は、左右を入れ替える tmp = a_Ar(iLeft) a_Ar(iLeft) = a_Ar(iRight) a_Ar(iRight) = tmp '// 左側を1つ右にずらす iLeft = iLeft + 1 '// 右側を1つ左にずらす iRight = iRight - 1 Loop '// 中央値の左側を再帰でクイックソート If (iFirst < iLeft - 1) Then Call quicksort(a_Ar, iFirst, iLeft - 1) End If '// 中央値の右側を再帰でクイックソート If (iRight + 1 < iLast) Then Call quicksort(a_Ar, iRight + 1, iLast) End If End Sub |