和風スパゲティのレシピ

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

Dictionaryで重複のないリストを作る

Dictionary(ディクショナリ:連想配列)オブジェクトを使って、
重複のないリストを作成する方法を解説します。

重複なしリストの作成

こんな処理ですね。


こういったリスト化して処理するコードは、
Dictionaryを使うと簡潔に読みやすく書ける上、
処理速度も高速になります。


Excel365の新関数「UNIQUE」が使える環境だとそれで事足りるところもありますが、
Dictionaryの入り口としてもいい題材なので、ぜひ覚えていってください。

ソースコード

Sub 商品データから商品リストを作成する()

    Dim Dic商品リスト As New Dictionary

    ' 重複なしリストの作成
    Dim データ範囲 As Range
    Set データ範囲 = Worksheets("商品データ").Range("A2:A11")
    
    Dim cell As Range
    For Each cell In データ範囲.Cells
        Dim key商品名 As String: key商品名 = cell.Value
    
        If Dic商品リスト.Exists(key商品名) = False Then
            Dic商品リスト.Add key商品名, ""
        End If
        
    Next
    
    ' リストの使用(イミディエイトに出力するサンプル)
    Dim Arr商品リスト
    Arr商品リスト = Dic商品リスト.Keys
    
    Dim i As Long
    For i = 0 To UBound(Arr商品リスト)
        Debug.Print Arr商品リスト(i)
    Next

End Sub

 
実行結果
実行結果

コードの解説

Dictionaryを使う準備

まず最初のこのコード

Dim Dic商品リスト As New Dictionary

でDictionary型の変数を用意します。
(正確にはインスタンスですがとりあえず変数と思って問題ありません)


ちなみにこれは、

Dim Dic商品リスト As Object
Set Dic商品リスト = CreateObject("Scripting.Dictionary")

こう書くこともできます。


前者で書くには「Microsoft Scripting Runtime」を参照設定しなければいけないのですが、その代わり、入力候補(インテリセンス)機能が使えるようになります。

Dictionaryのインテリセンス


Dictionaryはこの「インテリを使ってサクサク書ける」のも良さのうちなので、
面倒がらずに参照設定をして、前者で書くのをおすすめします。


「ツール」→「参照設定」→「Microsoft Scripting Runtimeにレ点」
で終わる作業です。

参照設定

 

重複なしリストの作成部分

重複のないリストを作成している部分がこちらになります。

Dim cell As Range
For Each cell In データ範囲.Cells
    Dim key商品名 As String: key商品名 = cell.Value

    If Dic商品リスト.Exists(key商品名) = False Then
        Dic商品リスト.Add key商品名, ""
    End If

Next

ループ文自体は、基本に忠実なセル範囲へのForEach文ですね。


本題のDictionary部分も、ストレートに読みやすい構文なので、
ただ読むだけでもわかりやすいと思います。
 

  1. keyが登録されているかをExistsメソッドで調べて
  2. まだ登録されていない商品(=False)なら
  3. Addメソッドで商品をDictionaryに追加

という処理を行うコードです。



Dictionaryは「キーと値(item)をセットで持つ」機能ですので、
イメージしやすい例だと

item有Dictionary例

こんな登録をするなど、様々な処理を行うことができます。


が、今回は単純にkeyの重複をなくしたリストが欲しいだけなので、

Dic商品リスト.Add key商品名, ""

と、Addメソッドには第1引数keyのみを使い、
第2引数itemには適当に""でも渡しておきましょう。


ちなみに丁寧に

key商品名 = cell.Value

と説明変数を用意しましたが、これを省く場合は、

Dic商品リスト.Add cell.Value, ""

と、「必ずValueプロパティを明記」してください。


Dictionaryはkeyにオブジェクトも渡せるため、

Dic商品リスト.Add cell, ""

とすると、keyにRangeオブジェクトそのものが渡ってうまく動きません。

結構やらかすポイントですので、肝に銘じておきましょう。



このExistsメソッドを使ったキーの既登録判定が「簡単に書けてしかも速い」というのがDictionaryの強みです。
 

If Dic.Exists(key) = False Then
    Dic.Add key, item
End If

を覚えるだけで相当応用が利きますので、
まずはこれを頭に叩き込みましょう。

作ったリストの使い方

作ったリストを使ったコードがこちらです。

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

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

Dictionaryオブジェクトのkeys/itemsメソッドは、
すべてのkey/itemを1次元配列として返してくれます。

これも強力な武器になりますので、必ず覚えておきましょう。


そしてこのことをしっかり理解してから↓のコードを見てください。

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

このコードはひとつ上のコードと結果は同じになります。
※ 結果は同じですが遅くなるので注意(後述)


違いは「配列を一旦変数に入れるか入れないか」だけなので、
当たり前といえば当たり前ですね。


しかし、なんとなくこのコードをコピペして使っていると、

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

この部分を見てDictionaryがindexを持っていると誤認している方を結構見かけます。


上記の通りDictionary自体はindexを持っておらず、
あくまで「keyで探してitemを取ってくる仕組み」です。

つまり、

Dic商品リスト.Keys(i)

この「i」は、

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

ということになります。

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


そしてこの配列自体も、Dictionaryさんの持ち物ではありません。

DictionaryのKeysメソッドは配列を「作って返す」仕組みです。
Dictionaryが配列を常時持っているわけではありません。


つまり、

' Keysを配列に格納パターン
Dim Arr商品リスト
Arr商品リスト = Dic商品リスト.Keys

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

' Keysを直接指定パターン
Dim i As Long
For i = 0 To Dic商品リスト.Count - 1
    Debug.Print Dic商品リスト.Keys(i)
Next

この2つコードは、
配列に格納パターンでは配列を1度だけ作っているのに対し、
直接指定パターンではループのたびに配列を作り直しています。

格納数によっては恐ろしく遅くなりますのでくれぐれもご注意ください。


配列変数を作らずに、かつ無駄な配列を作成したくない場合は、

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

と、配列をFor Each文で回せばOKです。

これならば、配列はFor Eachの開始時に1つ作られるだけとなります。


この辺の説明を最初から完璧に理解する必要はありません。

とりあえずは「下の書き方では遅い」と知っておき、
Dictionaryはindexを(+配列も)持っていない」と認識するだけでも、
今後Dictionaryを学ぶ上でだいぶ混乱がなくなると思います。

心の片隅にでも置いておいてください。

作った重複なしリストをセルに出力する

さて、リストを実際に使うとなると、
やはり「作ったリストをセルに出力する」という使い方が多いです。


この場合は、Keysメソッドが1次元配列を返す性質を利用して、
For文を使わず直接セルへの出力ができます。

そのコードがこちら↓

' リストをセルに出力する
Dim 始点セル As Range
Set 始点セル = Worksheets("商品リスト").Range("A2")

始点セル.Resize(Dic商品リスト.Count).Value _
    = WorksheetFunction.Transpose(Dic商品リスト.Keys)

 
出力セル範囲を「始点セルからDictionaryのキー数だけResizeした範囲」でとり、
そのValueプロパティにkeysがくれる配列を代入
すれば完了です。


ただし1次元配列はそのままだと右に向かって出力されてしまうため、
シート関数「TRANSPOSE(行列の逆転)」をかませて渡します。


このコードも非常に簡潔で読みやすい&書きやすいコードですので、
是非とも覚えて帰ってください。

 

汎用関数(Function)化

さて今回の解説は以上ですが、このコードは非常に汎用性のあるコードですので、
Functionプロシージャにしておくと強力な武器になります。

コードはこんな感じになります↓

Function セル範囲から重複をなくした配列を生成する(rng As Range) As Variant
    Dim Dic As Object: Set Dic = CreateObject("Scripting.Dictionary")

    Dim cell As Range
    For Each cell In rng.Cells
        If Dic.Exists(cell.Value) = False Then
            Dic.Add cell.Value, ""
        End If
    Next
    
    セル範囲から重複をなくした配列を生成する = Dic.Keys
End Function

 

この関数を実際に使ったコードがこちら↓

Dim Arr商品リスト
Arr商品リスト = セル範囲から重複をなくした配列を生成する(Worksheets("商品データ").Range("A2:A11"))

Worksheets("商品リスト").Range("A2").Resize(UBound(Arr商品リスト) + 1).Value _
    = WorksheetFunction.Transpose(Arr商品リスト)

 

セル範囲⇒重複なし配列のコードが1行になるので、
この手のリストを扱うコードが相当書きやすくなりますね。


ちなみにFunction名を短くしたい場合は、

Function GetArrayセル範囲→重複なしリスト(rng As Range) As Variant

あたりがおすすめです。



Function化でこの処理が1行で済むようになるのも相当なメリットですが、
加えてDictionayであることを意識せず使えるのも大事なメリットですね。

そのメリットを生かすために、

Dim Dic As Object: Set Dic = CreateObject("Scripting.Dictionary")

と、参照設定が不要なコードを採用しています。

これで
「他ではDictionaryを使わないブックでこの関数だけを使うときに参照設定が不要」
というメリットを享受できます。


ちなみにArrayにせず、Dictionaryのまま使いたいという方は、

Function CreateDictionaryセル範囲→重複なしリスト(rng As Range) As Dictionary
    Set CreateDictionaryセル範囲→重複なしリスト = New Dictionary

    Dim cell As Range
    For Each cell In rng.Cells
        If CreateDictionaryセル範囲→重複なしリスト.Exists(cell.Value) = False Then
            CreateDictionaryセル範囲→重複なしリスト.Add cell.Value, ""
        End If
    Next

End Function

こちらにすれば、

Dim Dic商品リスト As Dictionary
Set Dic商品リスト = CreateDictionaryセル範囲→重複なしリスト(データ範囲)

こんな風にDictionaryを生成できますので、お好きな方をお使いください。


私は

  • itemが空のディクショナリは別に要らない
  • 使い捨てマクロを参照設定なしで書きたい

あたりの理由で前者推しですかね。



この様に、何度も使える処理は汎用関数にしておくと、
コードが格段に読みやすくなります。


Dictionaryを使いたい=それくらい複雑な処理を書く必要が出た方にとって、
Dictionaryと同じくらい、プロシージャ分割も重要です。

Functionプロシージャの作り方も、一緒に勉強してみてください。