2022/12/02

可変長配列(Dictionary等)を使った重複除外リスト(単列)




連想配列などと呼ばれる可変長配列について、下記のようなシリーズで説明しています。
可変長配列の機能整理
・可変長配列を使った重複除外リスト(単列)   ←今回
可変長配列を使った重複除外リスト(複数列)

今回は「重複しないリスト」を、「Collection」「Dictionary」「ArrayList」「SortedList」オブジェクトを使って実現させます。また比較のために「For~Next」を使って「同じ値は除外」する方法も併せて載せます。
なお、データ追加時・出力時に様々なアルゴリズムを使って並び変えを行う事は可能ですが、今回は省略しています。但しオブジェクト内に、並べ替えのメソッドを持っている場合には使用する事とします。

また、今回扱うリストは「単列(一次元配列)」とし、複数列(二次元配列)の場合については別項「可変長配列を使った重複除外リスト(複数列)」で説明します。但し「添付Excelファイル」は、比較の為に単列・複数列を1つにまとめました。コードは別々(単列はModule1+UserForm1、複数列はModule2+UserForm2)に記述していますが、元データはSheet1で共通です。

1.概要

今回は「ある一次元配列のデータ」をCollectionなどの可変長配列を使って「重複データを除外」し、その結果を確認することで、「各可変長配列の特徴」を把握することを目的としています。

そのため図1のように、ワークシート上に置いた元データを一次元配列にし、フォーム上のボタンにより各可変長配列での処理を行い、その結果をリストボックスに表示することとしました。
データ処理の概要
図1


処理コードは以降で1つずつ紹介していきますが、処理の結果は図2のようになりました。
処理後のデータ内容
図2


まず「ArrayList」は、備わっているSortメソッドを使って並べ替えを行ったので、リストボックスのデータが昇順で並んでいます。また「SortedList」は自動的に並べ替えが行われています。
重複処理の結果は「重複除外処理のまとめ」で整理しますが、「そのまま」は除き、全部で3種類の重複判断があるようです。

2.マクロ

2-1.ワークシート(Sheet1)

元データは図3のように、Sheet1のA1セルから縦方向に並んでいるものとします。そのデータをフォーム起動時に吸い上げて一次元配列を作成します。
フォームの起動は、シート上のボタン(今回説明するのは「1次元配列」のボタンの方です)から行います。
ワークシート上のデータ範囲等
図3


2-2.標準モジュール(Module1)

単列(一次元配列)のコードは、Module1に記述しています。
まず、元データの一次元配列を「OrgArray1」としてPublic宣言します(図4)。
  1. '========== ⇩(1) 共通変数の宣言 ============
  2. Public OrgArray1 As Variant    '←ワークシート上の元データを一次元配列にしたもの
図4


シート上の「1次元配列」という名前のボタンから呼び出されるのが図5です。
12行目「Call DataIn1」では、図6を呼び出して「シート上のデータを一次元配列に変換」しています。
13行目「UserForm1.Show 0」では、UserForm1を起動します。ここではモードレス(フォーム起動時にシートの操作が可能)で起動していますが、これはUserForm1とUserForm2を同時起動させて、「一次元配列の処理」と「二次元配列の処理」の比較をできるようにするためです。
  1. '========== ⇩(2) フォームの起動 ============
  2. Public Sub start1()
  3.  Call DataIn1
  4.  UserForm1.Show 0
  5. End Sub
図5


図5の12行目から呼び出されるのが図6です。
  1. '========== ⇩(3) 元データの配列化 ============
  2. Private Sub DataIn1()
  3.  Dim r As Range   '←元データの先頭セル範囲
  4.  Set r = Sheets("Sheet1").Range("A1")
  5.  If r.Value = "" Then
  6.   OrgArray1 = Empty   '←データが無い時はEmpty
  7.   Exit Sub
  8.  End If
  9.  OrgArray1 = WorksheetFunction.Transpose(r.CurrentRegion)
  10.  If IsArray(OrgArray1) = False Then
  11.   ReDim OrgArray1(1 To 1)
  12.   OrgArray1(1) = r.Value
  13.  End If
  14. End Sub
図6


24行目「Set r = Sheets("Sheet1").Range("A1")」では、Sheet1のA1セルを「データ先頭セル」として設定します。
25行目「If r.Value = "" Then」では、先頭セルが空白セルか否かを調べ、空白セルだった場合には「元データは無し」とするため、26行目「OrgArray1 = Empty」で変数OrgArray1にEmptyを設定した上で、27行目「Exit Sub」でプロシージャを抜け出します。

「わざわざEmptyを設定しなくても、OrgArray1はVariant型だから、初期値のEmptyになるのでは」と思われると思います。確かにA1セルにデータが無い時の初回はEmptyになるのですが、違和感が出る場合があります。それは、A1セル以降にデータが並んでいる状態でフォームを開いた後、一旦フォームを閉じてからA1セルのデータを削除する場合です。
この場合、次にフォームを開いた時に「A1セルにデータが無いので、そのままプロシージャを抜け出してしまう」と、変数OrgArray1はEmptyにはならず、1つ前の「A1セル以降にデータがある」状態のままとなってしまうのです。
そのため、26行目で「プロシージャを抜け出す前に、変数OrgArray1にEmptyを設定」しています。

先頭セル(r)に値が入っている場合は、30行目以降で配列変数「OrgArray1」に元データを格納していきます。
30行目「OrgArray1 = WorksheetFunction.Transpose(r.CurrentRegion)」は、元データ領域を「先頭セルからつながっている範囲」とするためにCurrentRegionプロパティを使用し、縦に並んでいるデータを取得します。しかし「縦に並んでいる」ために、そのままでは「縦に長い二次元配列」となってしまいますので、Transpose関数で行列を逆転させ、(横に長い)一次元配列としています。

しかしTranspose関数で行列逆転させた場合、データが複数行存在するのであれば「一次元配列」となるのですが、1行しかデータが無い場合(=データは1つのみ)には、配列にならずに「単なる変数」になってしまいます。
今回はデータが1つ以上であれば「一次元配列」としたいので、1行しかデータが無い場合には31~34行目で「1要素の一次元配列」を作成します。
31行目「If IsArray(OrgArray1) = False Then」では、30行目で作成したものが「配列か否か」を調べています。データ領域が1行であれば配列では無くなるのでFalseとなり、32~33行目を実行します。
32行目「ReDim OrgArray1(1 To 1)」では変数OrgArray1を1要素の配列に変更します。元々代入されていたA1セルの値が入った変数OrgArray1はクリアされてしまう形になります。
33行目「OrgArray1(1) = r.Value」では、改めて先頭セル(A1セル)の値を配列OrgArray1の要素に代入します。

以上により、データがゼロであればEmpty値の変数OrgArray1、データが1つ以上であれば「一次元配列OrgArray1」が作成されます。

2-3.ユーザーフォーム(UserForm1)

単列(一次元配列)のコードは、UserForm1に記述しています。

2-3-1.フォームレイアウト

フォーム上のコントロール類のレイアウトは、図7のようにしました。処理を実行するためのボタンを6つと、処理後のデータを表示するためのリストボックスを適当に配置しています。ボタン表面のCaptionは配置時にプロパティ変更しています。
フォーム上のコントロールの配置
図7


2-3-2.フォームモジュール

2-3-2-1.ボタンクリックによる分岐
フォーム上のコマンドボタンをクリックした時に呼び出されるのが、図8の各プロシージャです。各プロシージャから呼び出すのはリストボックスのリスト作成をするmakeListプロシージャ(図9)です。
makeListプロシージャには、引数として「リスト表示させるための一次元配列」を渡しますが、その一次元配列は「各可変長配列で重複データを除外した配列」となります。
  1. '========== ⇩(4) ボタンクリックによる分岐 ============
  2. Private Sub CommandButton1_Click()    '←「そのまま」ボタン
  3.  Call makeList(OrgArray1)
  4. End Sub
  5. Private Sub CommandButton2_Click()    '←「For~Nex」ボタン
  6.  Call makeList(ForNext(OrgArray1))
  7. End Sub
  8. Private Sub CommandButton3_Click()    '←「Collection」ボタン
  9.  Call makeList(Collect(OrgArray1))
  10. End Sub
  11. Private Sub CommandButton4_Click()    '←「Dictionary」ボタン
  12.  Call makeList(Dict(OrgArray1))
  13. End Sub
  14. Private Sub CommandButton5_Click()    '←「ArrayList」ボタン
  15.  Call makeList(ArrayL(OrgArray1))
  16. End Sub
  17. Private Sub CommandButton6_Click()    '←「SortedList」ボタン
  18.  Call makeList(SortL(OrgArray1))
  19. End Sub
図8


「そのまま」ボタンをクリックした場合には、図6で作成したオリジナルの「元データ」の配列(OrgArray1)をmakeListに渡してリストを作成します。

「For~Nex」ボタン時には、元データを図10のForNextプロシージャに渡し、重複データを除外した後の一次元配列をmakeListに渡してリストを作成します。
「Collection」ボタン時には、元データを図11のCollectプロシージャに渡し、重複データを除外した後の一次元配列をmakeListに渡してリストを作成します。
「Dictionary」ボタン時には、元データを図12のDictプロシージャに渡し、重複データを除外した後の一次元配列をmakeListに渡してリストを作成します。
「ArrayList」ボタン時には、元データを図15のArrayLプロシージャに渡し、重複データを除外+並べ替えをした後の一次元配列をmakeListに渡してリストを作成します。
「SortedList」ボタン時には、元データを図16のSortLプロシージャに渡し、重複データを除外+並べ替えをした後の一次元配列をmakeListに渡してリストを作成します。

2-3-2-2.リストボックス作成
図8の各プロシージャから呼び出され、リストボックスのリストを作成するのが図9です。引数として「リスト化するための一次元配列」を受け取ります。
  1. '========== ⇩(5) リストボックス作成 ============
  2. Private Sub makeList(ListArray As Variant)
  3.  Me.ListBox1.Clear
  4.  If IsEmpty(ListArray) = True Then Exit Sub
  5.  Me.ListBox1.List = ListArray
  6. End Sub
図9


73行目「Me.ListBox1.Clear」では、リストを一旦クリアしています。
74行目「If IsEmpty(ListArray) = True Then Exit Sub」では、受け取った引数がEmpty(=1つもデータが無い)だった時には、リスト作成を行わずに終了します。
75行目「Me.ListBox1.List = ListArray」では、配列そのものをリストに直接入れて、一発でリストを作成しています。なお、引数で受け取った一次元配列の要素数分だけ繰り返しながら、「Me.ListBox1.AddItem ListArray(i)」等で1つ1つリスト行を作成する方法でもOKです。

2-3-2-3.For~Next処理
図8の46行目から呼び出されるのが図10です。引数として元データの一次元配列を受け取り、重複を除外した配列を戻します。
  1. '========== ⇩(6) For~Next処理 ============
  2. Private Function ForNext(ListArray As Variant) As Variant
  3.  Dim buf() As Variant   '←新たな配列(戻り値となる配列)
  4.  Dim i As Integer     '←引数で受け取った配列の要素数
  5.  Dim j As Integer     '←新しい配列の要素数(順次増える)
  6.  If IsEmpty(ListArray) = True Then Exit Function
  7.  ReDim buf(1 To 1)
  8.  buf(1) = ListArray(1)
  9.  For i = 2 To UBound(ListArray, 1)
  10.   For j = 1 To UBound(buf, 1)
  11.    If buf(j) = ListArray(i) Then Exit For
  12.   Next j
  13.   If j > UBound(buf, 1) Then
  14.    ReDim Preserve buf(1 To UBound(buf, 1) + 1)
  15.    buf(UBound(buf, 1)) = ListArray(i)
  16.   End If
  17.  Next i
  18.  ForNext = buf
  19. End Function
図10


86行目「If IsEmpty(ListArray) = True Then Exit Function」では、引数がEmpty(=元データが1つも無い)の時に、図10を抜け出します。Functionのデータ型Variantの初期値である「Empty」が呼び出し元に戻る事になります。

データが1つ以上存在する場合は、呼び出し元に戻るデータも「必ず1つ以上存在」しますので、88行目「ReDim buf(1 To 1)」で「1要素の配列」を作成します。
作られた配列bufの要素には、89行目「buf(1) = ListArray(1)」で、引数として受け取った一次元配列の「先頭の要素」を格納します。

1つ目の要素の格納が完了したら、2つ目以降は91~101行目で「重複していないデータが来たら、配列を拡大してから格納」していきます。
91行目「For i = 2 To UBound(ListArray, 1)」では、引数として受け取った一次元配列の2つ目の要素から最終要素までを回しています。もし引数で受け取った配列が1要素だった場合は「For i = 2 To 1」の様に、初回と最終回が逆転してしまうためFor~Next内は実行されない事になります。

92~94行目のFor~Nextでは「新しく作っている配列(buf)内に、これから格納しようとしている値が存在するか否か」を調べています。
92行目「For j = 1 To UBound(buf, 1)」で、新しい配列(buf)の要素数分だけカウンタ変数jを回します。
93行目「If buf(j) = ListArray(i) Then Exit For」では、格納しようとしている値が既に存在(=重複)したら、92~94行目のFor~Nextを抜け出します。
93行目のIf文が成立して、For~Nextを抜け出す(Exit For)時には、その時点でのカウンタ変数j値がメモリ上に残ります。またIf文が一回も成立せずにFor~Nextが回り切ってしまった場合には、カウンタ変数j値は「UBound(buf, 1) + 1」になります。これはFor~Nextを回すたびにj値が増え、最後に92行目のFor文で「最終値(UBound(buf, 1))を超えているからFor~Nextは終了」という判断をするためです。

このj値を使って、重複の有無を96行目「If j > UBound(buf, 1) Then」で行い、For~Nextが回り切った(=重複は無かった)時に97~98行目を実行します。
97行目「ReDim Preserve buf(1 To UBound(buf, 1) + 1)」で、格納する配列(buf)のサイズを現在よりも1つ大きなサイズに変更します。既に格納済みのデータを消さないようにPreserveキーワードを付けます。
98行目「buf(UBound(buf, 1)) = ListArray(i)」で、サイズを大きくした配列bufの一番最後の要素に新しい値を追加します。

91~101行目のFor~Nextを回し、引数で得た元データ配列を重複を除外しながら新たな配列(buf)に格納し直したら、103行目「ForNext = buf」で、関数プロシージャForNextの戻り値として配列bufを設定し、終了します。

2-3-2-4.Collection処理
図8の50行目から呼び出されるのが図11です。引数として元データの一次元配列を受け取り、重複を除外した配列を戻します。
  1. '========== ⇩(7) Collection処理 ============
  2. Private Function Collect(ListArray As Variant) As Variant
  3.  Dim C As Collection   '←Collectionオブジェクトの宣言
  4.  Dim buf() As Variant   '←←新たな配列(戻り値となる配列)
  5.  Dim i As Integer     '←引数で受け取った配列の要素数
  6.  If IsEmpty(ListArray) = True Then Exit Function
  7.  Set C = New Collection
  8.  For i = 1 To UBound(ListArray, 1)
  9.   On Error Resume Next
  10.    C.Add Item:=ListArray(i), Key:=CStr(ListArray(i))
  11.   On Error GoTo 0
  12.  Next i
  13.  ReDim buf(1 To C.Count)
  14.  For i = 1 To UBound(buf, 1)
  15.   buf(i) = C.Item(i)
  16.  Next i
  17.  Collect = buf
  18.  Set C = Nothing
  19. End Function
図11


116行目「If IsEmpty(ListArray) = True Then Exit Function」では、引数がEmpty(=元データが1つも無い)の時に図11を抜け出し、Variant型の初期値であるEmptyを戻します。

118行目「Set C = New Collection」では、Collectionオブジェクトを生成します。
120~124行目では、Collectionオブジェクトに元データを1つずつ追加していきます。CollectionのKeyはString型で指定する必要があり、またKeyの重複は出来ません。と言って、他可変長配列のように「Keyの存在をチェックするメソッド」がありませんので、ここでは「無理やり追加してみて、エラーが出たら重複していると判断」することにします。

120行目「For i = 1 To UBound(ListArray, 1)」では、カウンタ変数iを元データ配列の要素数分だけ回します。
122行目「C.Add Item:=ListArray(i), Key:=CStr(ListArray(i))」では、Keyには「文字列型にした値」を設定し、Itemには「そのままの値」を設定します。Keyに重複が無ければそのまま格納されますが、重複していれば「エラーが発生し、格納はされない」ことになります。エラーが発生するとプログラムが止まってしまいますので、121行目「On Error Resume Next」でエラーはスルーさせます。

Collectionオブジェクトへの格納が終了したら、126~129行目で値を取り出し、別な配列(buf)に格納し直します。
まず126行目「ReDim buf(1 To C.Count)」で、配列bufのサイズを指定します。Collectionに格納した要素数はC.Countで得られますので、その数と同じサイズの配列にします。
127行目「For i = 1 To UBound(buf, 1)」では、カウンタ変数iを配列bufの要素数(=Collectionの要素数)分だけ回します。
128行目「buf(i) = C.Item(i)」で、配列bufの要素へCollectionの値(Item値)を1つずつ代入していきます。

配列bufへの代入が完了したら、131行目「Collect = buf」で関数プロシージャCollectの戻り値に配列bufを設定します。最後に132行目「Set C = Nothing」でオブジェクトを解放し終了します。

2-3-2-5.Dictionary処理
図8の54行目から呼び出されるのが図12です。引数として元データの一次元配列を受け取り、重複を除外した配列を戻します。
  1. '========== ⇩(8) Dictionary処理 ============
  2. Private Function Dict(ListArray As Variant) As Variant
  3.  Dim D As Object    '←Dictionaryオブジェクトの宣言
  4.  Dim i As Integer    '←引数で受け取った配列の要素数
  5.  If IsEmpty(ListArray) = True Then Exit Function
  6.  Set D = CreateObject("Scripting.Dictionary")
  7.  For i = 1 To UBound(ListArray, 1)
  8.   If D.Exists(ListArray(i)) = False Then
  9.    D.Add Item:="", Key:=ListArray(i)
  10.   End If
  11.  Next i
  12.  Dict = D.Keys
  13.  Set D = Nothing
  14. End Function
図12


145行目「If IsEmpty(ListArray) = True Then Exit Function」では、引数がEmpty(=元データが1つも無い)の時に図12を抜け出し、Variant型の初期値であるEmptyを戻します。

147行目「Set D = CreateObject("Scripting.Dictionary")」では、Dictionaryオブジェクトを生成します。
149~153行目では、Dictionaryオブジェクトに元データを1つずつ追加していきます。
149行目「For i = 1 To UBound(ListArray, 1)」では、カウンタ変数iを元データ配列の要素数分だけ回します。
Dictionaryオブジェクトには「Keyの存在を調べるExistsメソッド」がありますので、150行目「If D.Exists(ListArray(i)) = False Then」でKeyの重複調査をします。
そのKeyの調査結果がFalse(=重複が無い)の時に、151行目「D.Add Item:="", Key:=ListArray(i)」でDictionaryオブジェクトにデータを追加します。DictionaryにはKeyとItem(値)の両方をセットで追加する必要がありますが、今回はKeyだけで良いので「Key:=ListArray(i)」とし、Itemにはダミーで「""(長さゼロの文字列)」を設定しています。

なおDictionaryオブジェクトにデータを追加する手段として「Dictionary.Item(Key) = Item」という方法もあります。これは「データの修正」にも使用できるため、Existsメソッドを使用せずに「重複が有ったら上書き」していくという事にも使えます。この方法を使用すると、図12の149~153行目を図13のように置き換える事ができます。
  1.  For i = 1 To UBound(ListArray, 1)
  2. '  If D.Exists(ListArray(i)) = False Then 
  3.    D.Item(ListArray(i)) = ""
  4. '  End If 
  5.  Next i
図13


Dictionaryオブジェクトへのデータ追加が完了したら、155行目「Dict = D.Keys」で全てのKey値を取り出して戻り値であるDictに代入します。Keysメソッドは全てのKey値を「配列」の形で戻しますので、代入される変数Dictも配列となります。
なお戻される配列のインデックスは「ゼロ始まり」です。今回システムでは不具合は生じませんが、もし1始まりで無いとエラーが発生してしまう時には、図14のように「Transpose関数」を2回実行することで「インデックスは1始まり」に変換されます。
  1.  Dict = D.Keys
  2.  Dict = WorksheetFunction.Transpose(Dict)
  3.  Dict = WorksheetFunction.Transpose(Dict)
図14


関数プロシージャDictの戻り値を設定後は、Dictionaryオブジェクト変数Dを158行目「Set D = Nothing」で解放します。

なお、151行目を「D.Add Item:=ListArray(i), Key:=ListArray(i)」と「値をDictionaryオブジェクトのItem側に追加」し、155行目を「Dict = D.Items」として「全ての値を戻す」手法でもOKです。

2-3-2-6.ArrayList処理
図8の58行目から呼び出されるのが図15です。引数として元データの一次元配列を受け取り、重複を除外+並べ替えをした配列を戻します。
  1. '========== ⇩(9) ArrayList処理 ============
  2. Private Function ArrayL(ListArray As Variant) As Variant
  3.  Dim A As Object    '←ArrayListオブジェクトの宣言
  4.  Dim i As Integer    '←引数で受け取った配列の要素数
  5.  If IsEmpty(ListArray) = True Then Exit Function
  6.  Set A = CreateObject("System.Collections.ArrayList")
  7.  For i = 1 To UBound(ListArray, 1)
  8.   If A.Contains(CStr(ListArray(i))) = False Then
  9.    A.Add Value:=CStr(ListArray(i))
  10.   End If
  11.  Next i
  12.  A.Sort
  13.  ArrayL = A.toArray
  14.  Set A = Nothing
  15. End Function
図15


175行目「If IsEmpty(ListArray) = True Then Exit Function」では、引数がEmpty(=元データが1つも無い)の時に図15を抜け出し、Variant型の初期値であるEmptyを戻します。

177行目「Set A = CreateObject("System.Collections.ArrayList")」では、ArrayListオブジェクトを生成します。
179~183行目では、ArrayListオブジェクトに元データを1つずつ追加していきます。
179行目「For i = 1 To UBound(ListArray, 1)」では、カウンタ変数iを元データ配列の要素数分だけ回します。
ArrayListオブジェクトには「値の存在を調べるContainsメソッド」がありますので、180行目「If A.Contains(CStr(ListArray(i))) = False Then」で値の重複調査をします。
その値の調査結果がFalse(=重複が無い)の時に、181行目「A.Add Value:=CStr(ListArray(i))」でArrayListオブジェクトにデータを追加します。なおArrayListオブジェクトの値は、本来「データ型の混在はOK」なのですが、今回はSortメソッドを使って並べ替えを行うために「データ型を揃える」必要があります。ですので全て文字列型にするためにCstr関数を使って、データ型変換をした上で重複チェック(180行目)・データ追加(181行目)を行っています。

データ追加が完了した後、185行目「A.Sort」で「値の並べ替え」を行い、186行目「ArrayL = A.toArray」でArrayListオブジェクトの全データを配列の形で変数ArrayL(関数プロシージャArrayLの戻り値)に設定します。
なおtoArrayで戻される配列は、Dictionaryオブジェクトと同様に「インデックスはゼロ始まり」です。インデックスを1からにしたい場合は、図14と同じく「Transpose関数」を2回実行して下さい。

2-3-2-7.SortedList処理
図8の62行目から呼び出されるのが図16です。引数として元データの一次元配列を受け取り、重複を除外+並べ替えをした配列を戻します。
  1. '========== ⇩(10) SortedList処理 ============
  2. Private Function SortL(ListArray As Variant) As Variant
  3.  Dim S As Object    '←SortedListオブジェクトの宣言
  4.  Dim buf() As Variant   '←←新たな配列(戻り値となる配列)
  5.  Dim i As Integer    '←引数で受け取った配列の要素数
  6.  If IsEmpty(ListArray) = True Then Exit Function
  7.  Set S = CreateObject("System.Collections.SortedList")
  8.  For i = 1 To UBound(ListArray, 1)
  9.   If S.ContainsKey(CStr(ListArray(i))) = False Then
  10.    S.Add Value:=ListArray(i), Key:=CStr(ListArray(i))
  11.   End If
  12.  Next i
  13.  ReDim buf(1 To S.Count)
  14.  For i = 1 To UBound(buf, 1)
  15.   buf(i) = S.GetByIndex(i - 1)
  16.  Next i
  17.  SortL = buf
  18.  Set S = Nothing
  19. End Function
図16


206行目「If IsEmpty(ListArray) = True Then Exit Function」では、引数がEmpty(=元データが1つも無い)の時に図16を抜け出し、Variant型の初期値であるEmptyを戻します。

208行目「Set S = CreateObject("System.Collections.SortedList")」では、SortedListオブジェクトを生成します。
210~214行目では、SortedListオブジェクトに元データを1つずつ追加していきます。
210行目「For i = 1 To UBound(ListArray, 1)」では、カウンタ変数iを元データ配列の要素数分だけ回します。
SortedListオブジェクトには「Keyの存在を調べるContainsKeyメソッド」がありますので、211行目「If S.ContainsKey(CStr(ListArray(i))) = False Then」でKeyの重複調査をします。
その調査結果がFalse(=Keyの重複が無い)の時に、212行目「S.Add Value:=ListArray(i), Key:=CStr(ListArray(i))」でArrayListオブジェクトにデータを追加します。なおSortedListオブジェクトのKeyは「データ型の混在はNG」ですので、全て文字列型に揃えるためにCstr関数を使いデータ型変換をした上で重複チェック(211行目)・データ追加(212行目)を行っています。なお、値(Value)はデータ型混在OKなので、そのまま(ListArray(i))格納しています。

なおDictionaryオブジェクトと同様に、SortedListではデータ追加・データ修正の手段に「SortedList.Item(Key) = Value」が使えます。ですので210~214行目は図17のように書き換える事が可能です。
  1.  For i = 1 To UBound(ListArray, 1)
  2. '  If S.ContainsKey(CStr(ListArray(i))) = False Then 
  3.    S.Item(CStr(ListArray(i))) = ListArray(i)
  4. '  End If 
  5.  Next i
図17


SortedListオブジェクトへのデータ追加が完了したら、216~219行目で値を取り出し、別な配列(buf)に格納し直します。
まず216行目「ReDim buf(1 To S.Count)」で、格納する配列bufのサイズを指定します。
217行目「For i = 1 To UBound(buf, 1)」では、カウンタ変数iを配列bufの要素数(=SortedListの要素数)分だけ回します。
218行目「buf(i) = S.GetByIndex(i - 1)」で、配列bufの各要素へSortedListの値を1つずつ代入していきます。SortedListのインデックスはゼロ始まりですので「i - 1」としています。

配列bufへの代入が完了したら、221行目「SortL = buf」で関数プロシージャSortLの戻り値に配列bufを設定します。

なお、今回システムでは「リストボックスに値を入れる」ことが目的となっています。リストボックス内では「値は全て文字列扱い」ですので、渡される側としては「文字列のみの配列」で充分です。逆に渡す側(図16)からすれば「元データのデータ型を保った値(Value)」で無くても、並べ替えの為に「文字列型に変換したKey値」でOKということになります。ですので図18のような事もOKとなります。
  1.  S.Add Value:="", Key:=CStr(ListArray(i))
  1.  buf(i) = S.GetKey(i - 1)
図18


図18では、212行目「S.Add Value:="", Key:=CStr(ListArray(i))」で、Key値には文字列型で格納し、値(Value)には適当な値(ここでは、長さゼロの文字列)を入れています。
そして218行目で取り出す時も「文字列型」であるKey値をGetKeyメソッドを使って取り出します。関数プロシージャSortLとして戻す配列内のデータ型は異なりますが、リストボックスに表示される時には全く同じとなります。

3.重複除外処理のまとめ

上記の「For~Next」と4種の可変長配列の処理の結果について整理すると、図19のようになります。処理前の値の形を基準とし、各方法で同一と判断する範囲を緑線で囲っています。
データの形による重複判断の違い
図19


まず「Collectionオブジェクト」では「大文字小文字・全角半角・ひらがなカタカナ」は同じとして処理しています。
Collection以外の方法では、Keyや値に格納する時に、並べ替えに必要な「データ型の統一」のための文字列変換を行った時には、当然ながら配列内の「数値」と「文字列にした数字」は同一扱いになります。なお、数値のみをKey値にする場合は数値比較が出来ますので、数値→文字列に直してしまうと逆におかしな結果になります(例:"100" < "99")。
Collectionオブジェクトは、参照設定やCreateObjectなどが不要のため良く使われるようですが、この重複判断には注意した方が良いと思います。

アプリ実例

CSVファイルでデータを読み書きする月間予定表
サンプリング周期が異なるデータの補間法
複数行1データの並び替え
データの重みを考慮したComboBox入力補助
先入先出の入出庫管理システム
DVD等の内容・保管場所等管理システム


可変長配列(Dictionary等)を使った重複除外リスト(its-020-021.xlsm)
セキュリティ向上を目的として「インターネット経由でダウンロードしたOfficeファイル(Excel等)のマクロは、既定でブロック」されるようにOfficeアプリケーションの既定動作が変更になりました。(2022年4月より切替開始)
解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。