VBAでも辞書型変数が使えます。便利な使い方紹介。

# VBAでの辞書型変数の使い方と活用方法

## え、VBA！？

まあそう言わないでください。  
なんやかんや便利じゃないですか。  
社内の方がアドベントカレンダーに参加するハードルを下げたいという思惑もあります。

## 環境

Windows 10
Excel 2016
Microsoft Scripting Runtimeがオン (方法は後述します)

## 辞書型ってなに？

### 辞書型変数の前に配列のお話し

配列は値の出し入れに添え字を使います。

```vb
Sub array_test()

    Dim arr() As Variant
    ReDim Preserve arr(1, 0)
    
    'arr(0, x) には名前を代入
    'arr(1, x) には年齢を代入
    
    arr(0, 0) = "Taro"
    arr(1, 0) = 40
        
    ReDim Preserve arr(1, 1)
    arr(0, 1) = "Hanako"
    arr(1, 1) = 35

    Debug.Print arr(0, 0) 'Taro
    Debug.Print arr(1, 0) '40
    Debug.Print arr(0, 1) 'Hanako
    Debug.Print arr(1, 1) '35    
    
End Sub

```

コードを書いた人は各次元の0番目には名前、1番目には年齢、というように頭の中にイメージができあがってるかもしれません。  
しかし、要素数が増えていけば「あの要素って何番目だっけ？」ってなることは必至でしょう。
ましてや、メンテナンスする人にとっては解読が大変です。  
コメントにルールを書いても、それでもやっぱり辛いです。

あと、もう一つ最大の弱点として、動的には最終次元しか増やすことができないという問題があります（これは配列の弱点というより、VBAの弱点ですが）。  
この例でいうと、1次元目に"趣味"のような要素を後で増やすことができないです。  
かといって、この例では人数がどんどん増えていきそうなので、最終次元で人数を増やしていきたいです。  
というように、設計で不自由を感じることがあります。

### そこで辞書型！

余り知られていない気がしてますが、VBAでも辞書型変数を作成できます。  
collection型でもできますが、必ずkey - value が入るのであればdictionary型の方が使いやすいです。  

### VBAでdictionary型を使うための設定

dictionary型の良さをフルで享受しようと思ったら、VBAのエディタから
ツール -> 参照設定 -> Microsoft Scripting Runtime にチェック -> OK  
が必要です。  

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985480/1b7e2a63-31ff-9f7d-9384-dc25751caf8b.png)

### 上述の配列の例を辞書で扱ってみる

細かいところは後ほど解説します。  
まずは雰囲気だけでもつかめれば。

```vb
Sub dictionary_test()

    Dim arr() As Variant
    
    ReDim Preserve arr(0)

    ' 配列の1要素をdictionaryにしてしまう
    Set arr(0) = New Dictionary
    
    'こんな感じでkey - valueを追加していく
    arr(0).Add "name", "Taro"
    arr(0).Add "age", 40
    
    ReDim Preserve arr(1)
    Set arr(1) = New Dictionary
    
    arr(1).Add "name", "Hanako"
    arr(1).Add "age", 35

    'こんな感じでkeyから呼び出せる
    Debug.Print arr(0)("name") 'Taro
    Debug.Print arr(0)("age") '40
    Debug.Print arr(1)("name") 'Hanako
    Debug.Print arr(1)("age") '35
    
    '後から要素を増やしたくなってもOK    
    arr(0).Add "hobby", "soccer"
    arr(1).Add "hobby", "traveling"
    
    Debug.Print arr(0)("hobby") 'soccer
    Debug.Print arr(1)("hobby") 'traveling
    
End Sub

```

## 辞書型変数の使い方

では細かく辞書型の使い方を見ていきましょう

```vb
Sub dictionary_usage()

    'Dictionary型の宣言方法
    Dim myDict As New Dictionary
    
    'こんな感じでkey - valueを追加していく
    myDict.Add "name", "Taro"
    myDict.Add "age", 40

    Debug.Print myDict("name") 'Taro
    Debug.Print myDict("age") '40
    
    'keyも取り出せる
    Debug.Print myDict.Keys(0) 'name
    Debug.Print myDict(myDict.Keys(0)) 'Taro
    
    'もちろんfor eachも使える
    Dim v As Variant
    For Each v In myDict.Keys
        Debug.Print v & ": "; myDict(v)
    Next v
    
    'keyには数字も使えるし
    myDict.Add 5, "five"
    Debug.Print myDict(5) 'five

    '配列以外なんでもkeyに使えるとのこと(使ったことないけど)
    myDict.Add Worksheets("Sheet1"), Worksheets("Sheet1").name
    Debug.Print myDict(Worksheets("Sheet1")) 'Sheet1
    
    
    'Keyの存在確認
    
    Debug.Print myDict.Exists("name") 'True
    
    '要素数の確認
    Debug.Print myDict.Count '4
    
    '削除はこんな感じ
    myDict.Remove ("name")
        
    Debug.Print myDict.Exists("name") 'False
    Debug.Print myDict.Count '2
          
    '全消しもできちゃう
    myDict.RemoveAll
    Debug.Print myDict.Count '0
    
    
    '存在しないkeyを指定した場合、errorになるわけではなく、指定したキーと空のvalueでitemが増える
    '当初存在しない"hobby"というkeyで実験
    Debug.Print myDict.Exists("hobby") 'false
    Debug.Print myDict("hobby") '指定したキーと空のvalueの組み合わせができる
    Debug.Print myDict.Exists("hobby") 'True
    Debug.Print TypeName(myDict("hobby")) 'Empty
    
    '指定したkeyに後からvalueを入れることも可能
    myDict("hobby") = "soccer"
    Debug.Print myDict("hobby") 'soccer
    
    'つまり、上書きも可能
    myDict("hobby") = "tennis"
    Debug.Print myDict("hobby") 'tennis
    
    '.addの方はあくまでもkeyとvalueをセットでの代入
    'そして、重複するkeyを追加しようとするとerrorになる（上書きにはならない）
    
    myDict.Add "name", "Hanako"
    myDict.Add "name", "Jiro" 'このキーは既にこのコレクションの要素に割り当てられています (エラー 457)

End Sub
```

## どんな使い方が便利？

私が最初に使ったきっかけは、表のHeader行の辞書化でした。  
例えばこんな表があったとして

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985480/542cbfc2-c713-31e5-baba-dfd2b0f3bc15.png)

列番号をハードコードしている、メンテ性の悪い関数があったとします。

```vb

' メンテが大変な関数
Function get_name_from_dept(dept_name As String) As Variant()
    
    With Worksheets("employee_list")
                
        Dim i As Long
        Dim last_row As Long
        Dim name_list()
        Dim cnt As Long
        
        last_row = .Cells(Rows.Count, 1).End(xlUp).Row
        cnt = 0
        
        For i = 1 To last_row
            If .Cells(i, 5) = dept_name Then '列番号をハードコードしてしまってる
                ReDim Preserve name_list(cnt)
                name_list(cnt) = .Cells(i, 2) & " " & .Cells(i, 3) '列番号をハードコードしてしまってる
                cnt = cnt + 1
            End If
        Next i
    End With
    
    get_name_from_dept = name_list

End Function

' 関数を呼ぶ側
Sub call_get_name_from_dept()

    Dim v As Variant
    
    For Each v In get_name_from_dept("sales")
        Debug.Print v
    Next v

'sato taro
'sato kouji

End Sub

```

こんな状況で途中に列が追加されると、発狂してしまいますよね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985480/acd0a12f-4e18-4621-cfae-27dac1689b97.png)

発狂しないために、列名を辞書型で扱ってみます。

```vb

' メンテが楽になった関数
Function get_name_from_dept(dept_name As String) As Variant()
    
    With Worksheets("employee_list")                
                
        Dim last_col As Long
    
        last_col = .Cells(1, Columns.Count).End(xlToLeft).Column
        
        Dim i As Long
        Dim dict_header As New Dictionary
        
        For i = 1 To last_col
            dict_header.Add .Cells(1, i).Value, i 'ここでheader行をdictにしてしまう
        Next
        
        'ここでheaderの辞書は完成しています。
        '例えば、dict_header("family_name")は 3を返すので、
        '.cells(3, dict_header("family_name")) はyamadaを返してくれます。

        Dim j As Long
        Dim last_row As Long
        Dim name_list()
        Dim cnt As Long
        
        last_row = .Cells(Rows.Count, 1).End(xlUp).Row
        cnt = 0
        
        For j = 1 To last_row
            If .Cells(j, dict_header("dept")) = dept_name Then 'keyで列番号を取得できるから、列を追加されても発狂せずに済む
                ReDim Preserve name_list(cnt)
                name_list(cnt) = .Cells(j, dict_header("family_name")) & " " & .Cells(j, dict_header("first_name"))
                cnt = cnt + 1
            End If
        Next j
    End With
    
    get_name_from_dept = name_list

End Function

```

## 辞書を配列に入れてやれば、色々と夢も広がります。

検索ワードと列名を指定すれば、該当する行を辞書として引っ張ってくる関数を作ってみました。
DB使いなさいと言われちゃいそうですが、そうもいかない現場もきっとあるでしょう。

```vb

' 検索ワード(search_word)と列名(search_key)を指定してやれば、該当するレコードを辞書として引っ張ってくる関数
Function get_info_from_key(search_word As Variant, search_key As String) As Variant()
   
    With Worksheets("employee_list")
        
        Dim last_col As Long
    
        last_col = .Cells(1, Columns.Count).End(xlToLeft).Column
        
        Dim r As Range
        Dim dict_header As New Dictionary
        
        ' まずはheader行で列名と列番号の辞書を作成
        For Each r In .Range(.Cells(1, 1), .Cells(1, last_col))
            dict_header.Add r.Value, r.Column
        Next
        
        ' 引数に指定されたkeyが列名に存在しない場合は終了
        If Not dict_header.Exists(search_key) Then
            MsgBox ("該当するsearch_keyがありませんでした。")
            End
        End If
        
        Dim last_row As Long
        last_row = .Cells(Rows.Count, 1).End(xlUp).Row
        Dim j As Long
        Dim key As Variant
        Dim target_counter As Long
        target_counter = 0
        Dim intermediate() As Variant
        
        ' 2行目から最終行まで、search_keyで指定された列にsearch_wordがないかをなめる
        For j = 2 To last_row
            If .Cells(j, dict_header(search_key)) = search_word Then
                ' hitしたらReDimして辞書を生成
                ReDim Preserve intermediate(target_counter)
                Set intermediate(target_counter) = New Dictionary
                
                ' dict_headerのkeyをkey、該当列の値をvalueとした辞書を作成
                For Each key In dict_header
                    intermediate(target_counter).Add key, .Cells(j, dict_header(key)).Value
                Next key
                target_counter = target_counter + 1
            End If
        Next j
    End With
        
    ' search_wordでhitする行が0だったらintermediate(0)にnullを入れて返す
    If target_counter = 0 Then
        ReDim Preserve intermediate(0)
        intermediate(0) = Null
    End If
    get_info_from_key = intermediate
        
End Function

Sub get_age_from_family_name()

    Dim dict_array As Variant

    '例えば姓で検索して名前と年齢も引っ張る、なんて使い方

    dict_array = get_info_from_key("sato", "family_name")
    Dim v As Variant
    If TypeName(dict_array(0)) = "Dictionary" Then
        For Each v In dict_array
            Debug.Print Join(Array(v("family_name"), v("first_name"), "is", v("age"), "years old"), " ")
        Next v
    End If        

'sato taro is 50 years old
'sato kouji is 36 years old
        
End Sub

```

と、こんな感じで使い勝手がよい関数も作れます。  

## おわりに

VBAでも意外と辞書型が使えること、そして表データととても相性がいいことがお分かりいただけたのではないでしょうか。  
再帰処理もさせればQueryみたいなこともできるのではないか、など夢はどんどん広がりますが、それならいよいよDBの導入を検討した方が良いですね。

## 参考

[Microsoftのdictionaryのリファレンス](https://learn.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/dictionary-object)