無限不可能性ドライブ

『ニューラルネットワーク自作入門』に刺激されてExcelVBAでニューラルネットワークを作ってみたものの、やっぱり数学やらなきゃと思い少しずつやってきたのもあって、自分の知識の整理とかそういった感じです。

【VBA編】(準備)クラスについて(1)

準備

VBAニューラルネットワークを実装するにあたり、(ここでの実装では)クラスの概念が必要になります。
とはいえ、VBAでクラスを使うのはあまりなじみがないと思いますので、ここでは準備段階として簡単に説明しようと思います。

オブジェクト指向

オブジェクトとかクラスとかいう言葉を聞くと拒否反応を起こしてしまう方もいるのではないでしょうか。
でも、大丈夫です。
ExcelVBAはオブジェクト指向を完全にはサポートしていないので、覚えることはそれほど多くありません。
なので、ひとまずはあまりオブジェクト指向を意識しなくてもよいと思います。
とはいえ、概念だけでも知っておく必要はありますので、該当する部分だけを簡単に説明します。

クラス

クラスとは一般的には設計図やひな型のようなものという説明がされることが多いかと思います。
オブジェクト指向では、その設計図・ひな型(クラス)をもとに実体(オブジェクト、インスタンス)を作って利用していきます。
同じクラスから作られたオブジェクトは同じ属性、機能を持ちますが、(基本的には)それぞれが独立しています。
ユーザフォームのコントロールのようなものと考えればよいでしょうか。
たとえば、コマンドボタンを配置すると、いろいろと初期値が設定されていますが、
キャプションや色などはそれぞれのコマンドボタンごとに自由に設定できます(独立しています)。
クラスを定義し、そこからオブジェクトを作ることで、同じ機能を持つただし若干異なるオブジェクトを(比較的簡単に)大量に作ることができます。
オブジェクト指向では、継承や多様性などいろいろな用語が出てきますが、VBAではこの程度の理解で大丈夫だと思います。
つまり、
  「設計図・ひな型(クラス)となるものを用意し、それをもとに実体(オブジェクト)を作る」
まずは、これを抑えておきましょう。

[ひな型をもとに実体を作る]
f:id:celaeno42:20181101233203p:plain:w400

3冊の本を作ってみる

例として3冊の本を作ってみましょう。
本はそれぞれ「コード」「タイトル」「著者」「(税抜き)価格」の情報を持っていて、
「コード」「タイトル」「著者」「税抜き価格」「税込み価格」を表示します。

クラスを使わないで書いてみる

まずはクラスを使わないで書いてみましょう。
本ごとに必要な変数を用意して、それぞれの変数に必要な情報を格納するようなコードにしています。
(ユーザ定義型を使えばもう少しスマートに書けるとは思いますが…)

'[標準モジュール]
Option Explicit

Public Sub クラス不使用()
    Dim book01Code As String
    Dim book01Title As String
    Dim book01Author As String
    Dim book01Price As Long

    Dim book02Code As String
    Dim book02Title As String
    Dim book02Author As String
    Dim book02Price As Long
    
    Dim book03Code As String
    Dim book03Title As String
    Dim book03Author As String
    Dim book03Price As Long
    
    book01Code = "bk-01"
    book01Title = "長時間の影"
    book01Author = "H.P.ラヴクラフト"
    book01Price = 800
    
    book02Code = "bk-02"
    book02Title = "異次元を覗く家"
    book02Author = "W.H.ホジスン"
    book02Price = 1000
    
    book03Code = ""
    book03Title = ""
    book03Author = ""
    book03Price = -100
    
    Call printBook(book01Code, book01Title, book01Author, book01Price)
    Call printBook(book02Code, book02Title, book02Author, book02Price)
    Call printBook(book03Code, book03Title, book03Author, book03Price)
    
End Sub

Private Sub printBook(ByRef aCode As String, ByRef aTitle As String, ByRef aAuthor As String, ByRef aPrice As Long)
    Const TAX As Double = 1.08
    
    Debug.Print "----" & aCode & "----"
    Debug.Print "タイトル = " & aTitle
    Debug.Print "著者 = " & aAuthor
    Debug.Print "税抜き価格 = " & aPrice
    Debug.Print "税込み価格 = " & aPrice * TAX
    Debug.Print "------------"
End Sub


[イミディエイトウィンドウ](出力結果)

----bk-01----
タイトル = 長時間の影
著者 = H.P.ラヴクラフト
税抜き価格 = 800
税込み価格 = 864
------------
----bk-02----
タイトル = 異次元を覗く家
著者 = W.H.ホジスン
税抜き価格 = 1000
税込み価格 = 1080
------------
--------
タイトル = 
著者 = 
税抜き価格 = -100
税込み価格 = -108
------------


クラスを使って書いてみる - 1(クラスモジュール)

では、これをクラスを使って書いてみましょう。
クラスはクラスモジュールに書きますので、VBEの[挿入]から「クラスモジュール」を選んでクラスモジュールを追加しましょう。
クラスモジュールのオブジェクト名は「classBook」としておきます。

VBEのプロジェクトはこのようになります。
f:id:celaeno42:20181103231252p:plain

以下のコードは classBook に書きます。

'[クラスモジュール - classBook]
Option Explicit

Public pCode As String
Public pTitle As String
Public pAuthor As String
Public pPrice As Long
Const TAX As Double = 1.08

Public Function exTaxPrice() As Long
    exTaxPrice = pPrice
End Function

Public Function inTaxPrice() As Long
    inTaxPrice = pPrice * TAX
End Function

「コード」「タイトル」「著者」「(税抜き)価格」はこのモジュールの外から呼ばれることを想定して、それぞれ Public変数として宣言しています。
これらの「クラスの持つ変数」をここでは他の言語のマネをして「メンバ変数」と呼ぶことにします。
また、定数として「税率」を持っていて、「税抜き価格」「税込み価格」は Function プロシージャで計算結果を戻すようにしています。
(もっとも、「税抜き価格」はそのまま「価格」を返しているだけですが、Functionプロシージャとすることで、呼び出し側で「税込み価格」と同じような書式で呼び出すことができます。)

では、呼び出し側の処理と、結果を出力する処理を見てみましょう。
これらは標準モジュールに書いています。

'[標準モジュール]
Option Explicit

Public Sub クラス使用()
    Dim cBook11 As classBook
    Dim cBook12 As classBook
    Dim cBook13 As classBook
    
    Set cBook11 = New classBook
    cBook11.pCode = "bk-11"
    cBook11.pTitle = "銀河ヒッチハイクガイド"
    cBook11.pAuthor = "ダグラス・アダムス"
    cBook11.pPrice = 420
    
    Set cBook12 = New classBook
    cBook12.pCode = "bk-12"
    cBook12.pTitle = "プロジェクトぴあの"
    cBook12.pAuthor = "山本弘"
    cBook12.pPrice = 1500
    
    Set cBook13 = New classBook
    cBook13.pTitle = ""
    cBook13.pPrice = -100
    
    Call printBookInfo(cBook11)
    Call printBookInfo(cBook12)
    Call printBookInfo(cBook13)

    Set cBook11 = Nothing
    Set cBook12 = Nothing
    Set cBook13 = Nothing

End Sub

Private Sub printBookInfo(ByRef aBook As classBook)
    Debug.Print "----" & aBook.pCode & "----"
    Debug.Print "タイトル = " & aBook.pTitle
    Debug.Print "著者 = " & aBook.pAuthor
    Debug.Print "税抜き価格 = " & aBook.exTaxPrice
    Debug.Print "税込み価格 = " & aBook.inTaxPrice
    Debug.Print "------------"
End Sub

クラスは変数として宣言できます。
通常の変数と同様に

 Dim 変数名 as クラス名

とすればOKです。

そのあとで

 Set 変数名 = New クラス名

とすることで、クラス(設計図)からインスタンス(オブジェクト / 実体)が作られます。
(一般的に 「Newする」といいます。)

クラスの持つ Public な変数やプロシージャなどにアクセスするには、「 . 」(ドット演算子)を用います。
上記コードでは、[ cBook11.pCode = "bk-01" ] などとしているところで、
これは、「cBook11 オブジェクト のメンバ変数 pCode に 文字列 "bk-01" を代入する」という意味です。
フォームコントロールの [ Text1.Text = "bk-01" ] などの書き方と同じですね。

また、クラスはプロシージャの引数として渡すこともできます。
上記コードでは、printBookInfoの引数として classBook型の変数 を渡しています。

最後の [ Set cBook11 = Nothing ] でオブジェクトを解放(破棄)しています。
ただ、VBAの場合、変数は所属するプロシージャが終了すると破棄されるので、あまり神経質にならなくてもいいのでは?との意見もあります。

では、実行して出力を見てみましょう。
設定した値が表示されているのがわかりますね。

[イミディエイトウィンドウ]

----bk-11----
タイトル = 銀河ヒッチハイクガイド
著者 = ダグラス・アダムス
税抜き価格 = 420
税込み価格 = 454
------------
----bk-12----
タイトル = プロジェクトぴあの
著者 = 山本弘
税抜き価格 = 1500
税込み価格 = 1620
------------
--------
タイトル = 
著者 = 
税抜き価格 = -100
税込み価格 = -108
------------


クラスを使って書いてみる - 2(プロパティ)

さて、設定した値が表示されるのはいいのですが、例えばタイトルにブランクを設定したら「未定」と表示させたり、価格にマイナスの値が設定されたら 0 にしたりというようにしたい場合、上記のコードでは実現できません。
そこで、classBook をもう少し使い勝手の良いクラスに修正してみましょう。

'[クラスモジュール - classBook]
Option Explicit

Dim mCode As String
Dim mTitle As String
Dim mAuthor As String
Dim mPrice As Long
Const TAX As Double = 1.08

Public Property Let Code(ByRef aCode As String)
    If aCode = "" Then
        aCode = "bk-xx"
    Else
        mCode = aCode
    End If
End Property

Public Property Get Code() As String
    Code = mCode
End Property

Public Property Let Title(ByRef aTitle As String)
    If aTitle = "" Then
        mTitle = "未定"
    Else
        mTitle = aTitle
    End If
End Property

Public Property Get Title() As String
    Title = mTitle
End Property

Public Property Let Author(ByRef aAuthor As String)
    If aAuthor = "" Then
        mAuthor = "未定"
    Else
        mAuthor = aAuthor
    End If
End Property

Public Property Get Author() As String
    Author = mAuthor
End Property

Public Property Let Price(ByRef aPrice As Long)
    If aPrice < 0 Then
        mPrice = 0
    Else
        mPrice = aPrice
    End If
End Property

Public Function exTaxPrice() As Long
    exTaxPrice = mPrice
End Function

Public Function inTaxPrice() As Long
    inTaxPrice = mPrice * TAX
End Function


少し長くなりましたが解説していきましょう。
まず、前回 Public で宣言していたメンバ変数を Dim の宣言に変更しています。
そのため、前回のように呼び出し側(classBookの外)からは直接メンバ変数を操作できなくなっています。
そこで、メンバ変数を操作するために、Propertyプロシージャを利用します。
コードを見ると Property Let、Property Get でなんらかの同じ名前の定義をしていることがわかると思います。
これは、他の言語では セッター/ゲッター(Setter/Getter) と呼ばれるもので、クラスのメンバ変数の値を設定したり取得したりするプロシージャです。
VBAでは Property プロシージャと呼ばれ、通常は、Let(オブジェクトの場合はSet)と Get で同じ名前のプロシージャを定義して、値を設定したり取得したりする際に利用します。
Let(Set)は1つの引数をとり、Get は引数をとることができないという点も Property プロシージャ独特の仕様です。
これも次のようなフォームコントロールの使い方と同様です。

 Text1.Text = "Hello World"
 result = Text1.Text
 Debug.Print(result)

 [出力]
 Hello World

一部を抜粋しましょう。

Dim mTitle As String

Public Property Let Title(ByRef aTitle As String)
    If aTitle = "" Then
        mTitle = "未定"
    Else
        mTitle = aTitle
    End If
End Property

Public Property Get Title() As String
    Title = mTitle
End Property

これは Titleプロパティに Let で値(引数の aTitle) を設定、Get で値を取得するコードです。
引数 aTitle がブランクの場合は、Titleのデフォルト値として "未定" を設定するようにしています。
設定値はメンバ変数 mTitle に格納し、Get の戻り値としています。

これは例えば、

 cBook.Title = "アイの物語"  '値の設定(Let が呼ばれる)
 result = cBook.Title     '値の取得(Get が呼ばれる)
 Debug.Print(result)

 [出力]
 アイの物語

のように使います。

Dim mPrice As Long
Const TAX As Double = 1.08

Public Property Let Price(ByRef aPrice As Long)
    If aPrice < 0 Then
        mPrice = 0
    Else
        mPrice = aPrice
    End If
End Property

Public Function exTaxPrice() As Long
    exTaxPrice = mPrice
End Function

Public Function inTaxPrice() As Long
    inTaxPrice = mPrice * TAX
End Function

上記コードは、Let で価格(Price)を設定している個所です。
価格がマイナス(aPrice が 0 未満)なら、価格に 0 を設定するようにしています。
ただ、Price プロパティ については、Get がありません。
(代わりに exTaxPrice()、inTaxPrice() で値を返しています。)
このように、必ずしも Let(Set) と Get を対で利用しなくてもかまいません。

では、これを標準モジュールから呼び出してみましょう。

'[標準モジュール]
Option Explicit

Public Sub クラス使用()
    Dim cBook11 As classBook
    Dim cBook12 As classBook
    Dim cBook13 As classBook
    
    Set cBook11 = New classBook
    cBook11.Code = "bk-11"
    cBook11.Title = "銀河ヒッチハイクガイド"
    cBook11.Author = "ダグラス・アダムス"
    cBook11.Price = 420
    
    Set cBook12 = New classBook
    cBook12.Code = "bk-12"
    cBook12.Title = "プロジェクトぴあの"
    cBook12.Author = "山本弘"
    cBook12.Price = 1500
    
    Set cBook13 = New classBook
    cBook13.Title = ""
    cBook13.Price = -100
    
    Call printBookInfo(cBook11)
    Call printBookInfo(cBook12)
    Call printBookInfo(cBook13)

    Set cBook11 = Nothing
    Set cBook12 = Nothing
    Set cBook13 = Nothing

End Sub

Private Sub printBookInfo(ByRef aBook As classBook)
    Debug.Print "----" & aBook.Code & "----"
    Debug.Print "タイトル = " & aBook.Title
    Debug.Print "著者 = " & aBook.Author
    Debug.Print "税抜き価格 = " & aBook.exTaxPrice
    Debug.Print "税込み価格 = " & aBook.inTaxPrice
    Debug.Print "------------"
End Sub

[イミディエイトウィンドウ]

----bk-11----
タイトル = 銀河ヒッチハイクガイド
著者 = ダグラス・アダムス
税抜き価格 = 420
税込み価格 = 454
------------
----bk-12----
タイトル = プロジェクトぴあの
著者 = 山本弘
税抜き価格 = 1500
税込み価格 = 1620
------------
--------
タイトル = 未定
著者 = 
税抜き価格 = 0
税込み価格 = 0
------------

呼び出し方は先ほどとそれほど変わっていません。
ただ、3冊めの出力結果が若干変わっていて、タイトル = 未定 となっていたり、価格が(マイナスでなく)0 になっています。
これは、プロパティの処理で引数がブランクだったりマイナスだったりしたときにデフォルト値を設定するようにしているためです。

このように、メンバ変数へのアクセスは必ず Property や Function から行うようにすることで、(価格にマイナスが設定されるような)不用意な操作を防ぐことができ、より安全性の高いコードになります(なるといわれてます)。
オブジェクト指向では「情報隠蔽」とか「カプセル化」といったりします。)

クラスを使って書いてみる - 3(初期化1)

もう一度3つめの出力結果をよく見てみると、「コード」と「著者名」がブランクになっています。
それぞれの Property プロシージャでは、「引数としてブランクが渡されたらデフォルト値に設定する」としていましたが、そもそも呼び出し側でこれらの項目については何の設定もしていないため、Code と Author については、Let プロシージャが呼ばれていないのです。そのため、Get プロシージャでは何の設定もされていない mCode, mAuthor が返されています。

そこで、classBook が New されたときに初期値を設定することで「何も設定しなくても初期値が設定されている」ように修正してみましょう。

'[クラスモジュール - classBook]
Option Explicit

Dim mCode As String
Dim mTitle As String
Dim mAuthor As String
Dim mPrice As Long
Const TAX As Double = 1.08

Private Sub Class_Initialize()
    mCode = "bk-xx"
    mTitle = "未定"
    mAuthor = "未定"
    mPrice = 0
End Sub

Public Property Let Code(ByRef aCode As String)
    mCode = aCode
End Property

Public Property Get Code() As String
    Code = mCode
End Property

Public Property Let Title(ByRef aTitle As String)
    If aTitle <> "" Then
        mTitle = aTitle
    End If
End Property

Public Property Get Title() As String
    Title = mTitle
End Property

Public Property Let Author(ByRef aAuthor As String)
    If aAuthor <> "" Then
        mAuthor = aAuthor
    End If
End Property

Public Property Get Author() As String
    Author = mAuthor
End Property

Public Property Let Price(ByRef aPrice As Long)
    If aPrice > 0 Then
        mPrice = aPrice
    End If
End Property

Public Function exTaxPrice() As Long
    exTaxPrice = mPrice
End Function

Public Function inTaxPrice() As Long
    inTaxPrice = mPrice * TAX
End Function

f:id:celaeno42:20181103233013p:plain:w600


VBEのオブジェクトボックス(①の箇所)で「Class」、プロシージャボックス(②の箇所)で「Initialize」を選択すると、「Class_Initialize()」プロシージャが挿入されます。
このプロシージャはクラスが New されたときに自動的に呼び出される(実行される)プロシージャで、オブジェクトの初期設定に利用することができます。
他の言語では「コンストラクタ」と呼ばれる機能に近いと思います。
ただし、VBAの「Class_Initialize()」プロシージャは(残念ながら)引数をとることができないので、New する際には同じ初期値(デフォルト値)でしか初期化できません。
今回は、「コード」は「bk-xx」、「タイトル」は「未定」、「著者」は「未定」、「価格」は「0」で初期化しています。
また、前のコードでは、Let プロシージャで引数にブランクや0が渡されたときは、それぞれのデフォルト値を設定するようにしていましたが、今回のコードでは無視するようにしています。
New したときに自動的に実行される「Class_Initialize()」で初期値を設定しているため、それぞれの Let プロシージャであらためてデフォルト値を設定する必要がないためです。

では、実際に呼び出して結果を見てみましょう。
呼び出し元のコードは前回と同じです。

[イミディエイトウィンドウ]

----bk-11----
タイトル = 銀河ヒッチハイクガイド
著者 = ダグラス・アダムス
税抜き価格 = 420
税込み価格 = 454
------------
----bk-12----
タイトル = プロジェクトぴあの
著者 = 山本弘
税抜き価格 = 1500
税込み価格 = 1620
------------
----bk-xx----
タイトル = 未定
著者 = 未定
税抜き価格 = 0
税込み価格 = 0
------------

3冊目の「著者」がきちんと「未定」になっていますね。

クラスを使って書いてみる - 4(初期化2)

さて、いまの classBook では、New した後に「タイトル」プロパティには「タイトル」、「著者」プロパティには「著者名」など、それぞれのプロパティを代入する必要があります。
これはちょっと面倒なので、一気に設定できると便利ですね。
そこでプロパティを一気に設定できるプロシージャを作ることにしましょう。
先ほども書いたように、他の言語では New する際に初期値を引数で渡せますが、VBAではそれができないので、New した後にこの「初期値設定用のプロシージャ」を呼び出すことで、プロパティをひとつひとつ設定する手間を省くことができます。
今回、この「初期値設定用のプロシージャ」は「Initialize()」という名前にしましたが、「Class_Initialize()」と紛らわしいので別の名前にしても構いません。
(私は「Constructor()」とすることもあります。)

'[クラスモジュール - classBook]
Option Explicit

Dim mCode As String
Dim mTitle As String
Dim mAuthor As String
Dim mPrice As Long
Const TAX As Double = 1.08

Private Sub Class_Initialize()
    mCode = "bk-xx"
    mTitle = "未定"
    mAuthor = "未定"
    mPrice = 0
End Sub

'プロパティを一度に設定する
Public Sub Initialize(ByRef aCode As String, ByRef aTitle As String, ByRef aAuthor As String, ByRef aPrice As Long)
    Me.Code = aCode
    Me.Title = aTitle
    Me.Author = aAuthor
    Me.Price = aPrice
End Sub

Public Property Let Code(ByRef aCode As String)
    If aCode <> "" Then
        mCode = aCode
    End If
End Property

Public Property Get Code() As String
    Code = mCode
End Property

Public Property Let Title(ByRef aTitle As String)
    If aTitle <> "" Then
        mTitle = aTitle
    End If
End Property

Public Property Get Title() As String
    Title = mTitle
End Property

Public Property Let Author(ByRef aAuthor As String)
    If aAuthor <> "" Then
        mAuthor = aAuthor
    End If
End Property

Public Property Get Author() As String
    Author = mAuthor
End Property

Public Property Let Price(ByRef aPrice As Long)
    If aPrice > 0 Then
        mPrice = aPrice
    End If
End Property

Public Function exTaxPrice() As Long
    exTaxPrice = mPrice
End Function

Public Function inTaxPrice() As Long
    inTaxPrice = mPrice * TAX
End Function

「Initialize()」プロシージャでは、引数をそれぞれの Property プロシージャに渡しています。
これは、「Initialize()」 の引数として渡されてきた値が不用意にプロパティに格納されないようにするためです。
いったん Property プロシージャに渡すことで Property Let でのチェックが働くので、コードの安全性を高めることができます。

では、呼び出し側を見てみましょう。

'[標準モジュール]
Option Explicit

Public Sub クラス使用()
    Dim cBook11 As classBook
    Dim cBook12 As classBook
    Dim cBook13 As classBook
    
    Set cBook11 = New classBook
    cBook11.Code = "bk-11"
    cBook11.Title = "銀河ヒッチハイクガイド"
    cBook11.Author = "ダグラス・アダムス"
    cBook11.Price = 420
    
    Set cBook12 = New classBook
    Call cBook12.Initialize("bk-12", "プロジェクトぴあの", "山本弘", 1500)
    
    Set cBook13 = New classBook
    Call cBook13.Initialize("", "", "", -100)
    
    Call printBookInfo(cBook11)
    Call printBookInfo(cBook12)
    Call printBookInfo(cBook13)
    
    '価格改定!
    Debug.Print
    Debug.Print "--! 価格改定 !--"
    cBook12.Price = 1200
    Call printBookInfo(cBook12)

    Set cBook11 = Nothing
    Set cBook12 = Nothing
    Set cBook13 = Nothing

End Sub

Private Sub printBookInfo(ByRef aBook As classBook)
    Debug.Print "----" & aBook.Code & "----"
    Debug.Print "タイトル = " & aBook.Title
    Debug.Print "著者 = " & aBook.Author
    Debug.Print "税抜き価格 = " & aBook.exTaxPrice
    Debug.Print "税込み価格 = " & aBook.inTaxPrice
    Debug.Print "------------"
End Sub

「cBook12」と「cBook13」のプロパティ設定を今回作成した「Initialize()」プロシージャで行っています。
「cBook11」と比べるとスッキリしていますね。
なお、必要に応じて「Initialize()」プロシージャの引数に Optional キーワードを使うことで、呼び出し側で必要な引数のみを設定すればよくなるため、より使い勝手の良いクラスになると思います。

今回は、一度書籍情報を表示した後、「cBook12」の価格を変更してみました。
このようにいったんオブジェクトの初期値を一度に設定した後、一部のプロパティを変更することも可能です。

出力はこのようになります。

[イミディエイトウィンドウ]

----bk-11----
タイトル = 銀河ヒッチハイクガイド
著者 = ダグラス・アダムス
税抜き価格 = 420
税込み価格 = 454
------------
----bk-12----
タイトル = プロジェクトぴあの
著者 = 山本弘
税抜き価格 = 1500
税込み価格 = 1620
------------
----bk-xx----
タイトル = 未定
著者 = 未定
税抜き価格 = 0
税込み価格 = 0
------------

--! 価格改定 !--
----bk-12----
タイトル = プロジェクトぴあの
著者 = 山本弘
税抜き価格 = 1200
税込み価格 = 1296
------------

2冊目と3冊目が正しく初期値設定されていることと、きちんと2冊目の価格が変更されたことがわかりますね。

次回はクラスについてもうちょっと見ていきます。