|
|
連載
改訂版 プロフェッショナルVB.NETプログラミング
Chapter 05 オブジェクト関連の変化
株式会社ピーデー
川俣 晶
2004/04/15 |
|
ここでは、主にオブジェクトに関係するVB 6とVB.NETの相違点についてまとめてある。「VB.NETになって、Visual Basicもようやくオブジェクト指向になるらしいが、VBプログラマーにオブジェクト指向が分かるのだろうか?」と心配する人も少なくないが、実際にはVB 6の段階で、すでにクラス・モジュールを作成することができ、PrivateやPublicといったキーワードでアクセス範囲を制限する機能も用意されていた。オブジェクト指向を実現する上で、足りない主要な機能は継承ぐらいであったといえる。そのため、ここではVB 6にすでにあって、それが変化した機能のみを解説している。継承に関する機能は、継承とポリモーフィズムで解説している。
リスト5-1はVB 6でクラスを作成して、そのインスタンスを利用する簡単なサンプル・プログラムである。まずは、クラスそのものの定義を示す。「Class1」という名前で、クラスのプロパティ設定は作成したデフォルトのままである。
1: Private Sub Class_Initialize()
2: Debug.Print "Class_Initialize called"
3: End Sub
4:
5: Private Sub Class_Terminate()
6: Debug.Print "Class_Terminate called"
7: End Sub
8:
9: Public Sub Test()
10: Debug.Print "Test called"
11: End Sub
|
|
リスト5-1 クラス「Class1」を定義したプログラム
|
さて、このクラスのインスタンスを生成して利用するソース・コードとして、リスト5-2のようなものを書いてみた。
1: Private Sub Form_Load()
2: Dim obj As New Class1
3: Debug.Print "Dim obj As New Class1"
4: obj.Test
5: End Sub
|
|
リスト5-2 リスト5-1のクラスを利用するプログラム
|
これを実行すると以下のようになる。
1: Dim obj As New Class1
2: Class_Initialize called
3: Test called
4: Class_Terminate called
|
|
リスト5-3 リスト5-2の実行結果
|
このサンプル・ソースの動作を説明しておこう。VB 6では、As Newキーワードで宣言されたオブジェクト変数は、インスタンスが自動的に作成されて変数に格納される。しかし、生成されるタイミングは、宣言された行の位置ではなく、実際にそれが使用されたときまで遅延される。そのため、このサンプル・プログラムでは、4行目のobj.Testのコードが最初にこの変数を利用するタイミングになるので、これに先だってインスタンスが生成される。その結果、3行目の“Debug.Print "Dim obj As New Class1"”よりもあとでインスタンスが生成されることになるので、出力結果は、
Dim obj As New Class1
が先で、
Class_Initialize called
があとになる。
さて、これに相当するコードをVB.NETで記述してみよう。まずはクラス・モジュールから。
1: Public Class Class1
2: Public Sub New()
3: Trace.WriteLine("Class1.New called")
4: End Sub
5:
6: Protected Overrides Sub Finalize()
7: MyBase.Finalize()
8: Trace.WriteLine("Class1.Finalize called")
9: End Sub
10:
11: Public Sub Test()
12: Trace.WriteLine("Test called")
13: End Sub
14: End Class
|
|
リスト5-4 クラスを定義しているVB.NETのプログラム
|
これを見ると、VB 6の“Class_Initialize”がNewに、“Class_Terminate”がFinalizeにそれぞれ変わっていることが分かるだろう。なお、6行目のOverridesキーワードについては、Overridesキーワードを使って継承時にメソッドの動作を入れ替えるを、Protectedキーワードについては、Protectedアクセスを参照してほしい。また、MyBase.Finalize()は基底クラスのデストラクタ(Finalizeメソッド)を呼び出している。これらは、継承されたクラスを解放するときに正しく終了処理を行うために必要なものである(継承については、継承とポリモーフィズムを参照)。
さて、このクラスを呼び出すソース・コードはリスト5-5のようになる。
1: Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
2: Dim obj As New Class1()
3: Trace.WriteLine("Dim obj As New Class1()")
4: obj.Test()
5: End Sub
|
|
リスト5-5 リスト5-4のクラスを利用するプログラム
|
こちらの方は、2行目の最後に括弧が増えている以外は、ほとんど同じといえるだろう。括弧が付くようになったのは、次で説明するコンストラクタへの引数のためである。
これを実行すると以下のようになる。
1: Class1.New called
2: Dim obj As New Class1()
3: Test called
|
|
リスト5-6 リスト5-5の実行結果
|
見てのとおり、クラス(リスト5-4)の8行目のコードである“Class1.Finalize called”のメッセージが表示されていない。このメッセージはだいぶあとで見ることができる。タイミングは決まっていないが、筆者が試したときは、ウィンドウを閉じる際に、やっと、以下のメッセージが出力された。
4: Class1.Finalize called
|
|
リスト5-7 リスト5-5の実行結果の続き
|
相違の原因は、具体的に2つの理由に分けられる。第1の理由は、As Newキーワードを付けたオブジェクト変数でインスタンスを生成するとき、生成するタイミングが遅延されなくなったことにある。このため、表示結果の1行目と2行目が入れ替わってしまっている。これは、タイミングが変わるというだけでなく、VB 6では生成されなかったインスタンスが、VB.NETでは生成される可能性があることに注意しよう。つまり、VB 6では、As Newキーワードを付けたオブジェクト変数を宣言しても、そのオブジェクト変数が使われなければインスタンスは生成されないまま終わったが、VB.NETでは必ず生成されてしまうのである。インスタンスの生成や消滅時に何かの動作を行っているクラスなら、プログラムの動作そのものが変わる可能性があり得る。
第2の理由は、インスタンスを消滅させるメカニズムが変化したことにある。VB 6では、あるインスタンスへの参照がなくなると、その時点でインスタンスは消滅させられる。しかし、VB.NETでは、.NET Frameworkのほかの言語と同様に、不要となったインスタンスはガベージ・コレクタと呼ばれる自動ゴミ掃除機能が回収する。そのため、VB.NETではデストラクタが実行されるタイミングはガベージ・コレクタ次第であり、プログラマーからは予測できない。つまり、使い終わってから、実際に回収されるまでにタイムラグがあり、その長さがどれぐらいになるかは、特に決まった規則がないことを意味する。
このようなタイミングの違いは、ときとして、プログラムの正常な動作を阻害するので、VB 6のソースをVB.NET用に書き換えるときは、挙動の違いを正しく把握するようにしておこう。
明示的に資源を解放するタイミングをコントロールする場合は、例外処理のFinallyブロックやDisposeパターンを使用する。
VB 6では、同じ名前のメソッドを同じコンテキスト内で複数記述するとエラーが起きた。リスト5-8はVB 6でエラーになるサンプル・プログラムである。引数の型や数が異なるTwoTimesというメソッドが3つ定義されているが、同じフォーム・モジュール中に同じ名前のメソッドが複数あるとエラーになる。
1: Private Sub TwoTimes(ByVal n As Integer)
2: Debug.Print n * 2
3: End Sub
4:
5: Private Sub TwoTimes(ByVal n1 As Integer, ByVal n2 As Integer)
6: Debug.Print (n1 + n2) * 2
7: End Sub
8:
9: Private Sub TwoTimes(ByVal s As String)
10: Debug.Print s & s
11: End Sub
12:
13: Private Sub Form_Load()
14: TwoTimes 123
15: TwoTimes 123, 456
16: TwoTimes "123"
17: End Sub
|
|
リスト5-8 引数の型や数が異なる同名のメソッドを定義したプログラム(コンパイルエラーとなる)
|
しかし、これをそのままVB.NET用に書き換えると、これは動作する(リスト5-9)。
1: Private Sub TwoTimes(ByVal n As Integer)
2: Trace.WriteLine(n * 2)
3: End Sub
4:
5: Private Sub TwoTimes(ByVal n1 As Integer, ByVal n2 As Integer)
6: Trace.WriteLine((n1 + n2) * 2)
7: End Sub
8:
9: Private Sub TwoTimes(ByVal s As String)
10: Trace.WriteLine(s & s)
11: End Sub
12:
13: Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
14: TwoTimes(123)
15: TwoTimes(123, 456)
16: TwoTimes("123")
17: End Sub
|
|
リスト5-9 リスト5-8をVB.NET用に書き換えたプログラム
|
これを実行すると以下のようになる。
|
リスト5-10 リスト5-9の実行結果
|
このように、引数の型や種類が異なる同じ名前のメソッドを宣言することをオーバーロードという。VB.NETでは、オーバーロードが可能となっている。VB 6ではデータは何でもとりあえずVariant型で受けて処理するという方法が使えたので、オーバーロードの必要性は薄かったかもしれない。だが、VB.NETでデータ型を厳密に扱うプログラミング(Option Strictを行うようになると、必須となる機能といえる。同じ名前のメソッドでも、データ型を細かく分けて別々のメソッドとして処理するプログラミングをするようになれば、避けては通れない機能である。
なお、以下の要素が異なっていても、異なるメソッドとは認識されない。
- 型メンバの修飾子(SharedやPrivateなど)
- パラメータの修飾子(ByValやByRefなど)
-
パラメータの名前
- メソッドの戻り値の型
- プロパティの要素型
オーバーロードは、メソッドだけでなく、コンストラクタやプロパティでも使用できる。
インスタンス内部の初期値を、インスタンスを生成する側から設定するケースを考えてみよう。例えば、名前と年齢を保持する2つの変数を持つクラス「Class1」のインスタンスを生成して利用する場合は、この2つの変数に値を設定する必要がある。VB 6では、インスタンスを生成してから、必要な値を設定するので、個別の値を設定する手段をクラスの中に用意しなければならない。リスト5-11は、プロパティ経由で値を設定する機能を含めて作成してみたクラスである。
1: Private name0 As String
2: Private age0 As Integer
3:
4: Public Property Let name(ByVal s As String)
5: name0 = s
6: End Property
7:
8: Public Property Let age(ByVal a As Integer)
9: age0 = a
10: End Property
11:
12: Public Sub OutputData()
13: Debug.Print name0
14: Debug.Print age0
15: End Sub
|
|
リスト5-11 プロパティ経由でクラス内の変数を設定できるプログラム
|
これを利用する側のコードは、以下のような感じになる。
1: Private Sub Form_Load()
2: Dim taro As New Class1
3: taro.name = "Taro"
4: taro.age = 17
5: taro.OutputData
6: End Sub
|
|
リスト5-12 リスト5-11を利用するプログラム
|
これを実行すると以下のようになる。
|
リスト5-13 リスト5-12の実行結果
|
これをVB.NET用として書き換えてみよう。クラス内の変数への値の設定は1回限りとすれば、コンストラクタの引数を経由して値を入れてしまうことができる。そのようなクラスを記述してみたのが、リスト5-14である。
1: Public Class Class1
2: Private name As String
3: Private age As Integer
4:
5: Public Sub OutputData()
6: Trace.WriteLine(name)
7: Trace.WriteLine(age)
8: End Sub
9:
10: Public Sub New(ByVal n As String, ByVal a As Integer)
11: name = n
12: age = a
13: End Sub
14: End Class
|
|
リスト5-14 コンストラクタの引数によりクラス内の変数を設定するVB.NETのプログラム
|
このソースのNewメソッドに引数が付いていることが分かるだろう。この引数は以下のように記述することで利用できる。
1: Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
2: Dim taro As New Class1("Taro", 17)
3: taro.OutputData()
4: End Sub
|
|
リスト5-15 リスト5-14を利用するプログラム
|
これを実行すると以下のようになる。
|
リスト5-16 リスト5-15の実行結果
|
このソース(リスト5-15)の2行目のように、As Newキーワードを使用した宣言のときに、クラス名の後に引数を記述することができる。
コンストラクタに引数を渡すのは、以下のような書式でも可能である。
1: Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
2: Dim taro As Class1
3: taro = New Class1("Taro", 17)
4: taro.OutputData()
5: End Sub
|
|
リスト5-17 コンストラクタ呼び出しを別の書式に変えたプログラム
|