クラスでしあわせプログラミング

クラスってなんだっ!?

次期Visual Basicに乗り遅れないために



初音 玲 HATSUNE, Akira



複数のフォームから共通に使うようなロジック(プログラムコード)はどこに記述すべきなのだろうか。素直に考えれば、標準モジュールにPublicサブプロシージャやPublic関数として記述するだろう。しかし、Visual Basicにはクラスモジュールというものもある。このクラスモジュールにもPublicサブプロシージャやPublic関数を記述できる。この両者の違いや、そもそも「クラス」という名前の意味を考えてみたい。このような基礎的なことを今のうちに理解し、クラスに慣れ親しんでおくことは、これからのVisual Basicプログラマには必要不可欠である。なぜなら、次期Visual Basicでは、このクラス周りも大幅に拡張されることが予告されているからだ。だからこそ、来るべき新Visual Basicのヘルプファイルをすらすら読めるだけの基礎知識を今から身に付けておくべきだろう。

クラスとはユーザー定義型だっ!

 標準モジュールとクラスモジュールの違いは、変数とユーザー定義型の違いに似ている。ユーザー定義型は、好きな要素を織り込んだ独自の変数型を作成できる機能だが、ユーザー定義型にそのまま値を設定することはできないので、ユーザー定義型変数を宣言して、その変数に対して値を設定する(リスト1)。

リスト1:ユーザー定義型変数を宣言し、変数に対して値を設定する
Option Explicit
Private Type typFile
  strISBN    As String * 17
  strTitle   As String * 30
  strDate    As String * 10
  strPrice   As String * 6
  strPage    As String * 6
  strRemark  As String * 11
  strCRLF    As String * 2
End Type
Private Sub subDispSet(ByRef rusrFile As typFile)
'*******************************************************************************
'  プロシージャ名       : subDispSet
'  機能                 : 画面に表示する
'  パラメータ           : rusrFile, (IN), typFile, ファイル名
'  作成日               : 2000/01/21 V1.0L00
'*******************************************************************************
  txtISBN(0).Text = rusrFile.strISBN
  txtISBN(1).Text = Trim$(rusrFile.strTitle)
  txtISBN(2).Text = CCur(rusrFile.strPrice)
  txtISBN(3).Text = CLng(rusrFile.strPage)
  txtISBN(4).Text = Format$(CDate(rusrFile.strDate), "YYYY/MM/DD")
  txtISBN(5).Text = Trim$(rusrFile.strRemark)
End Sub

 同様にクラスモジュールも、その中の関数やサブプロシージャをそのまま使うことはできない。クラスモジュール変数を宣言して、その変数を通して関数やサブプロシージャを使うことになる(リスト2)。

リスト2-1:クラスモジュール変数の宣言
Private Sub cmdObjUse_Click()
  Dim objSamp201  As clsSamp201
  lblObjUse.Caption = objSamp201.intCountDisp
End Sub

リスト2-2:クラスモジュール(clsSamp201)
Public Function intCountDisp() As Integer
  mintCount = mintCount + 1
  intCountDisp = mintCount
End Function

 この場合、クラスモジュールの中のPublicサブプロシージャやPublic関数を標準モジュールに定義したときと同様の方法で呼び出そうとしてもエラーになる。どのようなエラーになるか、実際にクラスモジュールを定義して呼び出すサンプルで確認すると理解が進むだろう。

クラスモジュールの使い方
(サンプル1:Samp0701)

 標準モジュールとクラスモジュールの両方に同じロジックを記述する(リスト3)。

リスト3:実験用ロジック
Option Explicit
Private mintCount   As Integer
Public Function intCountDisp() As Integer
  mintCount = mintCount + 1
  intCountDisp = mintCount
End Function

 このロジックは、モジュール共通変数としてmintCountが用意されていることからわかるように、モジュールが生成されてからPublic関数が呼び出された回数を返すものだ。
 このロジックを図1のような画面のボタンに対応したイベントプロシージャから、リスト4のように呼び出す。

図1:サンプル1の実行画面
図1

リスト4:ボタンごとのイベントプロシージャ
Option Explicit
Private Sub cmdStdMod_Click()
' [標準モジュールを使う]ボタン
  lblStdMod.Caption = basSamp0701.intCountDisp
End Sub
Private Sub cmdClsMod_Click()
' [クラスモジュールを使う]ボタン
  lblClsMod.Caption = clsSamp0701.intCountDisp
End Sub
Private Sub cmdObjUse_Click()
' [オブジェクトを使う]ボタン
' @クラスオブジェクト変数
  Dim objSamp  As clsSamp0701
' Aクラスオブジェクトの生成
  Set objSamp = New clsSamp0701
' B実行結果の設定
  lblObjUse.Caption = objSamp.intCountDisp
' Cクラスオブジェクトの解放
  Set objSamp = Nothing
End Sub

標準モジュールを呼び出す

[標準モジュールを使う]ボタンを左クリックしてcmdStdMod_Clickイベントプロシージャを呼び出すと、その回数がコマンドボタンコントロールの隣に正しく表示される。これは、標準モジュールが実行ファイルの一部としてプログラム起動時にメモリ上に展開され、その状態のまま素直に使われていることを意味している。当然、[mintCount]モジュール共通変数もプログラム起動時に初期化される。

クラスモジュールを呼び出す

[クラスモジュールを使う]ボタンを左クリックしてcmdClsMod_Clickイベントプロシージャを呼び出そうとしても、図2のようなエラーが表示され、クラスモジュール内のPublic関数(もちろんPublicサブプロシージャも含む)は直接呼び出せない。

図2:[クラスモジュールを使う]ボタンを左クリック
図2

クラスオブジェクトを呼び出す

 クラスモジュールの使い方としては、
  1. 該当クラスモジュール型の変数を宣言
    Asの後にはクラスモジュールのオブジェクト名を指定する
  2. クラスオブジェクトの生成
    Setステートメントの右辺にNew付きでクラスモジュールのオブジェクト名を指定し、該当クラスモジュールのオブジェクトを生成する。このときはじめてクラスモジュールの作業領域がメモリ上に確保される。その作業領域を識別するのが、Setステートメントの左辺のオブジェクト変数だ
  3. 変数を介してPublic関数を使う
    オブジェクト変数を介して、つまり、メモリ上のクラスモジュールの管理情報を指定して、Public関数を使う
  4. クラスオブジェクトの解放
    使い終わったら、メモリ上から管理情報を削除する。このように明示的に解放したほうがよいのは、プログラムが理解しやすくなるという理由だけではない。もし、明示的に解放しないと、今回のオブジェクト変数は、プロシージャ内のローカル変数なので、プロシージャから抜けたときに変数が初期化される。仕様的には初期化タイミングで、その変数で管理しているメモリ上のクラスモジュール管理情報も解放されるはずだが、Visual Basicのバージョンやサービスパックの状態によっては、うまく解放されないときもある。よって、そういった危うい状態を回避するためには、言語の内部的な動作に頼らずに明示的に解放することをお勧めする。もちろん、これはクラスモジュールに限らず、AcriveX DLLやActiveX EXEなどオブジェクト変数を使うときの一般的な法則だ。
という流れになる。このようにロジックを組んでおけば、[オブジェクトを使う]ボタンを左クリックしてcmdObjUse_Clickイベントプロシージャを呼び出すと、コマンドボタンコントロールの隣に回数が表示される。しかし、この回数は何回クリックしても「1」としか表示されず、標準モジュールのようにはカウントアップされない。これは、クラスモジュールのモジュール共通変数の有効期間がクラスモジュールの生成から消去の間に限られているからである。

クラスモジュールの有効期間を延ばす
(サンプル2:Samp0702)

 サンプル1では、クラスモジュールの有効期間がcmdObjUse_Clickイベントプロシージャの中だけだったので、コマンドボタンコントロールに対する複数回の左クリックにまたがった情報を保持することができなかった。そこで、有効期間を標準モジュールと同じように実行ファイルの起動から終了までに拡張する。
 そのためには、リスト5のように、

リスト5:有効期間の延長
Option Explicit
'@クラスオブジェクト変数
Private mobjSamp  As clsSamp0702
Private Sub Form_Initialize()
'Aクラスオブジェクトの生成
  Set mobjSamp = New clsSamp0702
End Sub
Private Sub cmdObjUse_Click()
'[オブジェクを使う]ボタン
' B実行結果の設定
  lblObjUse.Caption = mobjSamp.intCountDisp
End Sub
Private Sub Form_Terminate()
'Cクラスオブジェクトの解放
  Set mobjSamp = Nothing
End Sub
  1. 該当クラスモジュール型の変数を宣言
    モジュール共通変数としてオブジェクト変数を宣言する
  2. クラスオブジェクトの生成
    スタートアップフォームである[Samp0702]フォームのForm_Initializeイベントプロシージャで、Setステートメントの右辺にNew付きでクラスモジュールのオブジェクト名を指定して、該当クラスモジュールを生成する。このタイミングでクラスオブジェクトを生成することで、標準モジュールと同じ有効範囲を得ることになる
  3. 変数を介してPublic関数を使う
    オブジェクト変数を介して、つまり、メモリ上のクラスモジュールの管理情報を指定して、Public関数を使う
  4. クラスオブジェクトの解放
    使い終わったら、メモリ上から管理情報を削除するが、そのタイミングは、スタートアップフォームのForm_Terminateイベントプロシージャになる
という流れでプログラミングする。このようにすれば、図3のように[オブジェクト使う]ボタンを左クリックするたびに数字がカウントアップされてゆく。

図3:サンプル2の実行画面
図3

変数宣言とクラスモジュール生成

 オブジェクト変数の宣言とクラスモジュールの生成を別々のタイミングで行なう方法を紹介してきたが、Visual Basicの文法を紐解いてみると、

Dim objClsMod As New clsSamp0702
のようにオブジェクト変数の宣言とクラスモジュールの生成を同時に行なえるとある。しかし、この方法ではクラスモジュールを明示的に解放できない。よって、オブジェクト変数の有効範囲外に実行ステップが移った時のオブジェクトの自動解放に期待するしかないのだが、このときもVisual Basicのバージョンやサービスパックの状態によっては、うまく解放されないときもある。よって、そういった危うい状態を回避するためには、変数の宣言とクラスオブジェクトの生成は別々に行なうのがよい。

クラスモジュールvs標準モジュール
(サンプル3:Samp0703)

 モジュール共通変数を使ってクラスオブジェクトの生成と解放タイミングを工夫すれば、クラスモジュールと標準モジュールは同じように扱えることはわかった。そうすると、どちらを使ったほうがよいかという新たな疑問が湧いてくるだろう。
 明示的に有効範囲を指定できるクラスモジュールのほうが標準モジュールに比べて拡張性がありそうなので、標準モジュールを使わずにクラスモジュールのみを使うという方針もあるかもしれない。
 しかし、拡張性と性能が共に高いということはなかなかないのがソフトウェアの世界だ。サンプルを作成して、標準モジュールのロジックを使うときとクラスモジュールのロジックを使うときの処理時間の差を測定する。なお、クラスモジュールの生成時間は除外している。

処理速度の測定方法

 処理速度を測定するためには、測定範囲の前後で時刻を取得してその差を求めるのが一般的だ。問題はどのように時刻を測定するかだろう。Visual Basicの標準関数であるTimeでは1秒単位でしか時刻を取得できない。そこで、timeGetTime APIを使って100分の1秒単位で時刻を測定する(リスト6)。
リスト6:IDEと実行ファイルとの速度を測定する
Option Explicit
Private Declare Function timeGetTime Lib "WINMM" () As Long
' クラスオブジェクト変数
Private mobjSamp  As clsSamp0703
Private Sub Form_Initialize()
' クラスオブジェクトの生成
  Set mobjSamp = New clsSamp0703
End Sub
Private Sub Form_Terminate()
' クラスオブジェクトの解放
  Set mobjSamp = Nothing
End Sub
Private Sub cmdStdMod_Click()
'[標準モジュールを使う]ボタン
  Dim lngTime     As Long
  Dim iintLoop    As Integer
  Dim iintCnt     As Integer
  Dim intRet      As Integer
  lngTime = timeGetTime
  For iintCnt = 1 To 10
    Call basSamp0703.subCountClear
    For iintLoop = 1 To 10000
      intRet = basSamp0703.intCountDisp
    Next
  Next
  lblStdMod.Caption = timeGetTime - lngTime & " (ms)"
End Sub
Private Sub cmdObjUse_Click()
'[オブジェクトを使う]ボタン
    Dim lngTime     As Long
    Dim iintLoop    As Integer
    Dim iintCnt     As Integer
    Dim intRet      As Integer
    lngTime = timeGetTime
    For iintCnt = 1 To 10
        Call mobjSamp.subCountClear
        For iintLoop = 1 To 10000
            intRet = mobjSamp.intCountDisp
        Next
    Next
    lblObjUse.Caption = timeGetTime - lngTime & " (ms)"
End Sub
' 標準モジュールとクラスモジュール
Option Explicit
Private mintCount   As Integer
Public Function intCountDisp() As Integer
  mintCount = mintCount + 1
  intCountDisp = mintCount
End Function
Public Sub subCountClear()
  mintCount = 0
End Sub

 さて、サンプルを実行して処理時間を測定するときに注意しなければならない点がある。それは、Visual Basic 5.0以降では、ネイティブコンパイルの機能があるため、Visual Basicの開発環境(IDE)上で実行したときと、コンパイルしてから実行ファイルを起動したときでは、処理速度に違いが生じることだ。具体的には、IDE上ではPコードと呼ばれる中間コードをIDEが逐次解釈して動作するのに対して、ネイティブコンパイル後はそのような逐次解釈部分が必要なくなる分だけ高速に動作する。

IDE上での確認

 まずは、IDE上で実行してみると、図4のような結果になり、標準モジュールのほうが1.7倍ほど高速に処理できることが分かる。もちろん、実際の処理時間や処理速度比は、動作させているマシンのCPUの種類や動作周波数、メモリ容量やOSの種類などにより変化するが、クラスモジュールよりも標準モジュール内のロジックのほうが高速に呼び出せるということは変わらない。

図4:IDE上での確認
図4

コンパイル後の実行ファイルでの確認

 ネイティブコンパイル後の実行ファイルを実行してみると、図5のような結果になり、標準モジュールのほうが6.8倍も速くなる。

図5:コンパイル後の実行
図5

 以上のことを踏まえると、クラスモジュールと標準モジュールの世界でも“拡張性と性能の一挙両得は難しい”という法則は崩れていないということだ。

クラスとは配列だっ!
(サンプル4:Samp0704)

 クラスモジュールを標準モジュールのように使うのは、処理速度の観点からだけ評価してもあまりよい方法ではない。では、クラスモジュールはどういった局面で使えばよいのだろうか。
 その答えは、変数と配列の関係を考察すると見えてくる。配列を使えば同じ変数型の情報を大量に管理できる。それと同じようにクラスモジュールも同じロジックを大量に管理できる。それはどういったことだろう。
 今までのサンプルにもあったように、Setステートメントの右辺にNew付きでクラスモジュールのオブジェクト名を指定して、オブジェクト変数とクラスオブジェクトを結びつけて使う。このSetステートメントが実行されるたびに新たなクラスオブジェクトの情報領域が作成され、それぞれのクラスオブジェクトは別々に管理される。
 つまり、リスト7のようにプログラミングすれば、クラスモジュールのモジュール共通変数も別々の値を割り当てることが可能となる(図6)。このようなことを標準モジュールで実現するには、標準モジュール側と呼び出し側で配列の添字のようなものを管理して、それをPublic関数やPublicサブプロシージャ利用時に受け渡しながら動作するような煩雑なロジックが必要になり、障害発生時の対応やプログラム完成までに時間がかかってしまうだろう。こういった方法を使わなければ実現できないようなときにクラスモジュールを使うというのがひとつの指針だ。

図6:サンプル4の実行画面
図6

リスト7:配列的クラスモジュール利用法
Option Explicit
Private Declare Function timeGetTime Lib "WINMM" () As Long
' クラスオブジェクト変数
Private maobjSamp(0 To 4)  As clsSamp0704
Private Sub Form_Initialize()
' クラスオブジェクトの生成
  Set maobjSamp(0) = New clsSamp0704
  Set maobjSamp(1) = New clsSamp0704
  Set maobjSamp(2) = New clsSamp0704
  Set maobjSamp(3) = New clsSamp0704
  Set maobjSamp(4) = New clsSamp0704
End Sub
Private Sub Form_Terminate()
' クラスオブジェクトの解放
  Set maobjSamp(0) = Nothing
  Set maobjSamp(1) = Nothing
  Set maobjSamp(2) = Nothing
  Set maobjSamp(3) = Nothing
  Set maobjSamp(4) = Nothing
End Sub
Private Sub cmdObjUse_Click(Index As Integer)
' [オブジェクを使う]ボタン
  lblObjUse(Index).Caption = maobjSamp(Index).intCountDisp
End Sub

VB6以降のクラスモジュールの拡張機能
(データソースクラスモジュールの作成)

 Visual Basic 6.0以降では、クラスモジュールにさらなるパワーが与えられている。そのひとつが、クラスモジュールをデータソースとして使う機能だ。この機能を使えば、データコントロールでは接続できないような情報にクラスモジュールからアクセスして、その結果を使うときにはデータコントロールのように使えるという簡便性を実現できる。たとえばSMTP/POP3に対応したクラスモジュールを作成してそれをデータソースクラスとすれば、mdbファイルにアクセスするような気軽さでeメールをやり取りすることも可能だろう。
 クラスモジュールをデータソースとするためには、
  1. クラスモジュールのDataSourceBehaviorプロパティにvbDataSourceを設定
  2. BindingCollectionオブジェクトのDataSourceプロパティにクラスモジュールを指定
  3. BindingCollectionオブジェクトにAddメソッドを使って、クラスモジュールをデータソースとして使いたいデータコンシューマを追加
の手順になる。
 あとは、Class_GetDataMemberイベントサブプロシージャに初期化ロジックやMoveNextなどの操作系メソッドを作成すればよい。

VB6以降のクラスモジュールの拡張機能
(OLE DBプロバイダの作成)

 Visual Basic 6.0以降ではさらにクラスモジュールをOLE DBプロバイダとすることも可能だ。しかし、純然たるOLE DBプロバイダをVisual Basicのみで作成するのは困難なので、OLE DB Simple Providerを利用して、追加的な機能を追加することになる。このように既存の機能に独自機能を追加することをクラスの世界では“インプリメント”という。
 この半オーダーメードのOLE DBプロバイダを作るためには、
  1. 参照設定で
    ・Microsoft Data Source Interfaces
    ・Microsoft OLE DB Simple 1.5 Provider Library
    ・OLE DB Errors Type Library
    の3つを参照設定
  2. クラスモジュールのDeclarationsに、

    Implements OLEDBSimpleProvider
    
    を記述し、OLE DB Simple Providerをインプリメントする。これによって、クラスモジュール内にOLE DB Simple Providerオブジェクトが追加され、インプリメントされたイベントがイベントサブプロシージャとして追加される
  3. 追加されたプロシージャに処理を記述する

 この方法では、BindingCollectionオブジェクトを使ってクラスモジュールをデータソースとして定義したときよりも簡単に扱える利点がある。

最後に

 クラスモジュールとは、何かを簡単にまとめると、ロジックやデータを隠蔽して使いやすくする技術と言える。また、インプリメントを使って、既存のものに新たな機能やデータを追加する拡張性も兼ね備えている。次版のVisual Basicでは、この拡張性をさらに汎用的にし、一般に言われているオブジェクト指向プログラミングで使われるさまざまな概念を追加する予定らしい。これは、かなり難しい概念が伴う機能拡張が行なわれるとも言えるだろう。Visual Basicがその難しさをどうやさしくしてくれるのか、今から期待したいと思う。


VB Magazine ライブラリ| Visual Basic WorkGroup
int21 ホームページ| PCDN ホームページ

PCDN
Copyright (c) 1998 int21 CorporationAll Rights Reserved.
For questions or comments, please send mail to: pcdn@int21.co.jp