Visual Basic 中級講座 |
Visual Basic 中学校 > 中級講座 >
継承にまつわる各種機能および、継承を強制したり継承を不能にする制御について説明します。
概要 ・派生クラスではMyBaseを使用することで基底クラスにアクセスできる。 ・Meは常に現在実行中のインスタンスを指す。慣れないうちはMeやMyClassが何を指しているか注意する必要がある。 ・派生クラスでのコンストラクタの定義には少し特殊な仕様がある。 ・基底クラスのメソッドやプロパティと名前は同じだが無関係なメソッドやプロパティを作成するにはShadowsを使用する。 ・MustInherit・MustOverrideは継承を強制し、そのクラスのインスタンスを作成できなくする。→抽象クラス。 ・NotInheritable・NotOverridableは継承を不可能にする。 |
前回、継承の基本的なキーワードの使用方法を説明しました。
今回はまず基本に戻ってクラス自身を表すキーワードMeについて考えてみます。また、Meとよく似ているMyClassとMyBaseについても説明します。
ここで説明する内容を正しく理解していないと、あとあとポリモーフィズムの一環として継承を使用することが困難になるかもしれませんし、何よりも意図したとおりにMeが動作してくれないと言うことにもなりかねません。
まず、継承とは直接関係ありませんが「Me」は正確にいえばクラス自身ではなく現在のインスタンスを指しているキーワードです。ですから共有メンバの内部ではMeを使用することはできません。共有メンバでは使用できないと言う性質はMyBaseとこれから説明するMyClassにも当てはまりますし、キーワードの意味から考えると当然のことです。
以下では「Me」の性質にてついていろいろと説明していますが、Meが省略可能である点にも注意してください。Meが省略されている部分にも以下で説明するMeの性質は当てはまります。
次の小さなManmalクラスとApeクラスを例に説明します。
Public Class
Manmal Public Overridable ReadOnly Property JanapeseName() As String Get Return "哺乳類" End Get End Property |
Public Overridable
Sub Say() MsgBox(Me.JanapeseName) End Sub End Class |
Public Class Ape Inherits Manmal End Class |
■リスト1
Manmalクラスのインスタンスを作成してSayメソッドを呼び出すと「哺乳類」と表示されます。これは問題ないでしょう。
ではApeクラスのインスタンスを作成してSayメソッドを呼び出すとどうなるでしょうか?
ポイントはSayメソッドがMe.JapaneseNameを表示するようになっていることです。ApeクラスではJapaneseNameプロパティもSayメソッドもオーバーロードしていません。この場合、Me.JapaneseNameはどう解釈されるでしょうか?
Dim a As
New Ape a.Say() |
■リスト2
単純な話ではありますが、実際に試してみてください。答えは「哺乳類」です。
■画像1
では、Apeクラスでの実装を次のように変更したときのことを考えます。
Public Class
Ape Inherits Manmal Public Overrides ReadOnly Property JanapeseName() As String Get Return "類人猿" End Get End Property End Class |
■リスト3
この例は非常に注意を要します。SayメソッドはManmalクラス内で実装されているので、Me.JapaneseNameの部分の「Me」とはManmalクラスを指すように思ってしまうかもしれませんが、この場合はApeクラスのインスタンスを指すことになります。
ですから、これでApeクラスのSayメソッドを呼び出すと「類人猿」と表示されます。
これは「Me」が現在のインスタンスを指しているためと考えると理解しやすいです。Sayメソッドは確かにManmalクラス内で実装されていますが、そのコードを実行しているインスタンスはまぎれもなくApeクラスのインスタンスです。ですからMeはApeクラスの方を指すことになると考えるわけです。
この「Me」の性質は単に注意が必要なだけではなく、大きな可能性を秘めておりオブジェクト指向を活用する上で非常に重要なものになります。詳しくは今後のポリモーフィズムの説明の中で取り上げていく予定です。
■画像2
キーワードMyClassは常に現在のメンバが実装されているクラスを表します。今度はManmalクラスのMeの部分をMyClassに置き換えて実行してみてください。コードの全体は次の通りです。
Public Class
Manmal Public Overridable ReadOnly Property JanapeseName() As String Get Return "哺乳類" End Get End Property |
Public Overridable
Sub Say() MsgBox(MyClass.JanapeseName) End Sub End Class |
Public Class Ape Inherits Manmal Public Overridable ReadOnly Property JanapeseName() As String Get Return "類人猿" End Get End Property End Class |
■リスト4
この場合SayメソッドがManmalクラスで実装されているので、MyClass.JapaneseNameは常にManmalクラス内で実装されているJapaneseNameプロパティを指すことになります。ですからApeクラスのSayメソッドを呼び出しても「哺乳類」と表示されます。
MeとMyClassはなれない方にはかなり紛らわしいものと映るでしょう。参考のために言っておくとMyClassを使用することはあまりありません。 どちらを使用すべきか悩んだらMeの方が適切である場合が圧倒的に多いです。
■画像3
MyBaseは常に基底クラスを指します。こちらはすでに登場していますし、使い方に悩むことはほとんどないでしょう。
さて、基底クラスの機能を変更してしまうオーバーライドは簡単に利用できる上に非常に便利な機能ですが、元となっているメンバの名前や引数の型・順序、戻り値の型、それに適用範囲を変更することはできません。
もし、基底クラスのメンバと名前は同じにしたいけれども引数や戻り値や適用範囲を変更したいと言う場合にはシャドウイングを行います。
もう一度先程のManmalクラスを利用して説明します。
Public Class
Manmal Public Overridable ReadOnly Property JanapeseName() As String Get Return "哺乳類" End Get End Property |
Public Overridable
Sub Say() MsgBox(MyClass.JanapeseName) End Sub End Class |
■リスト5
このManmalクラスを継承してApeクラスを作成するときに、JapaneseNameメソッドに引数を追加してオーバーライドすることはできません。
引数を追加するとどのようなことになるか、実際に試してみます。
Public Class
Ape Inherits Manmal Public ReadOnly Property JanapeseName(ByVal IsHiragana As Boolean) As String Get If IsHiragana Then Return "るいじんえん" Else Return "類人猿" End If End Get End Property End Class |
■リスト6
まず、Overridesキーワードを付けることはできなくなります。つけるとビルドエラーになります。Overridesキーワードを取り除くとビルドして実行できるようになります。ただし、Overridesキーワードを取り除いた場合基底クラスのJapaneseNameプロパティと派生クラスのJapaneseNameプロパティはたまたま名前が同じなだけの別のプロパティとして認識されます。この構造を「シャドウ」または「シャドウイング」と呼びます。「派生クラスのJapaneseNameプロパティは基底クラスのプロパティをシャドウしている」などと表現します。
この状態でApeクラスのSayメソッドを呼び出してみましょう。
Dim a As
New Ape a.Say() |
■リスト7
「哺乳類」と表示されます。ApeクラスのJapaneseNameプロパティはまるっきり無視です。
前に紹介した例では、ApeクラスのSayメソッドはキーワードMeを使用しているので、派生クラスでオーバーライドされているプロパティが呼び出されると言う趣旨のことを書きました。それはどの通りなのですが今回の例ではJapaneseNameプロパティは名前は同じですがオーバーライドされていないので呼び出されなくなります。
■画像4
この現象は話が無駄に複雑になるので、できるだけ基底クラスのメンバをシャドウするようなことは止めましょう。無理に同じ名前のメソッドやプロパティを作成しないで名前を変えるかせめてオーバーロードしましょう(オーバーライドとオーバーロードは名前が似ていますが別物です)。
さらに、それでもどうしても同じ名前のメソッドやプロパティを作りたくなったら明確に区別するためにキーワードShadowsをつけるようにしましょう。
キーワードShadowsには特別な機能はありませんが、ソースコードを見たときに基底クラスのメンバをシャドウしていることがはっきりして少しだけわかりやすくなります。
Public Class
Ape Inherits Manmal Public Shadows ReadOnly Property JanapeseName(ByVal IsHiragana As Boolean) As String Get If IsHiragana Then Return "るいじんえん" Else Return "類人猿" End If End Get End Property End Class |
■リスト8
ところで、Overridesと間違ってOverridableと書いてしまった場合もシャドウイングが行われます。メンバの名前のところに緑の波線がでて警告が表示されるので気がつくとは思いますがご注意ください。
要するにOverrides抜きで基底クラスのメンバと同じ名前のメンバを定義するとすべてシャドウになるのです。
ここで説明したのは「継承によるシャドウ」です。この他に適用範囲によってもシャドウが行われる場合があります。話題がそれますが少しだけ紹介しておきます。
Dim Age As
Integer Public Sub New(ByVal Age As Integer) Me.Age = Age End Sub |
■リスト9
このコードではコンストラクタの内部で単にAgeと記述した場合には引数のAgeを意味することになり、クラスレベルの変数Ageにはアクセスできません。これは適用範囲内に同じ名前のものが存在する場合にはより適用範囲の狭い方と解釈されると言う性質によります。これが適用範囲のシャドウです。この場合コンストラクタの内部でクラスレベルの変数のAgeにアクセスするにはMe.Ageなどと記述します。
適用範囲のよるシャドウについては初級講座第8回 もっと変数でもふれています。
前回も少し書きましたが適用範囲がProtectedであるメンバは定義されているクラス以外では派生クラスからアクセスできませんから、派生クラスで使用されることが前提となっていると言うことができます。特にProtectedでかつOverridableになっているものは派生クラスでオーバーライドすることがはじめから想定されているのです。適用範囲がProtectedであるメンバのことを「プロテクトメンバ」と呼びます。
MSDNライブラリをみるとプロテクトメンバの数の多さに驚くかもしれません。クラスによってはPublicであるメンバよりもプロテクトメンバの方が多いものもあります。
プロテクトメンバの多くは、内部の処理に使用するメソッドで、派生クラスからも呼び出せた方が便利であったり派生クラスでオーバーライドする可能性があるものです。プロテクトメンバのもう1つの大きなグループは名前が「On」から始まっているメソッドです。これらは継承階層の中でイベントのように使用されます。また、実際にイベントを発生させる役目もあります。
もちろん、名前が「On」から始まると言う目印は文法的なものではないので、クラスの設計者はこの慣習を無視することもできます。しかし、.NET Frameworkの標準のクラスライブラリではこの慣習が守られているようです。
たとえば、MSDNライブラリでTextBoxのプロテクトメンバを見てみるとOnClick、OnKeyPress、OnTextChangedなどイベントに対応するかのように大量の「On」から始まるメソッドがあることがわかります。これらはオーバーライド可能であり派生クラス側で制御することができるようになっています。
これらのメソッドは実はイベントを発生させているだけで他には特に何もしません。名前を見れば想像がつくようにOnClickメソッドはClickイベントを発生させますし、OnKeyPressイベントはKeyPressイベントを発生させます。ということは、これらのメソッドを派生クラス側で変更することでイベントの発生を制御できるわけです。
そろそろ具体例を見ましょう。
次の例はTextBoxクラスを継承したTextBoxExクラスです。OnKeyPressメソッドをオーバーライドしていますが、特に処理は追加していません。
Public
Class TextBoxEx Inherits TextBox Protected Overrides Sub OnKeyPress(ByVal e As System.Windows.Forms.KeyPressEventArgs) MyBase.OnKeyPress(e) End Sub End Class |
■リスト10
念のためにKeyPressイベントがちゃんと発生することを確認しておきましょう。プログラムをビルドしてツールバーからTextBoxExを1つフォームに貼り付けてください。VB.NET2003以前をお使いの方は手動でツールボックスにコンポーネントを追加する必要があります。
そして、フォームに次のようにプログラムします。
Private
Sub TextBoxEx1_KeyPress(ByVal
sender As Object,
ByVal e As
System.Windows.Forms.KeyPressEventArgs) Handles
TextBoxEx1.KeyPress MsgBox("押されたキー:" & e.KeyChar) End Sub |
■リスト11
これで実行すると少し邪魔ですが、TextBoxExにキーを入力するたびに入力したキーが表示されます。つまりKeyPressイベントは正確に動作していることになります。
ところで、[ESC]キーを押してもKeyPressイベントが発生すると言うことをみなさんはご存じだったでしょうか?ためしに、TextBoxExで[ESC]キーを押してみてください。少し文字化けしたかのようなメッセージになりますが、メッセージが表示されることからイベント自体は発生していることがわかります。
ここでTextBoxExのプログラムを修正して[ESC]キーではKeyPressイベントが発生しないようにしてみましょう。
次のようになります。
Public
Class TextBoxEx Inherits TextBox Protected Overrides Sub OnKeyPress(ByVal e As System.Windows.Forms.KeyPressEventArgs) If e.KeyChar = Chr(Keys.Escape) Then e.Handled = True Return End If MyBase.OnKeyPress(e) End Sub End Class |
■リスト12
これで実行すると今度は[ESC]キーではイベントが発生しなくなります。
このようにして継承を利用して派生クラス側でイベントの発生を制御することが可能になるのです。
さて、Paintイベントはコントロールの外見を描画するイベントです。ですから、このイベントに対応するプロテクトメソッドであるOnPaintをオーバーライドするとコントロールの外見をかなりの自由度で変更することができます。
コントロールの継承についてはいろいろと考慮しなければいけない点もあるので、詳しくは別の機会に説明したいと思っていますが、参考までにButtonクラスを継承して丸い外見にするEllipseButtonクラスを紹介しておきます。
VB.NET2002、VB.NET2003で使用する場合はイベントハンドルの記述法や型変換の部分を少し変える必要がありますが、大部分は同じコードが通用します。
Imports System.Drawing.Drawing2D Public Class EllipseButton Inherits Button Protected Overrides Sub OnPaint(ByVal pevent As System.Windows.Forms.PaintEventArgs) Dim Color1 As Color Dim Color2 As Color If IsMouse Then If Control.MouseButtons = Windows.Forms.MouseButtons.Left Then Color1 = Color.Orange Color2 = Color.Beige Else Color1 = Color.Beige Color2 = Color.Orange End If Else Color1 = Color.Aqua Color2 = Color.Blue End If Dim b1 As New LinearGradientBrush(pevent.ClipRectangle, Color1, Color2, LinearGradientMode.ForwardDiagonal) Dim b2 As New LinearGradientBrush(pevent.ClipRectangle, Color2, Color1, LinearGradientMode.ForwardDiagonal) pevent.Graphics.FillRectangle(b1, pevent.ClipRectangle) Dim InnerRect As RectangleF = pevent.ClipRectangle InnerRect.Inflate(-5, -5) pevent.Graphics.FillEllipse(b2, InnerRect) Dim sf As New StringFormat sf.Alignment = StringAlignment.Center sf.LineAlignment = StringAlignment.Center Dim TextBrush As New SolidBrush(Me.ForeColor) pevent.Graphics.DrawString(Me.Text, Me.Font, TextBrush, InnerRect, sf) End Sub |
Private Function ToRectangleF(ByVal
Rect As Rectangle) As
RectangleF Return New RectangleF(Rect.Left, Rect.Top, Rect.Width, Rect.Height) End Function |
Dim IsMouse As
Boolean Private Sub EllipseButton_MouseEnter(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.MouseEnter IsMouse = True End Sub |
Private Sub
EllipseButton_MouseLeave(ByVal sender
As Object,
ByVal e As
System.EventArgs) Handles
Me.MouseLeave IsMouse = False End Sub |
Private Sub EllipseButton_Resize(ByVal
sender As Object,
ByVal e As
System.EventArgs) Handles
Me.Resize Dim Path As New GraphicsPath Path.AddEllipse(Me.ClientRectangle) Me.Region = New Region(Path) End Sub End Class |
■リスト13
このボタンを利用してフォーム側でもある程度作りこめばかなり変わった外観のアプリケーションを作成することができます。以下はその例です。
■画像5:オーナードローを活用するとこのような画面も作成できる。
OnPaintなど描画関連の仕組みをオーバーライドなどの手段でカスタマイズすることをオーナードローまたはオーナー描画とよびます。独自の外観のフォームやコントロールに興味がある方はオーナードローまたはオーナー描画をキーワードに調べてみてください。
ここまでで基底クラスに機能を追加する方法と、基底クラスの機能を変更する方法を説明しました。また、その際に注意すべきMeやMyClassの用法についてもふれました。
今度は基底クラスの機能を削除する説明をします。たとえば、基底クラスのメンバに直接アクセスされたくない場合や、インテリセンスの一覧から基底クラスのメンバを隠したい時などに基底クラスの機能(の呼び出し口)を派生クラス側では削除したいことがあります。厳密には「削除」というよりも継承したくないし、隠したいということになります。
※単純に「隠蔽」と言うと「シャドウ」のことを指すので混同しないように注意してください。
ところが、基底クラスのメンバを削除したり、継承しなかったり、アクセスできなくすると言う機能はありません。
派生クラス側で基底クラスのメンバと同じ名前のメンバのシャドウを作成してPrivateにすればアクセスできないようにできそうに思えるかもしれませんがうまくいきません。 この場合、VBは基底クラスのメンバに直接アクセスします。
たとえば、次のクラスは背景色を変更できないテキストボックスを作成することを意図して、TextBoxクラスのBackColorプロパティをアクセス不能にしようとしていますが、実際にはBackColorプロパティにはアクセス可能ですし、プロパティウィンドウで変更することも普通にできます。もちろん例外も発生しません。
Public Class
TextBoxEx Inherits TextBox Private Shadows Property BackColor() As System.Drawing.Color Get Throw New InvalidOperationException("BackColorプロパティはアクセス禁止!") End Get Set(ByVal value As System.Drawing.Color) Throw New InvalidOperationException("BackColorプロパティはアクセス禁止!") End Set End Property End Class |
■リスト14:この例は意図したとおりに作用しない。
これはShadowsはたまたま名前が同じだけの別のメンバとして認識されるので、基底クラスのBackColorプロパティがオーバーライドされていないと判断されるためです。オーバーライドされていないメンバは既定で、基底クラスのメンバに直接アクセスできます。これは今までのManmalクラスの例でもあきらかです。
なお、適用範囲をPublicにすれば、アクセス可能でも例外が発生するようにすることはできます。
これらのことを考えると普通は基底クラスのメンバへのアクセスをできなくするという計画は諦めた方がよいのですが、実はトリッキーな手段を使って疑似的に実現することができます。対象のメンバと同じ名前のイベントを実装してしまうのです。
Public Class
TextBoxEx Inherits TextBox Public Shadows Event BackColor() End Class |
■リスト15
このように書くと、通常の方法ではプログラムからTextBoxExクラスのBackColorプロパティにはアクセスできなくなります。
■画像6:インテリセンスからBackColorプロパティを隠蔽
インテリセンス上に表示されなくなるだけではなく、BackColorプロパティにアクセスするコードはビルドエラーになります。ただし、プロパティウィンドウは誤魔化せません。
それにこの方法ですとBackColorという名前の実体のないイベントが追加されてしまいスマートではありません。ここでは基底クラスのメンバをアクセス不能にできると言う例として紹介はしましたが、万一採用される場合はデメリットもよく検討してください。
最後に継承を強制したり、継承できなくする制御について簡単に説明します。
MustInheritを使用するとクラスレベルでかならず継承しなければならないようにすることが可能です。
Public
MustInherit Class Test Public Sub Say() MsgBox("Hello") End Sub End Class |
■リスト16
MustInheritをつけるとクラスはインスタンス化できなくなります。共有メンバの呼び出しだけはできますが、それ以外は継承して派生クラスをつくることしか使い道がありません。このようなクラスのことを「抽象クラス」と呼びます。抽象クラスはポリモーフィズムを実現するために使用します。
同じ理由でメンバ単位でオーバーライドしなければ使えないようにすることもできます。これにはMustOverrideを使用します。こちらは抽象メソッドや抽象プロパティと呼びます。
使えないクラスやメソッドやプロパティなど何のために宣言するのか疑問に思われて当然です。この事情が分かればオブジェクト指向にもだいぶ慣れてきたことになるでしょう。詳しくは次回説明する予定です。
逆にNotInheritableを使用すると継承不可なクラスを作成することができます。
Public
NotInheritable Class Test Public Sub Say() MsgBox("Hello") End Sub End Class |
■リスト17
これは、クラスが他のクラスとの関係を前提としているなど、派生クラスを作成してもうまく動作させることができない場合や、制御が複雑すぎるなど何らかの理由で混乱をさける目的で使用したりするようです。メンバのレベルでオーバーライドを不可能にするにはNotOverridableを使用します。
NotInheritableは指定してもしなくてもそのクラス自体の機能や性質にはまったく影響はありません。継承を不可能にしてまで何かを守ろうとするかは設計者の判断に委ねられます。MSDNライブラリをよく見ると.NET Frameworkには継承不可能なクラスがたくさんあります。たとえばDBNullクラスやAppDomainクラスは継承できませんし、理由もすぐに想像できます。DBNullクラスは唯一性が重要なので派生クラスの存在自体を否定する必要がありますし、AppDomainクラスはアプリケーション境界として働きますので派生クラスで妙な実装をされて.NET Frameworkの仕組みが台無しにされるのを防いでいるのでしょう。とは言え、この目的であればメソッドレベルでNotOverrideにするだけで足りますからクラス自体がNotInheritableになっている事情はいまいちわかりにくいです。
他にもFileInfoクラスやStringクラスなどさまざまなクラスが継承不可能です。VBの継承機能はVB.NET 2002から加わった機能なのですが、当時私はまっさきにStringクラスの継承にチャレンジしました。自分で文字列に機能を追加できるとしたらいろいろな使い道がありそうだと思ったからです。しかし、残念ならがStringクラスは継承できないのでした。VBの言語仕様に密接に関連しているからでしょうか?できれば継承したいものです。
VB2008からは継承の他に、拡張メソッドと言う機能を使用してクラスに機能を追加できるようになりました。これでようやくStringクラスに機能を追加することができます。継承できないクラスに機能を追加する場合は拡張メソッドの使用を検討してください。
次の例はStringクラスに文字列をすべて全角に変換するToWideメソッドを追加します。
Module ExtensionMethods <System.Runtime.CompilerServices.Extension()> _ Public Function ToWide(ByVal s As String) As String Return StrConv(s, VbStrConv.Wide) End Function End Module |
■リスト18
このモジュールが適用範囲にあれば、ToWideメソッドが使用可能になります。
■画像7:拡張メソッドの使用
最後に1つだけ注意しておくことがあります。継承は大変便利な機能で使い方次第でプログラムにかかる時間が大幅に短縮できる可能性がありますが、実際に継承を使う際には本当に継承が必要であるかどうかよく検討してください。
ある程度の規模のプログラムになると、無計画に継承を使用していくとプログラムが複雑になり収拾がつかなくなってしまいます。一つだけ機能を変更するつもりで基底クラスを変更したら、思わぬところに影響がでるということは容易に想像ができます。基底クラスの修正は影響が大きいのです。
ですから、たとえば単にClassAとClassBで共通の機能があるという理由だけで、ClassZを作成して、ClassAとClassBはそれを継承すると言うようなことは避けるべきです。単に共通の機能があるというだけならば共通の機能にアクセスするためのシンプルなクラスを1つ作成する方が優れています。
博士のワンポイントレッスン
|