VBAの「コンストラクタに引数渡せない」問題

VBAの「コンストラクタに引数を渡せない」問題

VBAでクラスモジュールを使い始めたときに必ずぶち当たるのが、

なんでコンストラクタに引数が渡せねえんだよ!
この金髪豚野郎!!!!!!!!!


問題だろう。(個人の感想です)

これは本当に不便な話で、「VBAのイマイチなところ大特集」でもやったら、かなり上位に来る項目だと思う。

よくあるやり方は、別途initメソッドを持たせておいて、Newしたら必ずセットでinitメソッドも実行する、というもの。これは自分でもよくやる。

initメソッド未実行ならば、一切メソッドの実行もプロパティの参照もさせないようにしておくことで、まあそこそこの信頼性は確保できる。

akashi-keirin.hatenablog.com

こんな感じ。まあ、このときはinitメソッド実行済みチェックが不完全ですなあ。ははは。

達人の方なんかだと

VBA 自分と同じクラスの新規オブジェクトを返すメソッドを作ってコレクションにスマートに代入する

みたいに、すげえやり方で実装していたりする。

私はなにぶんにも素人なので、もっとアホみたいな対応策を考えてみた。

インスタンス作成用Function

CreateObject関数みたいな感じのFunctionを作って、Newさせずにインスタンスを得る、という方向で考えた。

リスト1 標準モジュールの宣言セクション

まずは、準備として標準モジュールの宣言セクションに次のコードを書く。

Public Enum RacerClass
  SS = 0
  S1
  S2
  S3
  A1
  A2
  A3
  A4
  B1
  B2
End Enum

Public isInstantiated As Boolean

列挙体とPublic変数の宣言をしているだけ。先に書くと、Public変数を使う、という点が今回の対応のイマイチなところだという自覚はあるw

スト2 クラスモジュール

クラスモジュールを挿入し、オブジェクト名を「SampleRacer」とした。

Option Explicit
Private Const ERROR_MESSAGE_10001 As String = _
  "SampleRacerクラスを直接Newすることはできません。" & vbCrLf & _
  "createSampleRacerObjectメソッドを使用してインスタンス化してください。"
Private Const ERROR_MESSAGE_10002 As String = _
  "initメソッドを複数回実行することはできません。"

Private registeredName_ As String
Private racingClass_ As RacerClass
Private graduatedTerm_ As Integer
Private isInitialized As Boolean

Public Property Get registeredName() As String
  registeredName = registeredName_
End Property

Public Property Get racingClass() As String
  Select Case racingClass_
    Case RacerClass.SS
      racingClass = "S級S班"
    Case RacerClass.S1
      racingClass = "S級1班"
    Case RacerClass.S2
      racingClass = "S級2班"
    Case RacerClass.S3
      racingClass = "S級3班"
    Case RacerClass.A1
      racingClass = "A級1班"
    Case RacerClass.A2
      racingClass = "A級2班"
    Case RacerClass.A3
      racingClass = "A級3班"
    Case RacerClass.A4
      racingClass = "A級4班"
    Case RacerClass.B1
      racingClass = "B級1班"
    Case RacerClass.B2
      racingClass = "B級2班"
  End Select
End Property

Public Property Get graduatedTerm() As Integer
  graduatedTerm = graduatedTerm_
End Property

Private Sub Class_Initialize()    '……(1)'
  If Not isInstantiated Then _
      Err.Raise Number:=10001, _
                Description:=ERROR_MESSAGE_10001
  isInstantiated = False
End Sub

Public Sub init(ByVal racerName As String, _
                ByVal racingClass As RacerClass, _
                ByVal graduatedTerm As Integer)    '……(2)'
  If isInitialized Then Err.Raise Number:=10002, _
                                  Description:=ERROR_MESSAGE_10002
  Call setRacer(racerName, racingClass, graduatedTerm)
  isInitialized = True
End Sub

Public Sub setRacer(ByVal racerName As String, _
                ByVal racingClass As RacerClass, _
                ByVal graduatedTerm As Integer)
  Call setName(racerName)
  Call setClass(racingClass)
  Call setTerm(graduatedTerm)
End Sub

Public Sub setName(ByVal racerName As String)
  registeredName_ = racerName
End Sub

Public Sub setClass(ByVal racingClass As RacerClass)
  racingClass_ = racingClass
End Sub

Public Sub setTerm(ByVal graduatedTerm As Integer)
  graduatedTerm_ = graduatedTerm
End Sub

Public Sub showMyself()
  Debug.Print "ハロ~♪ CQ、CQ、私は" & Me.racingClass & "。"
  Debug.Print graduatedTerm_ & "期の" & registeredName & "で~す!"
End Sub

サンプルだから凝らなくてもいいのに、ムダにタテ長になってしまった。いつものことながら申しわけない。

ごく普通のクラスモジュールだが、(1)の

Private Sub Class_Initialize()
  If Not isInstantiated Then _
      Err.Raise Number:=10001, _
                Description:=ERROR_MESSAGE_10001
  isInstantiated = False
End Sub

では、普段ほぼ何の役にも立たないClass_Initializedプロシージャに

If Not isInstantiated Then

という条件式を書いている。isInstantiatedという変数については後述するが、普通にNewでインスタンス化しようとすると、ここでエラーを吐いて弾き返す、という仕組みにした。

最後に

isInstantiated = False

でisInitializedをFalseに戻す。こうしておかないと、次から普通にNewできてしまうw Function作った意味がなくなるので、要注意。

あと、(2)の

Public Sub init(ByVal racerName As String, _
                ByVal racingClass As RacerClass, _
                ByVal graduatedTerm As Integer)
  If isInitialized Then Err.Raise Number:=10002, _
                                  Description:=ERROR_MESSAGE_10002
  Call setRacer(racerName, racingClass, graduatedTerm)
  isInitialized = True
End Sub

が実質的なコンストラクタ。isInitializedフラグを用いることで複数回実行されることを防ぐ。

 
リスト3 標準モジュール

んで、コチラがインスタンス生成用のFunction。

Public Function createSampleRacerObject( _
                  ByVal racerName As String, _
                  ByVal racingClass As RacerClass, _
                  ByVal graduatedTerm As Integer) As SampleRacer
  isInstantiated = True    '……(1)'
  Dim smplRacer As New SampleRacer    '……(2)'
  smplRacer.init racerName, racingClass, graduatedTerm    '……(3)'
  Set createSampleRacerObject = smplRacer    '……(4)'
End Function

シンプルなコードなので説明するまでもないけれど、一応。

まず、(1)の

isInstantiated = True

でisInstantiatedをTrueにしておく。こうすることで、次に(2)でNewしたときにClass_Initializedで弾き返されることを防ぐ。

(2)の

Dim smplRacer As New SampleRacer

インスタンス化し、

(3)の

smplRacer.init racerName, racingClass, graduatedTerm

で引数を渡して初期化。

あとは、(4)の

Set createSampleRacerObject = smplRacer

インスタンスを呼び出し元に返す。

使用実験

次のコードでSampleRacerクラスを使ってみる。

リスト4 標準モジュール
Public Sub testSampleRacerClass()
  Dim sr1 As SampleRacer
  Set sr1 = New SampleRacer    '……(*)'
  Set sr1 = createSampleRacerObject("中野浩一", S1, 35)    '……(1)'
  With sr1
    .showMyself    '……(2)'
    .setRacer "左京源皇", A3, 72    '……(3)'
    .showMyself
    .setRacer "鶴岡篤人", B2, 52    '……(4)'
    .showMyself
  End With
  Dim sr2 As SampleRacer
  Set sr2 = createSampleRacerObject("吉岡稔真", S1, 65)    '……(5)'
  sr2.showMyself
End Sub

まず、このまま実行してみると、一見何の問題もなさそうな(*)の

Set sr1 = New SampleRacer

のところで、

f:id:akashi_keirin:20171216084409j:plain

エラーになる。狙い通り。

実行時は(*)をコメントアウトします。

f:id:akashi_keirin:20171216084419j:plain

(1)の

Set sr1 = createSampleRacerObject("中野浩一", S1, 35)

でcreateSampleRacerObjectに引数を3つ渡してインスタンス化。

(2)の

sr1.showMyself

でshowMyselfメソッドを実行。

(3)からの2行

sr1.setRacer "左京源皇", A3, 72
sr1.showMyself

では、setRacerメソッドでパラメータを書き換えた後、showMyselfメソッドを実行。

(3)からの2行

sr1.setRacer "鶴岡篤人", B2, 52
sr1.showMyself

も(2)と同じ。

(5)からの2行

Set sr2 = createSampleRacerObject("吉岡稔真", S1, 65)
sr2.showMyself

では、別のインスタンスを生成してshowMyselfメソッドを実行。

実行結果

f:id:akashi_keirin:20171216084427j:plain

f:id:akashi_keirin:20171216084436j:plain

f:id:akashi_keirin:20171216084446j:plain

f:id:akashi_keirin:20171216084454j:plain

無事、意図したとおりの結果となった。

おわりに

今にして思えば、別にNewを禁止することはなかったな。

引数が必要なければ普通にNew、引数が必要だったらFunction経由、という風に使い分けたらいいだけだし。

そうすれば、グローバル変数isInstantiatedも必要なくなるなあ。

インターフェイスを使うやり方も含め、もうちょっと研究してみる余地はありそう。