和風スパゲティのレシピ

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

クラスのメンバーに配列(Array)を定義する

クラスモジュールのメンバーに配列(Array)を定義する方法を解説します。

具体的には、以下のコードのように「クラス.配列名」という記述で、
通常の配列と同じような処理を実装することを目標にします。

Dim clsテスト As New Classテスト
clsテスト.Arr = Range("A1:C3").Value
clsテスト.Arr(1, 1) = "書き換えテスト"
Range("A1:C3").Value = clsテスト.Arr

 

Public変数を配列変数で定義するのはNG

まずは最も簡単な方法として、配列をPublic変数にしたいのですが、

' Classテスト内
Public Arr()

これは残念ながらコンパイル自体が通らず

定数、固定長文字列、配列、ユーザー定義型および Declare ステートメントは、オブジェクト モジュールのパブリック メンバーとしては使用できません。

エラーになります。


このように、クラスのPublic変数に配列変数は定義できません。

Variant変数で定義し、そこに配列を入れるのはOK

そこでクラス内ではただのVariant変数で定義し、呼出側で配列を渡してみます。

' Classテスト
Public Arr
' 標準モジュール
Sub クラスへの配列格納と要素の読み取りテスト()

    Dim clsテスト As New Classテスト
    clsテスト.Arr = Range("A1:C3").Value ' ← ちゃんと配列が入る
    
    Debug.Print clsテスト.Arr(1, 1) ' ← 元のA1セル値を問題なく取り出せる
    
End Sub

このコードは問題なく実行できます。

ひとまずこの方法で、配列をクラス内に実装することはできました。

クラス内に入れた配列を外から書き換えることはできない

さて上記の方法で「配列をクラスに渡す/クラスからもらう」ことはできるのですが、
実はこの方法では「配列を外から編集する」ことができません。

' 標準モジュール
Sub クラス内配列の書き換えテスト()

    Dim clsテスト As New Classテスト
    clsテスト.Arr = Range("A1:C3").Value ' ← ちゃんと配列が入る
    
    clsテスト.Arr(1, 1) = "書き換えテスト" ' ← 配列を書き換えてみる
    
    Debug.Print clsテスト.Arr(1, 1) ' ← !元のA1セルの値がそのまま出てくる!
    
End Sub

 
これができないことをイメージするのが難しいのですが、
クラスの内部では例えPublic変数であったとしても、

Property Get Arr()
    Arr = Arr
End Property

こんな風にProperty Getを実装したときと同じ動きになるようです。


よって

clsテスト.Arr(1, 1) = "書き換えテスト"

このコードは

  • 「クラス内の配列Arrの(1,1)を書き換え」たのではなく
  • 「クラスからGetした配列Arrのコピーの(1,1)を書き換え」た

ことになります。

つまり何もしていないのと同じ処理だったわけですね。


この現象は組込クラスのDictionaryでも見ることができ、

Dictionary変数.Add key名, Array(1, 2, 3)
Dictionary変数(key名)(0) = 2
Debug.Print Dictionary変数(key名)(0) ' ← 1と表示される

このようにDictionaryのItemにArrayを格納しても、
その要素を外から書き換えることはできません。


クラス内に配列を実装する場合は、この仕様への対応が必要になります。

Propertyプロシージャを使って中の配列を書き換える

クラス内の配列を編集できるようにするには、
以下のような「要素を書き換える用のPropertyプロシージャ」を作成します。

' Classテスト
Private Arr_ ' ← Private変数に

' (i, j)を配列の添字ではなくPropertyプロシージャの引数として定義
Property Get Arr(i As Long, j As Long)
    Arr = Arr_(i, j)
End Property
Property Let Arr(i As Long, j As Long, 代入値 As Variant)
    Arr_(i, j) = 代入値
End Property
' 標準モジュール
Sub クラス内配列の書き換えテスト()

    Dim clsテスト As New Classテスト
    clsテスト.Arr = Range("A1:C3").Value
    
    clsテスト.Arr(1, 1) = "書き換えテスト" ' ← 同じように見えて(1, 1)はProperty Letの引数
    
    Debug.Print clsテスト.Arr(1, 1) ' ← ちゃんと"書き換えテスト"に書き換わっている
    
End Sub

 
配列の添字(i, j)がPropertyプロシージャの引数(i, j)になっていますね。

この記述であれば中身の配列を書き換えることができますし、
傍目には配列と同じ記述になるため読みやすいコードになります。


ひとまずこの方法を用いれば、
クラス内の配列要素を外から書き換えることができるようになります。

配列全体の入出力を定義し直す

さてこれで一件落着と言いたいのですが、
残念ながら上記のコードはコンパイルを通りません。


どこがダメかと言えば、

clsテスト.Arr = Range("A1:C3").Value

↑この部分で、こんな代入はできなくなっているからです。


Arrは引数2つのProperty Letで定義してしまいましたからね。

当然ですがArrとだけ書いても「引数は省略できません」エラーとなりますし、
配列全体を取り出すときも同じエラーになってしまいます。

Range("A1:C3").Value = clsテスト.Arr " ← これも当然エラー

 
オーバーロード(同名で別引数の定義)ができる言語であれば、

Property Let Arr(i As Long, j As Long, 代入値 As Variant)
    Arr_(i, j) = 代入値
End Property
Property Let Arr(代入値 As Variant)
    Arr_ = 代入値
End Property
    ' ※ もちろんこれでは「名前が適切ではありません」エラー

こんな風に実際の配列と同じ仕様で実装できるのですが、
残念ながらVBAではオーバーロードができませんので以下で代替します。

配列の一括入出力を別名のプロシージャで作成する

まずは配列ごと操作するプロシージャを別名で用意するパターンです。

' Classテスト
Private Arr_

' 配列の要素を書き換えるプロパティ
Property Get Arr(i As Long, j As Long)
    Arr = Arr_(i, j)
End Property
Property Let Arr(i As Long, j As Long, 代入値 As Variant)
    Arr_(i, j) = 代入値
End Property

' 配列ごと入出力を行うプロパティ・メソッド
Property Get GetArr()
    GetArr = Arr_
End Property
Sub LetArr(二次元配列)
    Arr_ = 二次元配列
End Sub
' 標準モジュール
Sub クラス内配列の書き換えテスト()

    Dim clsテスト As New Classテスト

    Call clsテスト.LetArr(Range("A1:C3").Value) ' ← 代入文ではなくCallで一括代入を対応
    
    clsテスト.Arr(1, 1) = "書き換えテスト"
    
    Range("A1:C3").Value = clsテスト.GetArr ' ← 配列の取得は別名のProperty Getで対応
    
End Sub

 
上記のように配列全体を一括で入出力するプロシージャを別に作れば、
目的の「セル範囲 ⇒ 配列 ⇒ 書き換え ⇒ セル範囲に戻す」を、
しっかり実行できるクラスになります。


一括取得を別Propertyで定義している点では、
Item(key)で要素を書き換えて、配列全体はItemsでもらうDictionary
に近い実装といえるかもしれません。

配列の添え字を省略可能にしてオーバーロードを偽装する

続いて「なんちゃってオーバーロード」を作る方法です。

配列の添字 i , j をOptionalで省略可能にして作ります。

' Classテスト
Private Arr_

' i,jをOptionalで定義し、省略された場合はArrへ直接アクセスする
Property Get Arr(Optional i As Long = -1, Optional j As Long = -1)
    If i = -1 And j = -1 Then
        Arr = Arr_
    Else
        Arr = Arr_(i, j)
    End If
End Property
Property Let Arr(Optional i As Long = -1, Optional j As Long = -1, 代入値 As Variant)
    If i = -1 And j = -1 Then
        Arr_ = 代入値
    Else
        Arr_(i, j) = 代入値
    End If
End Property
' 標準モジュール
Sub クラス内配列の書き換えテスト()

    Dim clsテスト As New Classテスト

    clsテスト.Arr = Range("A1:C3").Value ' ← 本当はArr()だけどArrとも書けるので見た目は配列
    
    clsテスト.Arr(1, 1) = "書き換えテスト"
    
    Range("A1:C3").Value = clsテスト.Arr ' ← ここも本当はArr() ※ もちろんそう書いても動く
    
End Sub

なかなかいい感じの記述になりましたね。

標準モジュール側はもう本当に配列を扱っているようにしか見えません。


本当はArr()なのですが、VBAは引数全省略時はカッコなしで書けるため、
Arrという配列を扱っているのと見た目は完璧同じコードになってくれます。


クラスの中身がやや複雑になるのと、添字入力時の

Propertyプロシージャのクイックヒント

このクイックヒントが「???」になるデメリットがありますが、
割と使いやすい実装なんじゃないかと思います。


どちらも性能面はたぶん違いはありませんので、
書きやすい/読みやすいと思った方を使ってもらえればと思います。



以上でクラスのメンバーに配列(Array)を定義する方法の解説を終わります。

PublicでVariant変数を定義すれば簡単と思いきや、
外から各要素の書き換えが出来ない仕様への対応が大変でしたね。


この仕様はDictionaryでも起こる問題ですので、
本記事のようなクラスを実際に作成するかはさておき、
その配列は実態なのかコピーなのか
という視点は持っておいた方がいいかもしれません。


その上でクラス内配列を実装する必要が出た時に、
本記事のコードが参考になれば幸いです。

おまけ:入出力の微調整(カスタマイズ)

話を複雑にしないよう触れてきませんでしたが、
クラス内の配列はVariant変数で定義せざるを得ませんので、
違うものが入ってきた対策はある程度しておきましょう。


今回は二次元配列を扱う想定のクラスでしたので、

If IsArray(代入値) Then

If Get配列の次元数(代入値) = 2 Then

あたりの判定は書いておいた方がいいと思います。

www.limecode.jp



あとは、入出力を配列っぽく定義するためにいろいろやっていましたが、
クラスが「セルとのやり取り」を想定しているものであれば、

Sub Arrをセルに出力する(出力起点セル As Range)
    出力起点セル.Resize(Get配列の要素数(Arr_, 1)
                                Get配列の要素数(Arr_, 2)).Value = Arr_
End Sub

みたいなプロシージャを用意してしまえば、

Range("A1:C3").Value = clsテスト.Arr
    ' ↓ こう書き替えることができる
Call clsテスト.Arrをセルに出力する(Range("A1"))

このように出力先Rangeの範囲取得までクラスに内包することもできます。


このあたりはせっかくのクラスモジュールですので、
その時々に応じて、より便利に組んでみてください。