t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA クラスモジュール 超 入門

このブログではこれまでにクラスモジュールを活用したコードをいくつか紹介してきたが、使いどころの紹介がメインでクラスモジュールそのものの使い方について一から学べるような構成は取っていなかった。

今回は「クラスモジュール超入門」と題してクラスモジュールを初めて使う方やオブジェクト指向がいまひとつ分からないという方に向けて解説してみようと思う。

クラスモジュールとは、一言でいえばオブジェクトを作成するためのひな形である。ただまぁそう言われてもピンと来ない方も多いかと思う。まずこの「オブジェクト」というものがよく分からずに悩むのではないだろうか。そこでまずは「オブジェクト」について簡単に説明しよう。

目次

オブジェクトとは

オブジェクトとは、データ命令をセットにしてプログラム上で扱いやすくしたモノだ。たとえばExcel VBAで最もよく利用されるRangeオブジェクトを例に挙げると、値、背景色、高さ・幅、アドレス、フォントなど、そのRangeが持つ固有の情報がデータである。また、削除する、中身をクリアする、コピーする、隠すなどそのRangeに対して行える操作が命令である。

オブジェクトという考え方が登場する以前は、データはデータ、命令は命令という風にバラバラに存在するのが当たり前だったが、これではどのデータに対してどんな命令が適用できるのか、プログラマーがいちいち覚えておかないといけない。

データと命令をオブジェクトという一つの単位にまとめることで、そのオブジェクトがどんなデータを備えているのか、どんな命令が出せるのかを、まるでカタログから選ぶかのように簡単に選択できるようになる。

f:id:t-hom:20161231004036p:plain
f:id:t-hom:20161231004059p:plain

今回クラスモジュールで作成するオブジェクト

前項でRangeを例に挙げてオブジェクトを説明したが、RangeのようにExcel上の部品と結びついたオブジェクトばかりではない。むしろクラスモジュールを使って作るのは変数・配列・関数のようにプログラムの実行中に裏方で活躍するオブジェクトが殆どだ。今回題材とするのも、そのようなオブジェクトである。

今回はシンプルなカウンターオブジェクトを作る。初めに断っておくとこれはあくまで説明のサンプルなので、何かの役にたつという代物ではない。どうせ作るなら役に立つものが良いと思うだろうけど、ここは我慢してほしい。いきなり実用性を求めるとそれなりに複雑なオブジェクトになってしまい、説明もややこしくなる。やはり初めての学習にはシンプルなものが理解しやすい。

今回作るカウンターオブジェクトは、データ「Value」に数値を設定・取得でき、命令「Up」でその数値を増やすことができるオブジェクトとする。

カウンタークラスの作成

まずVBEの挿入メニューからクラスモジュールを挿入し、
f:id:t-hom:20161231004405p:plain
プロパティウインドウ※からオブジェクト名Class1をCounterに変更する。
f:id:t-hom:20161231004455p:plain

※プロパティウインドウが表示されていない場合はVBEの表示メニューから表示させることができる。

そしてCounterモジュールを開き、以下のコードを記述する。

Public Value As Long
Sub Up()
    Value = Value + 1
End Sub

Public Value As Longと書いた部分は、いわゆる変数宣言である。

通常の変数はSubプロシージャなどの中にDimで宣言するが、プロシージャの外でDimの代わりにPublicを用いて宣言すると、その変数はオブジェクトの外からアクセスできるようになる。
※もちろんオブジェクトの中からもアクセス可能

このValue変数が、オブジェクトのデータを保持する役割を果たす。

カウンターオブジェクトなので今回はたまたま値という意味のValueという変数名にしたが、これはただの変数名なので自分でオブジェクトを作るときは任意の名前をつけて良い。

そしてUpプロシージャがオブジェクトの命令にあたる。プロシージャはデフォルトでPublic扱いなのでこちらもやはりオブジェクトの外から実行することができる。

Up命令が実行されるとValueが増える仕組みになっている。

これでクラスモジュールは完成したが、クラスはあくまでオブジェクトを作るためのひな型なので単体で実行することはできない。

カウンターオブジェクトを使うサンプルコード

さて、では標準モジュールを挿入し、次のコードを記述しよう。VBエディタの入力支援がどのように働くのかも体験してほしいので、できればコピー&ペーストではなく手入力をお勧めする。

Sub CountUpSample()
    Dim c As Counter
    Set c = New Counter
    c.Value = 1
    MsgBox c.Value
    c.Up
    c.Up
    c.Up
    MsgBox c.Value
End Sub

実行すると、最初のメッセージが1、次のメッセージが4と出てプログラムが終了する。

最初に断ったように、これは全く実用性のないサンプルである。何のためにという疑問は一旦よけておいて、どのように動いているのかという点に注目してほしい。

では、次項でコードの中身を詳しく説明していく。

サンプルコードの解説

まずは次の2行から。

    Dim c As Counter
    Set c = New Counter

よくクラスモジュールそのものがオブジェクトであると勘違いされるケースも見受けられるが、クラスモジュールはあくまでオブジェクトのひな形、つまり「型」である。

変数宣言は、Dim [変数名] As [型] という形式をとるので、最初の1行はCounter型の変数cを作成するコードだ。

さて、変数を宣言した時点では中身はまだ空である。変数の初期値は、整数型なら0、文字列型なら空文字が入っているように、オブジェクト型にはNothing(何もないという意味)という特殊な値が入っている。

変数宣言した直後はCounter型の入れ物が用意されただけで、まだ中身は入っていないのでオブジェクトとして利用できない。そこで実際のオブジェクトを作って入れてやる必要がある。そのオブジェクトを作るコードがNew [クラス名]である。

Set c = New Counterは変数cに新規のカウンターオブジェクトを作成して代入する文である。Setとついているのはオブジェクト型の変数に代入するときの決まりごとなので今は深く考える必要はない。

さて、では次の2行について説明する。

    c.Value = 1
    MsgBox c.Value

これは変数cに格納されたCounterオブジェクトのPublic変数Valueに1を代入し、再度Valueをメッセージボックスで表示させているだけ。これは簡単だ。

先ほど変数cにCounter型の新規オブジェクトを代入したので、cはオブジェクトそのものと同様に扱うことができる。ちょうどa = 10と代入したときにaを10という数字そのものとして扱えるのと同じである。

したがってc.Valueと書くとオブジェクト内部のPublic変数Valueにアクセスできる。

次に、c.UpではオブジェクトのUpプロシージャを呼び出している。Upプロシージャは内部のValue変数の値を増やすので、3回呼び出せば3増える。

最初に1を代入しているので、最後のMsgBoxでは4と表示される。

以上がCounterクラスを使ったサンプルの解説である。

ひとつのクラスから複数のオブジェクトが作れる

先ほどのサンプルコードではc.Valueに値を設定したり取得したりした。ここもよく勘違いしやすいのだが、ここでアクセスしているのはクラスモジュールが持つ変数ではなく、クラスモジュールによって生成されたオブジェクト内部の変数である。オブジェクトは1つのクラスモジュールから複数生成することができ、生成されたオブジェクトの内部変数は、それぞれのオブジェクトごとに別の値を保持している。

たとえば以下のコードで説明すると、変数cに入っているオブジェクトと、変数dに入っているオブジェクトは別物なので、内部変数Valueもそれぞれ異なる。

Dim c As Counter
Set c = New Counter
Dim d As Counter
Set d = New Counter
c.Value = 10
d.Value = 20

f:id:t-hom:20161231005601p:plain

オブジェクト変数は箱じゃなくてネームタグでイメージしよう

さて、先ほどは変数cとdにそれぞれ新しいオブジェクトをセットした。
では次のコードはどうだろうか。

Dim c As Counter
Set c = New Counter
Dim d As Counter
Set d = c

この場合、cとdには同一のオブジェクトが入るのでcのValueをセットするとdのValueも同じ値になってしまう。ここでまた混乱される方が多い。どういうことか説明しよう。

プログラミングの解説書では変数を「箱」に例えて説明されるケースが多い。通常の値を保持する変数はこの理解で何の問題もないのだが、オブジェクト変数を「箱」でイメージすると不都合が生じる。

たとえば先ほど紹介した変数cと変数dに同一のオブジェクトが入るという部分だが、現実世界で別の箱に同一の物が入るなんてことはありえない。ここでいう同一とは、同じ種類のことではなく、唯一無二のモノとしての同一である。

仮に2つの箱に同一のリンゴを入れることができたなら、片方の箱に入ったリンゴを切ると、もう片方の箱の中のリンゴも切れている。そんなばかな。

ということは、オブジェクト変数を箱に例えるのはそもそも無理がある。

じゃあどうするかというと、オブジェクト変数はネームタグだと思えばいい。

たとえば1つのリンゴにタグcとタグdを括り付ける。タグcのリンゴを切ったらタグdのリンゴも切れている。同じリンゴに括ったのだから当たり前だ。

オブジェクトを代入する際に使用するSetはタグを括るためのおまじないだと考えると良い。過去記事で詳しく説明したが、とりあえず今のところは単にタグという理解で良い。

カウンタークラスのデータを保護する

さて、今回作成したCounterクラスでは、外部からValueが自由に書き換えできてしまう。1ずつ増やす想定なのに一気に100を代入することだってできてしまうわけだ。

今はまだシンプルなクラスでただのサンプルコードだから良いけど、実務用のマクロは複雑なので、書き手に悪意がなくてもコーディングミスなどでデータを不正に書き換えてしまうリスクがある。

ということで、クラス側でそういった想定外の操作ができないようにしてしまおう。

もう一度Counterクラスのコードを見てみよう。

Public Value As Long
Sub Up()
    Value = Value + 1
End Sub

外側からValueを自由に書き換えられるのは、ValueがPublicで宣言されているためだ。
これをまずPrivateに変更する。

Private Value As Long
Sub Up()
    Value = Value + 1
End Sub

Privateにすると、外側からはValueにアクセスできなくなるので不正な値を代入されることはなくなる。これをオブジェクト指向では隠ぺいと呼ぶ。なんだか悪いことをしているような気分になる呼び名だ。

隠ぺいすると外部からは直接アクセスできなくなるが、そのクラス内からはアクセスできるので、外からUpを呼び出せば間接的にValueを増やすことができる。

でもこれだとUpを呼んでValueを増やすことはできても外から値が参照できないので意味がない。そこで、GetValueというFunctionプロシージャを作って値を取得できるようにする。

Private value As Long

Sub Up()
    value = value + 1
End Sub

Function GetValue() As Long
    GetValue = value
End Function

これなら外からGetValueを呼び出すことで値を取得できるし、Upで値を増やすことができる。

このように内部のPrivate変数にアクセスするためのプロシージャをアクセサという。

それではメインのコードも改修していこう。元のコードはこちら。

Sub CountUpSample()
    Dim c As Counter
    Set c = New Counter
    c.Value = 1
    MsgBox c.Value
    c.Up
    c.Up
    c.Up
    MsgBox c.Value
End Sub

Valueに直接アクセスできなくなったので、まずc.Value = 1と書いている箇所はこのままでは動かない。ValueはLong型なので初期値は0。1で初期化したいのでc.Upを呼び出すように変更する。また、2箇所のMsgBox c.Valueは先ほど作成したアクセサ(GetValue)に変更する。

修正したコードがこちら。

Sub CountUpSample()
    Dim c As Counter
    Set c = New Counter
    c.Up
    MsgBox c.GetValue
    c.Up
    c.Up
    c.Up
    MsgBox c.GetValue
End Sub

クラスのコンストラクターを利用する。

さて、一応Valueが好き勝手に書き換えられる危険はなくなったけれど、カウンターの初期値を設定するのにc.Upを利用しているのはちょっと不恰好だ。

クラスモジュールには、新規オブジェクトが生成されたときに実行される特殊なプロシージャがあるのでそれを利用してValueの初期値を1に設定しよう。

まずオブジェクトボックスからClassを選択する。
f:id:t-hom:20170101170847p:plain

すると、自動的にClass_Initializeというプロシージャが作成される。
f:id:t-hom:20170101170946p:plain

Class_Initializeプロシージャが作成されたらValue = 1を記述する。

Private Value As Long

Sub Up()
    Value = Value + 1
End Sub

Function GetValue() As Long
    GetValue = Value
End Function

Private Sub Class_Initialize()
    Value = 1
End Sub

これでValueが1で初期化されるようになったので、メインコードの最初のc.Upは削除できる。

Sub CountUpSample()
    Dim c As Counter
    Set c = New Counter
    MsgBox c.GetValue
    c.Up
    c.Up
    c.Up
    MsgBox c.GetValue
End Sub

このようにオブジェクトが生成されたときに一度だけ実行されるプロシージャをコンストラクターという。反対にオブジェクトが破棄されるタイミングで実行されるデストラクターもあるが、今回は使わないので説明は省略する。

値を設定するためのアクセサを作成する

ここでやっぱりカウンターの値を外から代入できるようにしたいという要望が発生したとしよう。ただし設定できる値は1から100の間に制限したいとする。

そこで値を設定するため、LetValueという名前でアクセサを作成する。
引数を渡すと、1から100の範囲ならValueに設定し、それ以外ならエラーを発生させるプロシージャである。

Private Value As Long

Sub Up()
    Value = Value + 1
End Sub

Function GetValue() As Long
    GetValue = Value
End Function

Sub LetValue(n As Long)
    If n >= 1 And n <= 100 Then
        Value = n
    Else
        Err.Raise vbObjectError, "Counter", "その値は設定できません。"
    End If
End Sub

Private Sub Class_Initialize()
    Value = 1
End Sub

このようにアクセサにチェック機能を持たせることで、不正な値でデータを壊すことなく、外部から安全にValueを設定することができる。

CountUpSampleは次のようにLetValueアクセサで30を設定するように書き換えたみた。

Sub CountUpSample()
    Dim c As Counter
    Set c = New Counter
    c.LetValue 30
    MsgBox c.GetValue
    c.Up
    c.Up
    c.Up
    MsgBox c.GetValue
End Sub

たとえば1000などの値に変えて実行してみるとちゃんとエラーが出るのが分かる。

アクセサ専用の構文 Property を使おう

さて、アクセサを使うことでデータを保護できるようになったが、アクセサといっても所詮ただのFunctionとSubなので、呼び出すときにはそれぞれのルールに従う必要があり、メインコードを書くときにどうにも煩わしい。

アクセサを使わずにPublic変数で作った場合はc.Value = 値、MsgBox c.Valueという風にふつうの変数と同じように書くことができたのに、GetValueとLetValueを使い分けなければいけないし、LetValueはSubプロシージャなので代入ではなく引数渡しで書かないといけない。

そこでアクセサを作る専用の構文を使用する。
それがPropertyプロシージャである。

Propertyプロシージャを使うと、外部から見たときにあたかもPublic変数に直接アクセスしているかのようにValueを操作できるようになる。

まず下準備としてPrivate変数のValueをvalue_という名前に変更しておこう。これはプロパティ名をValueにする場合に名前の競合を避けるためだ。

Private value_ As Long

Sub Up()
    value_ = value_ + 1
End Sub

Function GetValue() As Long
    GetValue = value_
End Function

Sub LetValue(n As Long)
    If n >= 1 And n <= 100 Then
        value_ = n
    Else
        Err.Raise vbObjectError, "Counter", "その値は設定できません。"
    End If
End Sub

Private Sub Class_Initialize()
    value_ = 1
End Sub

次に、Function GetValueを Property Get Value()に、Sub LetValueを Property Let Valueに書き換える。

Private value_ As Long

Sub Up()
    value_ = value_ + 1
End Sub

Property Get Value() As Long
    Value = value_
End Property

Property Let Value(n As Long)
    If n >= 1 And n <= 100 Then
        value_ = n
    Else
        Err.Raise vbObjectError, "Counter", "その値は設定できません。"
    End If
End Property

Private Sub Class_Initialize()
    value_ = 1
End Sub

Propertyプロシージャは3種類あって、値を取得するGet、設定するLet・Setがある。値の設定には、オブジェクトの場合はSetを使い、それ以外はLetを使用する。今回value_はLong型の変数なのでLetを使用する。

こうしておくと、外部からのアクセスは単にc.Valueとすればよい。

c.Value = 10といった風に代入操作を行った場合、Property Let Valueが呼ばれる。代入しているように見えるが、内部的には引数渡しなので実際のところSub LetValueのときと同じ処理である。

MsgBox c.Valueという風に参照操作を行った場合、Property Get Valueが呼ばれる。これはFunction GetValueのときと同じ処理である。

実際にやっていることはプロシージャ呼び出しだけれど、外からみたらあたかもPublic変数を直接いじっているかのように扱えるのだ。それでいてアクセサとしてのチェック機能は働いているのでデータの不正な代入は防止できる。それがProperty構文だ。

ただし特にデータをチェックする必要がないケースではわざわざPropertyを作らずにPublic変数のままで良いと思う。

おわりに

今回はシンプルなカウンターオブジェクトを例にとってクラスモジュールの使い方を説明した。記事執筆にあたり工夫したのは以下の4点。

  • 極端なくらいシンプルで理解しやすいオブジェクトを題材に選ぶこと。
  • 実際に利用するRangeオブジェクトを例に挙げて、オブジェクトは単にデータと命令がセットになったモノであると定義。
  • 変数=箱の固定概念を捨て、オブジェクトにマッチするタグの概念で説明。
  • Property構文ありきで語られることが多い「データ」の格納方法について、Public変数→Sub・Functionによるアクセサ→アクセサ専用のProperty構文と順を追って解説。

ただし今回の記事だけでは使い方は分かってもこれが一体何の役に立つのかさっぱり分からないという方もいると思う。
以下のリンクから私が過去に書いたクラスモジュール関連の記事にアクセスできるので、活用事例などを参考にしていただければと思う。

thom.hateblo.jp

クラスモジュールを扱っているVBA本

残念ながら日本の書籍ではまともにクラスモジュールを扱っているものは殆ど無い。
2018年に良い書籍が出たので紹介。

以下は日本語書籍では最高峰。クラスモジュールにも多数ページを割いている。

以下の書籍もクラスモジュールを扱っており、直感的に理解できるような言い回しの工夫がみられる良書。

Excel VBAの教科書 (Informatics & IDEA)

Excel VBAの教科書 (Informatics & IDEA)

以下の本がクラスモジュールに少しページを割いている。

そこが知りたい!Excel VBAプロの技 Excel97/2000/2002/2003対応!

そこが知りたい!Excel VBAプロの技 Excel97/2000/2002/2003対応!

(絶版書のため価格高騰。希少本だったが、先述の2点が発売された今となってはわざわざこちらを買うメリットは無いかも。)

洋書なら以下の本が詳しいのでおススメである。

VBA Developer's Handbook

VBA Developer's Handbook

(絶版書のため価格高騰。しかし1000ページ超あり、日本語書籍とは比べものにならないくらい情報満載なので購入する価値はあると思う。)

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。