3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VBAの配列とVariant型

Last updated at Posted at 2025-12-01

はじめに

これは Visual Basic Advent Calendar 2025 2日目の記事です。

実行環境
Windows 11 Pro 64bit
Microsoft 365 の Excel (64bit)

配列で処理の高速化

Excel VBAでは、連続したセル範囲に同じ処理をするケースがよくあります。ForNext で10万行を超えるループを書いた結果、まるで画面が固まってしまったかのように動作が遅くなる経験をした方もいることでしょう。

10万行以上の処理など、VBAには向かないのでしょうか。

いえ、そんなことはありません。

処理速度を大きく改善するための代表的な方法が、配列の利用です。
VBAではセルの読み書きを大量に行うと、処理速度が大きく低下します。ループ内でセル操作を繰り返さず、処理は配列ですべて済ませて、その結果をセル範囲に渡す方法は、VBAの定石ともいえる高速化の手法です:sunglasses:

VBAの配列

配列の宣言は通常以下のように行います。要素数を決めて宣言する場合と、要素数を宣言後に決める場合があります。後者を動的配列と言います。

Rem 要素数を決めて宣言する場合
Dim arr1(9)        ' インデックスは0から9で要素数は10個
Dim arr2(1 To 10)  ' インデックスは1から10で要素数は10個

Rem 要素数を決めずに宣言(動的配列)
Dim arr3()           
Dim arr4() As String   ' As句でString型を明示的に宣言(メモリ効率は良い)
' ReDimで要素数を決める
ReDim arr3(4)          ' As句を省略するとVariant型になる
ReDim arr4(1 To 500)   ' As句を省略して宣言時の型を踏襲する

Rem Variant型の配列の要素は型を混在できる
arr3(0) = "Hoge"  ' String
arr3(1) = 1       ' Integer
arr3(2) = 0.3     ' Double
arr3(3) = Now     ' Date
arr3(4) = True    ' Boolean

これらの配列は一次元配列です。配列は次元を増やせます。次元とはどういうことか?
二次元配列の構造を見ればイメージがつかめると思います。

以下のようにインデックスを1始まりで二次元配列を宣言してみましょう。
二次元目以降の要素数はカンマで区切って宣言します。

Rem 2次元目以降の要素数はカンマ[,]で区切って宣言
Dim arr(1 To 8, 1 To 4)  ' 1次元目は、1から8、2次元目は、1から4の要素を持たせる

この二次元配列の構造は以下の通りです。

1 2 3 4
1 arr(1,1) arr(1,2) arr(1,3) arr(1,4)
2 arr(2,1) arr(2,2) arr(2,3) arr(2,4)
3 arr(3,1) arr(3,2) arr(3,3) arr(3,4)
4 arr(4,1) arr(4,2) arr(4,3) arr(4,4)
5 arr(5,1) arr(5,2) arr(5,3) arr(5,4)
6 arr(6,1) arr(6,2) arr(6,3) arr(6,4)
7 arr(7,1) arr(7,2) arr(7,3) arr(7,4)
8 arr(8,1) arr(8,2) arr(8,3) arr(8,4)

ご覧のとおり、まるでWorksheetオブジェクトのCellsプロパティのように見えます。一次元目のインデックスを行番号、二次元目のインデックスを列番号と見立てることができます。

この配列に値が格納されているとして、その値をセル範囲に代入することを考えてみましょう。それには配列の要素数を知る必要があります。
配列の要素数は以下のように取得します。LBound関数でインデックスの下限、UBound関数でインデックスの上限を得られます。

Rem 一次元配列のインデックスの取得
Dim arr1(99)
Dim lBnd As Long: lBnd = LBound(arr1) ' lBndは、0(インデックスの最小値)
Dim uBnd As Long: uBnd = UBound(arr1) ' uBndは、99(インデックスの最大値)

Rem 二次元配列のインデックスの取得
Dim arr2(1 To 10, 1 To 5)

Dim lBnd1 As Long: lBnd1 = LBound(arr2, 1) ' lBnd1は、1
Dim uBnd1 As Long: uBnd1 = UBound(arr2, 1) ' uBnd1は、10
Dim lBnd2 As Long: lBnd2 = LBound(arr2, 2) ' lBnd2は、1
Dim uBnd2 As Long: uBnd2 = UBound(arr2, 2) ' uBnd2は、5

二次元配列の場合、LBound関数及びUBound関数の第二引数にどの次元を測りたいかを指定します。

この知識を前提に、配列の内容をセル範囲へ反映するには、出力したいセル範囲の左上端セルを基点に、RangeオブジェクトのResizeプロパティで行列サイズを配列のサイズに合わせて拡張します。

Rem Resizeで二次元配列のサイズに合わせてセル範囲を拡大する
Sheet1.Cells(1, 1).Resize(UBound(arr, 1), UBound(arr,2)).Value = arr  ' .Valueを忘れずに

セル範囲への反映は、この代入の一行で済んでしまいます。ループして1要素ずつ値を設定する必要はありません。これは簡単ですね:laughing:

配列に化ける便利なVariant型

配列は、Variant型の変数(Dim arr()ではなく、単にDim arrで)でも作れます。以下のようにEmptyを絡めると意外に便利です。

Rem Varinat型の変数で動的配列を作る
Dim arr As Variant           ' () を書かない
ReDim arr(1 To 100, 1 To 5)  ' Variant型が二次元配列に化ける

' 処理1
'(条件によって、arr変数は全く使用されない場合があるとする)

Rem 例えば、IsEmpty関数でarrが使われたかどうか判定(Emptyなら未使用)
If IsEmpty(arr) Then GoTo Finally

' 処理2
'(arrを使用した処理) 

Rem 使用後にarr変数を無効化できる
arr = Empty  ' arr変数が、カッコが付いた配列、arr() として宣言されていたらエラー
  • 初期化されていない状態をIsEmpty関数で判定できる
  • Emptyを代入して変数をリセットできる
    • ただし、arrarr()と宣言されていたらエラーになる

ちなみに変数が配列の場合の無効化は以下でも可能です。

Rem 一般的な配列変数の無効化
Erase arr 

これはarr変数が配列に化けていれば使えますが、配列ではない場合はエラーとなります。

image.png

Variant型変数の無効化の場合、Erase arrよりも arr = Empty の方がこのエラーを気にせず使えるのでお勧めです。
Emptyを代入することで、変数をまったく空っぽのVariant型に戻すことができます(配列化もリセットされる)。

IsEmpty関数

先ほどのサンプルコードに登場したIsEmpty関数は、VBAの組み込み関数で、Variant型の引数を受け、引数がEmptyかどうかを判定します。以下はVBAの様々な組み込み型の変数に値を代入する前にIsEmpty関数の戻り値がどうなるかのテストです。

Dim varArr As Variant: Debug.Print "-- Variant型 --"
Debug.Print "代入前: IsEmpty(varArr) = " & IsEmpty(varArr)

Dim strArr As String: Debug.Print "-- String型 --"
Debug.Print "代入前: IsEmpty(strArr) = " & IsEmpty(strArr)

Dim intArr As Integer: Debug.Print "-- Integer型 --"
Debug.Print "代入前: IsEmpty(intArr) = " & IsEmpty(intArr)

Dim dblArr As Double: Debug.Print "-- Double型 --"
Debug.Print "代入前: IsEmpty(dblArr) = " & IsEmpty(dblArr)

Dim dateArr As Date: Debug.Print "-- Date型 --"
Debug.Print "代入前: IsEmpty(dateArr) = " & IsEmpty(dateArr)

Dim boolArr As Boolean: Debug.Print "-- Boolean型 --"
Debug.Print "代入前: IsEmpty(boolArr) = " & IsEmpty(boolArr)

値を代入する前なので期待する戻り値は、空っぽ(Empty)であるという意味でTrueを予測しますが、以下のとおり、Trueを返すのはVariant型だけです。

出力結果
-- Variant型 --
代入前: IsEmpty(varArr) = True
-- String型 --
代入前: IsEmpty(strArr) = False
-- Integer型 --
代入前: IsEmpty(intArr) = False
-- Double型 --
代入前: IsEmpty(dblArr) = False
-- Date型 --
代入前: IsEmpty(dateArr) = False
-- Boolean型 --
代入前: IsEmpty(boolArr) = False

※当然ながら、代入後はどの型もFalseを返す

このテストからもわかるとおり、IsEmpty関数はVariant型のための組み込み関数となります。

また、配列とは離れますが、任意のセルが空かどうかを判定するとき、IsEmpty関数はEmpty値を使った論理式より正確です。

例えば、A1セルが選択されていて、そこに0が入力されているとき、

image.png

Debug.Print "(ActiveCell.Value = Empty) : " & (ActiveCell.Value = Empty)
Debug.Print "IsEmpty(ActiveCell.Value)  : " & IsEmpty(ActiveCell.Value)
出力結果
(ActiveCell.Value = Empty) : True
IsEmpty(ActiveCell.Value)  : False

※A1の 0 をDeleteキーで消去した場合、IsEmpty(ActiveCell.Value) は、True

Empty値での論理式だと0Emptyと判定してしまいますが、IsEmpty関数だと、セルに0が入力されているか、まったくの空なのか、を判定できます。

配列を返す関数とメソッド

以下は、よく使われる配列を返す関数とメソッドです。

  • Array 関数
    任意の値を引数で受け取り、それらを格納したVariant型の一次元配列を返す
  • Split 関数
    文字列を区切り文字で分割し、結果をString型の一次元配列で返す
  • Dictionary.Keys メソッド
    Dictionaryに登録されているキーの一覧をVariant型の一次元配列で返す
  • Dictionary.Items メソッド
    Dictionaryに登録されている値の一覧をVariant型の一次元配列で返す

以下、これらの関数やメソッドを使ってみます。いずれも戻り値の配列をVariant型の変数で受けとれます。

Dim arr As Variant

Rem Array関数の戻り値を受ける
arr = Array(10, 20, 30)
Debug.Print "--- arr = Array(10, 20, 30) ---"
Debug.Print "arr(0) = " & arr(0)
Debug.Print "arr(1) = " & arr(1)
Debug.Print "arr(2) = " & arr(2)
Debug.Print Empty

Rem arr変数をSplit関数の戻り値で上書き
arr = Split("40,50,60", ",")
Debug.Print "--- arr = Split(""40,50,60"", "","") ---"
Debug.Print "arr(0) = " & arr(0)
Debug.Print "arr(1) = " & arr(1)
Debug.Print "arr(2) = " & arr(2)
Debug.Print Empty

Rem Dictionaryにアイテムを追加
Dim dic As Object: Set dic = CreateObject("Scripting.Dictionary")
dic.Add "A", 1
dic.Add "B", 2
dic.Add "C", 3
Debug.Print "--- dic.Add ---"
Debug.Print "dic(""A"") = " & dic("A")  
Debug.Print "dic(""B"") = " & dic("B")
Debug.Print "dic(""C"") = " & dic("C")
Debug.Print Empty

Rem arr変数をDictionaryのKeysメソッドの戻り値で上書き
arr = dic.Keys
Debug.Print "--- arr = dic.Keys ---"
Debug.Print "arr(0) = " & arr(0)
Debug.Print "arr(1) = " & arr(1)
Debug.Print "arr(2) = " & arr(2)
Debug.Print Empty

Rem arr変数をDictionaryのItemsメソッドの戻り値で上書き
arr = dic.Items
Debug.Print "--- arr = dic.Items ---"
Debug.Print "arr(0) = " & arr(0)
Debug.Print "arr(1) = " & arr(1)
Debug.Print "arr(2) = " & arr(2)
出力結果
--- arr = Array(10, 20, 30) ---
arr(0) = 10
arr(1) = 20
arr(2) = 30

--- arr = Split("40,50,60", ",") ---
arr(0) = 40
arr(1) = 50
arr(2) = 60

--- dic.Add ---
dic("A") = 1
dic("B") = 2
dic("C") = 3

--- arr = dic.Keys ---
arr(0) = A
arr(1) = B
arr(2) = C

--- arr = dic.Items ---
arr(0) = 1
arr(1) = 2
arr(2) = 3

ここで一点注目すべきところは、何度も繰り返されるarr変数への代入の部分です。
もともと配列だったarr変数をたった1行で全く別の配列に上書きできてしています。これは便利な反面、バグの温床にもなりますので注意が必要です。

これらの関数とメソッドが返す配列のインデックスは0始まりとなります。配列の下限インデックスを設定するOption Base 1が宣言されていても、Array関数以外は0のまま影響を受けません。

Rem モジュールの先頭で宣言
Option Base 1

中途半端で混乱の元になるので、このステートメントは宣言しないことをお勧めします。

RangeオブジェクトのValueプロパティ

また、セル範囲への書き込みとは逆に、セル範囲の値を二次元配列として読み込むことも可能です。

image.png

Dim arr As Variant

Rem セル範囲の値をVariant型変数に一行で代入
arr = Sheet1.Range("A1:B4").Value                ' 代入後、arr変数はVariant型の二次元配列になる

Dim rw As Long
For rw = 1 To UBound(arr, 1)                     ' インデックスは1始まり
    Debug.Print arr(rw, 1) & " | " & arr(rw, 2)
Next
出力結果
商品名 | 価格
りんご | 698
みかん | 498
いちご | 798

この場合、配列のインデックスは1始まりとなります。

その他、注意すべきこと

  • 配列を返す関数・メソッドの戻り値は、インデックスが固定された配列には代入できない
    Rem 静的配列への代入はVariant型であっても不可
    Dim arr(2) As Variant
    arr = Array(10, 20, 30)  ' コンパイルエラーなのでプロシージャ実行前にエラーとなる
    
    image.png

  • 配列のセル範囲への代入はフィルタの影響を受ける
    image.png

    フィルタで、値3を非表示にする
    image.png

    以下を実行

    Rem 現在のセル値と同じ値の配列を準備して代入するテスト
    Dim arr As Variant
    arr = Array("値", 1, 2, 3, 4, 5)                                ' 一次元配列(横方向になる)
    Sheet2.Range("A1:A6").Value = WorksheetFunction.Transpose(arr)  ' 縦方向に変換して代入
    

    正しく反映されない:sweat_smile:
    image.png

    これはTransposeに問題があるのではなく、あくまでもフィルタの影響です。
    Transposeには別の問題があります。

    • WorksheetFunction.Transpose メソッド
      • 配列の縦横を入れ替えてVariant型の二次元配列で返す
      • 戻り値のインデックスは1始まりになる
      • 転置後の行数が 65,536 を超えるとエラー
      • 要素内にNULLが含まれるとエラー

    最近ではApplication.Transposeというものがあるらしく、基本的にはWorksheetFunction.Transposeのラッパーで、転置時にエラーがあっても実行時エラーで止まることなくエラー値を返せるそうです(ChatGPTの説明による。違っていたらすみません:sweat_smile:)。ここではそういうものがあるという言及だけに留めておきます。

    配列の縦横転置は難しくないので、自作してもいいでしょう。

    Rem 配列の行列を転置する
    Public Function TransposeArr(arr As Variant) As Variant
        Rem 配列の次元数を判定
        Select Case GetDimensions(arr)
        Rem 一次元配列の場合
        Case 1
            Dim lBnd As Long: lBnd = LBound(arr, 1)
            Dim uBnd As Long: uBnd = UBound(arr, 1)
      
            Rem ゼロベースの上限インデックス
            Dim maxIdx As Long: maxIdx = uBnd - lBnd
              
            Rem 転置のため、ゼロベースの二次元配列を準備
            Dim arr1 As Variant: ReDim arr1(maxIdx, 0)
            Dim idx As Long: idx = 0
              
            Rem 縦方向に転置
            Dim i As Long
            For i = lBnd To uBnd
                arr1(idx, 0) = arr(i)
                idx = idx + 1
            Next
      
            TransposeArr = arr1
        Rem 二次元配列の場合
        Case 2
            Dim lBnd1 As Long: lBnd1 = LBound(arr, 1)
            Dim uBnd1 As Long: uBnd1 = UBound(arr, 1)
            Dim lBnd2 As Long: lBnd2 = LBound(arr, 2)
            Dim uBnd2 As Long: uBnd2 = UBound(arr, 2)
              
            Dim arr2 As Variant: ReDim arr2(lBnd2 To uBnd2, lBnd1 To uBnd1)
          
            Rem 縦横転置
            Dim c As Long, r As Long
            For c = lBnd2 To uBnd2
                For r = lBnd1 To uBnd1
                    arr2(c, r) = arr(r, c)
                Next
            Next
          
            TransposeArr = arr2
        Rem 二次元を超える場合
        Case Else
            ' 何もしない
        End Select
    End Function
      
    Rem 配列の次元数を取得
    Public Function GetDimensions(arr As Variant) As Long
        On Error GoTo Catch
          
        Dim dimNum As Long: dimNum = 0
          
        Dim i As Long, rt As Long
        For i = 1 To 60            ' VBAの最大次元数は 60
            rt = LBound(arr, i)
            dimNum = dimNum + 1
        Next
      
    Catch:
        GetDimensions = dimNum
    End Function
    

    いずれにしても、フィルタの影響は受けますが、Transposeの制約は無くなります。

さいごに

宣言時のAs句による明示的な型指定は、メモリ効率が良いのは確かです。
ただし現在のパソコン性能を考えれば、メモリ効率を理由にVariant型を避ける必要はほとんどありません。

明示的な型指定を否定しているわけではありませんが、Variant型はVBA上でバーチャルなセル空間を表現できる、とても便利なデータ型です。

実際今まで見てきたとおり、Excel VBAはVariant型の変数を使う前提で設計されている部分が多く、セルの値やセル範囲を扱う処理では、Variant型の利用が自然かつ効率的なのです:santa:

:christmas_tree::christmas_tree::christmas_tree::snowman::christmas_tree::christmas_tree::christmas_tree::snowman2::christmas_tree::christmas_tree::christmas_tree::snowman:

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?