配列


複数の値をセットまとめて管理

ループの説明ではサイコロを10回振った結果をシートに直接出力しましたが、ここでは出力する前に一旦保存(メモリに格納)することについて考えてみます。

ある値を一時的に保存したい場合、単純に変数を使うことができます。


基本的な配列

ループの練習で書いたdice1関数を以下のように書き換えることで、セルの代わりに変数にサイコロの出目を格納することができます。10回だと長いので繰り返し回数は4回に変更し、一旦変数に格納した後、セルに書き込むことにします。

'変数に格納後、出力
Sub dice1_Hensuu()
    Dim kekka1 As Long
    Dim kekka2 As Long
    Dim kekka3 As Long
    Dim kekka4 As Long
    
    Randomize
    kekka1 = Fix(Rnd * 6) + 1
    kekka2 = Fix(Rnd * 6) + 1
    kekka3 = Fix(Rnd * 6) + 1
    kekka4 = Fix(Rnd * 6) + 1
    
    Cells(1, 1) = kekka1
    Cells(2, 1) = kekka2
    Cells(3, 1) = kekka3
    Cells(4, 1) = kekka4
End Sub

回数が増えるとどうなるか、簡単に想像できますね。とても面倒くさそうです。

ループを使いたいところですが、結果を格納する変数名がそれぞれ異なるため、一つの処理文で共通化するのができず、効率が悪そうに見えます。

そこで配列という仕組みを使って書き直したのが以下のコードです。

’配列に格納後、出力
Sub dice1_Hairetu()
    Dim kekka(4) As Long
    Dim i As Long
    Erase kekka()
    Randomize
    For i = 1 To 4
        kekka(i) = Fix(Rnd * 6) + 1
    Next
    
    For i = 1 To 4
        Cells(i, 1) = kekka(i)
    Next
End Sub

ここでは kekka という配列を定義しています。

配列は括弧内の値(添え字・インデックス)に対応した位置(要素)を指定し、値を格納したり参照したりすることができる仕組みです。複数の変数が要素として連続的に並び、添え字で指定できるイメージです。

最初の例(配列を使わない場合)では複数の変数にそれぞれラベルを付けていましたが、配列を使うと配列名を示すラベルが1つで済みます。配列内の複数の要素はインデックスで指定できるため、ループ構文を使ってシンプルに書き換えることができました。

ループの回数を変更したくなったとしても、行を追加したりせずに修正できそうです。


配列に関する補足説明

配列の宣言

配列は、配列名に括弧を付けることで宣言することができます。その際に、括弧の中には配列のサイズ(最大のインデックス)を記述します。

Dim kekka(4) As Long』ならばkekkaという名前の、Long型を(0)~(4)までの5つ格納できる配列を宣言(メモリ上に確保)したことを示しています。

括弧内の値が4なのに5つの要素が確保されることを疑問に感じると思いますが、VBAでは括弧内の添え字は配列の最後のインデックスを示しています。他のプログラミング言語では基本的にこのような場合には0~3までの4要素の配列が確保されるのが一般的ですから、今後他のプログラミング言語を学ぶ場合には相違に注意してください。

配列の初期化

VBAでは宣言した直後の変数、配列は基本的に特定の値で初期化されています。ただし、必ずしも期待した値で初期化されているとは限りません。(変数のスコープを参照)

さしあたり初心者が意識する必要はないように思いますが、使用可能なメモリ上にラベルをつけて、そこを変数の保存先として確保することが変数の宣言です。

例えば新しいノートはどのページを開いても何も書かれていませんが、使用済みのノートであれば開いたページに『何か』が書き込まれていることがあります。メモリも当然無限ではありませんから、ノートのページと同様にラベルをつけた領域には既に『何らかのデータ』が書き込まれている場合があります。

一度実行したときに、宣言直後の要素が初期化されていたとしても、常に同じことが期待できると安易に考えない方が賢明です。変数は使用する前に初期化(初期値を代入)する習慣をつけた方が後々のためでしょう。

Erase は配列の初期化を明示するための命令で、配列内の全ての要素の値をErase(消去)することができます。

これは意外に便利な命令で、知らないと配列の初期化にループを使ったりしてしまいがちです。


サイズが動的な配列(応用

通常、配列は宣言時か遅くとも使用時にはサイズを指定する必要があります。

上の配列だとサイコロを10回振ることにした場合、新たに行を追加する必要はありませんがサイズを指定し直さなければなりません。かといって大きめのサイズを確保しておくことも効率的ではありませんし、どのくらいなら十分かもプログラム作成時に確定していないことはよくあることです。

VBAではReDimを使って配列の要素数を動的に変化させる(再宣言する)ことができますが、使い勝手がいいとはいえません。

要素の数が予め決まっていないような集合を扱う場合、以下のような方法を使うと便利です。

'コレクションの利用
Sub test_Collection()
    Dim kekka As Collection 'コレクション型の変数
    Set kekka = New Collection 'kekkaを初期化
    
    kekka.Add (1234) '要素を追加
    kekka.Add ("文字列も入る")
    kekka.Add ("これは削除される予定")
    kekka.Add (56789)
    
    kekka.Remove (3) '3番目の要素を削除
    
    For i = 1 To kekka.count 'kekkaの要素数分ループ
        Cells(i, 1) = kekka(i)
    Next
    Set kekka = Nothing 'kekkaを解放
End Sub

上記はCollectionという仕組みを利用しています。リストと呼ばれることもあります。

Collectionを使用するには、まずコレクション型の変数を定義し、『set 変数名 = new collection』で初期化します。

Collectionはサイズが動的に変化し、『変数名.add(追加する要素)』と記述することで、1つずつ値を指定した要素を追加することができます。

特定の要素を指定するには、格納した順序(1から始まるインデックス)を使います。追加した要素の値を変更することもできます。他の言語だとリストのインデックスは0番から始まることが多いので、混同しないように気をつけてください。

変数名.remove(要素のインデックス)』と書いてインデックスで指定した要素自体を削除することもできます。要素と要素の間にある要素を削除した場合は自動的に詰められます。

VBAのCollectionの特徴の一つは、一つ一つの要素の型が異なっていても格納できる点です。

最後に『set 変数名 = nothing』でメモリを解放しています。これを忘れると動作が重くなる原因になったりしますので、やや注意が必要です(プログラムの実行中にエラーになったりしてこの処理が実行されないと、メモリの使用量が増えていく)。VBAにはガベージコレクションと呼ばれるメモリを自動開放する仕組みがあるので、入門段階ではそこまで深刻に考える必要はないかもしれません。

ちなみに、forループでは上限を数値で直接指定せずに kekka.count と指定しています。これはkekkaコレクションの要素数(countプロパティ)を示しており、直接4とか10とかの値を書かずに配列のサイズを記述しておくことで、コレクションのサイズが変わっても対応でき、バグを防ぎやすくなるというメリットがあります。

応用的補足

上記のcountプロパティは配列では使えません。

配列の場合、UBound関数を使うと配列の最大のインデックス(配列の要素数ではないので注意)を取得することができます。

以下、dice1_Hairetuの上限を書き換えたコードです。上限を変更するとき、配列の最大インデックスを書き換えるだけの修正で済み、ミスを減らすことができます。

'ループ上限をUBound関数で指定
Sub dice1_Hairetu_UBound()
    Dim kekka(4) As Long
    Dim i As Long
    Erase kekka()
    Randomize
    For i = 1 To UBound(kekka)
        kekka(i) = Fix(Rnd * 6) + 1
    Next
    
    For i = 1 To UBound(kekka)'
        Cells(i , 1) = kekka(i)
    Next
End Sub

ところで、UBound関数はその名の通り関数なので、呼び出したときに何らかの処理を行った結果を返します。つまり、上記コードでは全く同じ値を得るために複数箇所で同じUBound関数が使われており、効率の面からあまり良いとはいえません。

このような場合、最初にUBound(kekka)の値を取得した時点で何らかの変数に格納しておき、以降はこの変数を参照するようにすることで、無駄な処理を省くことによる実行速度の向上を期待することができます。


Dictionary - キーで指定する配列(応用

配列やコレクションではインデックスを使った指定を行いますが、例えば『リンゴの個数、ミカンの個数、2番パイナップルの個数』をそれぞれ格納する配列があった場合、要素の番号ではなく『パイナップル』のように指定することができれば便利です。

このように、配列のキー(要素の名前)とバリュー(要素の値)をセットで扱うことができる特殊なコレクションとして、VBAでは dictionary という仕組みを利用することができます。

dictionary を使用する場合、まず以下のようにツール参照設定Microsoft Scripting Runtimeにチェックを入れます。

参照の追加 Microsoft Scripting Runtimeの参照設定
'ディクショナリの利用
Sub test_Dictionary()
    Dim kekka As Object 'オブジェクト型の変数
    Set kekka = CreateObject("Scripting.Dictionary") 'ディクショナリをセット
        kekka.Add "リンゴ", 2 'キー=リンゴ、値=2のセットを追加
        kekka.Add "ミカン", 31
        kekka.Add "パイナップル", 110
        kekka("リンゴ") = kekka("リンゴ") + 50 'キー=リンゴの値に50加算
    Set kekka = Nothing 'kekkaを解放
End Sub

一つの要素としてキーと値をペアで保存するのが dictionary の特徴です。

dictionary を利用すると、キーの名前を使って要素を指定することができます。

キーに対して値が一意のセットになっているため、重複するキーを登録することはできません。既に存在するキーの要素を追加しようとした場合、エラーになります。

キー重複エラー

配列の全要素に対するループ

ここで、ループの項で保留していたFor Each文を紹介します。

For Eachは対象となる集集合の全要素に対して処理を行うループ構文です。

以下、ディクショナリの全要素を処理する例を見てみます。

'For Eachのテスト
Sub test_Foreach()
    Dim kekka As Object 'オブジェクト型の変数
    Set kekka = CreateObject("Scripting.Dictionary") 'ディクショナリをセット
        kekka.Add "リンゴ", 2 'キー=リンゴ、値=2のセットを追加
        kekka.Add "ミカン", 31
        kekka.Add "パイナップル", 110
        kekka("リンゴ") = kekka("リンゴ") + 50 'キー=リンゴの値に50加算
        Dim goukei As Long '合計の個数
        Dim keyname As Variant 'ループ用の変数
        For Each keyname In kekka.Keys
            goukei = goukei + kekka(keyname) 'キー(keyname)に該当する値を集計
        Next
        MsgBox "合計は" & goukei & "個"
        
    Set kekka = Nothing 'kekkaを解放
End Sub

文法は『For Each 変数 In 集合』です。上記例ではkekkaディクショナリのキー集合(リンゴ、ミカン、パイナップル)に含まれる全ての要素に対して1回ずつ処理を行います

上の例では具体的には順番にキーを取得し、対応する値を全て加算しています。

for文で書ける処理を無理してFor Eachで書く必要はありませんが、ある集合の全要素を列挙したい場合などには割と便利に使うことができます。

For Eachとfor文の使い分け

For Eachを使う最大のメリットはコレクションの全要素を列挙できることです。

for文を使うとき、"i"などのカウンタ変数を更新しますが、この"i"自体はほとんどの場合配列のインデックスを示す添え字として使われるに過ぎず、むしろ"i"が配列の要素数を超えてしまったりすることでエラーの原因となることがあります。

特に"i"を汎用変数として使いまわしたりしていると、今している処理と関係ない"i"の値を参照してしまう場合もありますし、一般的に必要のない変数は使用しない方が予期せぬバグを防ぐことにもつながります。

For Eachを使えば処理に必要な要素自体を過不足なく扱うことができますが、コレクションの一部に対してだけ処理を行ったりする場合にはfor文を使った方が効率的な場合もあります。