無限不可能性ドライブ

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

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

クラスについて その2

前回に引き続きクラスについてもう少し見ていきましょう。

インスタンス(オブジェクト)

クラスを使うにあたっての利点の一つは1つのクラスから複数のインスタンス(実体)を作れることではないでしょうか。
特に同じようで若干異なる処理をさせたいときなどに役に立つと思います。

次のような処理をしたい場合、クラスを使わないで書くとしたらどうなるでしょうか。
f:id:celaeno42:20181105230935p:plain

[処理結果]
f:id:celaeno42:20181105231040p:plain

ひとまずこのようにしてみました。
各種類の次の書き込み行をそれぞれの変数で制御しているのが煩わしい感じがします。

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

Public Enum CL
    Shokuhin = 5
    Bunbougu = 7
    Kaden = 9
End Enum

Public Sub Main()
    Dim r As Long
    Dim eRow As Long
    Dim shurui As String
    Dim rwShokuhin As Long
    Dim rwBunbougu As Long
    Dim rwKaden As Long
    
    Cells(1, CL.Shokuhin).Value = "食品"
    Cells(1, CL.Bunbougu).Value = "文房具"
    Cells(1, CL.Kaden).Value = "家電"
    
    rwShokuhin = 2
    rwBunbougu = 2
    rwKaden = 2
    
    eRow = Cells(Rows.Count, 1).End(xlUp).Row

    For r = 2 To eRow
        shurui = Cells(r, 1).Value

        Select Case shurui
            Case "食品"
                Cells(rwShokuhin, CL.Shokuhin).Value = Cells(r, 2).Value
                rwShokuhin = rwShokuhin + 1
            Case "文房具"
                Cells(rwBunbougu, CL.Bunbougu).Value = Cells(r, 2).Value
                rwBunbougu = rwBunbougu + 1
            Case "家電"
                Cells(rwKaden, CL.Kaden).Value = Cells(r, 2).Value
                rwKaden = rwKaden + 1
        End Select

    Next

    MsgBox "終了しました。"    

End Sub

クラスを使って書いてみます。

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

Dim mShurui As String
Dim mRow As Long
Dim mCol As Long

'初期化処理
'自分の担当する種類を覚えて、自分の列の1行目に表示する
'[引数] <- aShurui / String:自分の担当する種類, aCol / Long:表示先列
Public Sub Initialize(ByRef aShurui As String, ByRef aCol As Long)
    mShurui = aShurui
    mRow = 2
    mCol = aCol
    
    Cells(1, mCol).Value = mShurui
End Sub

'品目追加処理
'表示先セルに品目を表示して表示先行をインクリメントする
'[引数] <- aItem / String:品目
Public Sub AddItem(ByRef aItem As String)
    Cells(mRow, mCol).Value = aItem
    mRow = mRow + 1
End Sub

'自分の種類を返す
Public Property Get Shurui() As String
    Shurui = mShurui
End Property

「初期化処理」では、自分の担当する「種類」と「表示先の列」を引数として取っています。
「種類」と「列」を変数に格納したら、1行目に種類を表示します。
「品目」の表示は2行目からなので、mRow に 2 をセットしています。
なお、今回は 「Class_Initialize()」での処理は不要と判断して定義していません。

「品目追加処理」では、「品目」を引数として取り、表示先のセルに表示しています。
その後、行を更新し次の表示に備えています。

あと、呼び出し元で利用するために、「種類」を返す Property Get を定義しています。
今回は、「種類」の設定は「初期化処理」のみで行うので、Property Let は定義しませんでした。

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

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

Public Sub Main_class()
    Dim r As Long
    Dim eRow As Long
    Dim item As String
    
    Dim cShokuhin As classPlot
    Dim cBunbougu As classPlot
    Dim cKaden As classPlot
    
    Set cShokuhin = New classPlot
    Set cBunbougu = New classPlot
    Set cKaden = New classPlot
    
    '種類と表示先列を引数として振り分け担当(オブジェクト)を作る
    Call cShokuhin.Initialize("食品", 5)
    Call cBunbougu.Initialize("文房具", 7)
    Call cKaden.Initialize("家電", 9)
    
    eRow = Cells(Rows.Count, 1).End(xlUp).Row
    
    For r = 2 To eRow
        item = Cells(r, 2).Value
        Select Case Cells(r, 1).Value
            Case cShokuhin.Shurui
                Call cShokuhin.AddItem(item)
            Case cBunbougu.Shurui
                Call cBunbougu.AddItem(item)
            Case cKaden.Shurui
                Call cKaden.AddItem(item)
        End Select
    Next
    
    MsgBox "終了しました。"
    
End Sub

「食品」「文房具」「家電」を担当する「cShokuhin」「cBunbougu」「cKaden」を宣言し、
それぞれのインスタンスを生成しています(New しています)。
それぞれに該当の引数を与えて「初期化処理」をした後、A列を順番に見ていって
該当するインスタンスの「AddItem()」を呼び出して、「品目」を表示しています。

先ほどのコードと異なる点は、「品目」を表示する際に、呼び出し元では「何行目に表示するか」を一切気にしていないというところです。
また、列についても、「初期化処理」で設定してしまえば、以降は気にする必要がありません。

クラスを使うとコードが長くなる場合がありますが、必要な設定や処理はクラスに任せて、
呼び出し側では「あとはお願い」的にコードが書けるので考慮すべきことが少なくなります。

ということは、バグが混入する余地を減らすことにもつながります。

配列への格納

複数のインスタンスを作る際、上記の例のようにそれぞれで変数を宣言してもいいのですが、
宣言すべき変数が多くなる場合や、そもそもいくつ変数が必要になるかがわからない場合があるかと思います。
そういった場合はインスタンスを配列に格納すると便利です。

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

Dim mIndex As Long
Dim mColor As Long
Dim mBaseColor As Long
Dim mRow As Long
Dim mCol As Long

Private Sub Class_Initialize()
    mRow = 1
    mBaseColor = Cells(1, 1).Interior.Color
End Sub

Public Sub Initialize(ByRef aIndex As Long, ByRef aCol As Long)
    mIndex = aIndex
    mColor = Cells(1, aCol).Interior.Color
    mCol = aCol
End Sub

Public Property Get MyIndex() As Long
    MyIndex = mIndex
End Property

Public Property Get MyRow() As Long
    MyRow = mRow
End Property

Public Property Get MyColumn() As Long
    MyColumn = mCol
End Property

Public Function GoNext() As Boolean
    Dim res As Boolean
    Dim offsetR As Long
    Dim offsetC As Long
    
    res = True
    offsetR = 0
    offsetC = 0
    
    If Cells(mRow, mCol + 1).Interior.Color <> mBaseColor And Cells(mRow, mCol + 1).Interior.Color <> mColor Then
        offsetC = 1
    ElseIf Cells(mRow, mCol - 1).Interior.Color <> mBaseColor And Cells(mRow, mCol - 1).Interior.Color <> mColor Then
        offsetC = -1
    ElseIf Cells(mRow + 1, mCol).Interior.Color <> mBaseColor And Cells(mRow + 1, mCol).Interior.Color <> mColor Then
        offsetR = 1
    Else
        res = False
    End If
    
    If res Then
        mRow = mRow + offsetR
        mCol = mCol + offsetC
        
        Application.Wait [Now()+"00:00:00.1"]
        Cells(mRow, mCol).Interior.Color = mColor
    End If
    
    GoNext = res
End Function

今回はクラスモジュールのコード説明は省略します。

呼び出し側はこのようにしています。

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

Public Sub Main()
    Dim cPlayers() As classPlayer
    Dim i As Long
    Dim flagNext As Boolean
    
    ReDim cPlayers(4)
    
    For i = 1 To UBound(cPlayers)
        Set cPlayers(i) = New classPlayer
        Call cPlayers(i).Initialize(i, getColumn(i))
    Next
    
    For i = 1 To UBound(cPlayers)
        Do
            flagNext = cPlayers(i).GoNext
        Loop Until flagNext = False
        Cells(cPlayers(i).MyRow, cPlayers(i).MyColumn).Value = cPlayers(i).MyIndex
    Next
    
End Sub

Private Function getColumn(ByRef aIndex As Long) As Long
    Dim res As Long
    
    Select Case aIndex
        Case 1
            res = 2
        Case 2
            res = 6
        Case 3
            res = 10
        Case 4
            res = 14
    End Select
    
    getColumn = res
    
End Function

まずは、classPlayer 型の配列 cPlayers() を宣言しています。
素数はここで設定してもいいのですが、今回は ReDim で設定しています。

ひとつめの For - Next ですが、
[ Set cPlayers(i) = New classPlayer ] とすることによって配列の要素ごとにインスタンスを格納しています。
そのあとで [ Call cPlayers(i).Initialize() ] を実行して、インスタンスの初期化を行っています。

次の For - Next では、配列から取り出したインスタンスを利用して処理を行っています。

このように配列に格納することで、取り回しを楽にすることができます。
(実際、この後で書いていくニューラルネットワークのコードでは、この手法を利用しています。)

ちなみに [ Option Base 1 ] で配列のインデックスを 1 始まりにしているのは、
今回のコードに都合がいいようにそうしているだけです。


上記コードはExcelシートにこのようなあみだくじを作成して実行してみてください。
色は同じでなくても構いません。
(実行前に復帰用にシートをコピーしておくことをお勧めします。)
f:id:celaeno42:20181108230439p:plain


これでひとまずクラスの説明は終わります。

クラスを使えるようにしましょう

VBAはクラスを使わなくてもコードを書くことは可能です。
しかし、クラスを使うことで、より簡単に、便利に、安全にコードを書くことができるようになります。
もちろん、適材適所がありますので、なんでもかんでもクラスにすればいいというわけではありません。
ただ、必要な時にクラスが使えるようにしておくことで、手持ちの武器が増え、表現の幅が広がります。
クラスを使うコードは使わないコードとちょっと発想の転換が必要というか頭の使い方が異なると思うので、はじめはなかなか慣れないかもしれません。
そういった場合はちょっとVBAから離れて「オブジェクト指向」の考え方のほうに目を向けてみるのもいいかと思います。