Visual Basic 中級講座 |
Visual Basic 中学校 > 中級講座 >
キーワードや構文を中心に継承について説明します。
概要 ・継承とは既に存在するオブジェクトの振舞いを引き継いだ新しいオブジェクトを作成することである。 ・継承を使用するにはInheritsを使う。 ・継承元のクラスを「基底クラス」、「ベースクラス」などと言い、継承によって作成されるクラスを「派生クラス」と呼ぶ。 ・ 基底クラスのメソッドやプロパティの機能を変更するにはOverridesを使用する。ただし、そのためには基底クラス側で該当のメソッドやプロパティがOverridableまたはOverridesで宣言されている必要がある。 |
今回説明する継承とは、既に存在するオブジェクトの機能を引き継いだ新しいオブジェクトを作成する機能のことです。
継承を使用するとTextBoxにメソッドやプロパティを追加したりすることが簡単にできます。 継承の基になるクラスは.NET Frameworkのクラスでも独自に作成したクラスでも構いません。
プログラマは追加する機能だけをプログラムすればよいので、全角文字しか入力できないTextBoxなどを簡単に作成することができます。ただし、TextBoxの場合は入力制限は簡単にできても、コピー&貼り付けを制限するのは骨が折れます。
継承機能の使い方には大きく2つの方向性があります。
1つは、今例に挙げたようにクラスに独自の機能を追加してどんどん専門的で便利なクラスを作り上げていく方向です。1度作ったクラスを保存しておいていろいろなプロジェクトで使いまわせば驚くほどプログラム効率が向上することでしょう。この方向性は汎用のクラスを専門化・特殊化していく方向ですので、「特化」と 呼びます。
■画像1:「特化」のための継承のイメージ
もう1つの方向は、すでに専門的な用途のクラスが複数存在する場合に、その基となる機能をもったクラスを想定する方向です。たとえば、TextBoxとComboBoxは機能が似ています。この2つの共通機能だけを抽出したクラスと言うものは簡単に想像できます。このような共通化の方向で継承を考えることは次回説明するポリモーフィズムの発想に大きく貢献します。この方向を「汎化」と呼びます。
■画像2:「汎化」のための継承のイメージ
この図はわかりやすさを優先して書いており、実際にはTextBoxとComboBoxとMaskedTextBoxの継承関係はもっと複雑です。
一般的に特化のための継承は考え方が理解しやすく、いくつかのキーワードや構文を覚えればすぐに使えるようになります。汎化のための継承は考え方も難しく使いこなせるようになるには熟練が必要です。どちらの継承もキーワードや構文はまったく同じです。「何のために継承するのか」 、「継承によってどういう効果を意図しているのか」という発想が異なるだけです。
今回と次回は特化のための継承をとりあげて、継承に使用するキーワードや構文を説明します。
その次の回では汎化のための継承を取り上げてポリモーフィズムの領域まで話を進める予定です。
継承を利用するにはInheritsを使用するだけです。
たとえば、次の例ではTextBoxクラスの機能を引き継いだ新しいTextBoxExクラスを作成します。
Public Class
TextBoxEx Inherits TextBox End Class |
■リスト1:TextBoxクラスの継承
たったこれだけでTextBoxクラスと同じ機能をもったクラスが作成できるのが継承の優れたところです。もし自分でTextBoxをゼロから作成するとなるとかなりの量のコードを書く必要がありますが、すでに存在するTextBoxの機能をそのまま使用できるため、プログラマは追加したい機能や、変更した機能の部分だけをプログラムすればよいことになります。
まったく同じ機能のクラスを作成するのだったらはじめからあるTextBoxクラスを使用した方が簡単ですから 、継承を利用する場合のほとんどはこのようにもともとの機能に何かを追加するときや、もともとの機能に何か変更を加える場合です。
継承では元となるクラスのことを「基底クラス」、それを引き継ぐクラスのことを「派生クラス」と呼びます。この例ではTextBoxクラスが基底クラスでTextBoxExクラスが派生クラスになります。この基底クラス・派生クラスと言う言葉は継承を説明する上でとてもよくでてくるので必須です。
ただし、文化によっては別の言葉で表現する場合もありますから、念のために別名も書いておきます。他のドキュメントやWebサイトを見るときの参考にしてください。VB界では「基底クラス」・「派生クラス」という表現が公式のものです。しかし、MSDNライブラリやエラーメッセージでは基底クラスのことをベースクラスと表現することも多いようです。
用語 | 別名 |
基底クラス | スーパークラス、親クラス、ベースクラス。 |
派生クラス | サブクラス、子クラス。 |
■表1
基底クラスとして指定できるのは1つのクラスだけです。複数のクラスを基底クラスにすることはできません。C++などの重量級の言語では複数のクラスからの継承ができるようになっていますが、VBやC#などの言語は多重継承をできなくするかわりにわかりやすい構造になることを目指しているようです。
発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。 インターフェースを統一することが目的である場合は、Implementsキーワードを使用することでいくつでもインターフェースを実装することができます。ただし、Impelemntsではクラスを指定することはできません。インターフェースを指定できるだけですので当然機能の引き継ぎなどは行われません。 |
さて、上記の例に「Enterキーが押された場合に自動的に次のコントロールにフォーカスが移動する機能」を追加する例を紹介します。
次のようになります。
Public Class
TextBoxEx Inherits TextBox Protected Overrides Sub OnKeyPress(ByVal e As System.Windows.Forms.KeyPressEventArgs) 'KeyPressイベントを発生させる。 MyBase.OnKeyPress(e) 'イベントの受け取り側が処理済を通知してきた場合は何もしない。 If e.Handled Then Return End If '押されたキーが[Enter]だった場合はフォーカスを移動する。 If e.KeyChar = Chr(Keys.Enter) Then If Control.ModifierKeys = Keys.Shift Then 'Shiftキーが押されているときは前方にフォーカスを移動する。 Me.TopLevelControl.SelectNextControl(Me, False, True, True, True) Else '通常の場合は、後方にフォーカスを移動する。 Me.TopLevelControl.SelectNextControl(Me, True, True, True, True) End If 'イベントが処理済であることをシステムに通知する。 e.Handled = True End If End Sub End Class |
■リスト2:TextBoxを継承して機能を追加
ここではOverridesキーワードを利用してもともとのキー押下時の処理に変更を加えています。Overridesについては後で説明します。
このTextBoxExコントロールをフォームに貼り付けて使用するには、一度プロジェクトをリビルドします。VB2005以上であれば今回のような自作のコントロールはビルドすることで自動的にツールバーに追加されます。VB.NET2003以前の場合は手動でコンポーネントを追加する必要があります。
■画像3:ツールボックス
後は通常のコントロールと同じようにツールボックスからTextBoxExを選択してフォームに貼り付けるだけです。TextBoxExを3つくらいフォームに貼り付けてEnterキーで快適にフォーカスが移動できることを確認してください。[Shift] + [Enter]を使用すると逆にフォーカスが移動する機能もつけてあります。
ツールボックスに自動的にコントロールを追加するにはAutoToolboxPopulateがTrueになっている必要があります。 この設定は[ツール]メニューの[オプション]画面にある「Windowsフォームデザイナ」タブで変更できます。既定値はTrueなので特に通常は変更する必要はありません。また、VB.NET2003以前のバージョンにはこの設定はありません。 |
継承の利用方法さえわかっていればこのように簡単に既存の機能を拡張することができるのです。これはもっともわかりやすい継承の活用例です。ただしクラスによっては継承できないものもあります。たとえば、Stringクラスを継承することはできません。次の例はエラーになります。
Public Class
StringEx Inherits String End Class |
■リスト3:この例はビルドエラーになる。
ですのでStringクラスの機能を継承によって拡張することはできません。拡張メソッドを使用するとStringクラスに独自の機能を追加することは可能です。
クラスが継承可能であるか不可能であるかはクラスの設計者が自由に決めることができます。MSDNライブラリを見たときにクラスの定義にNotInheritableが含まれているものは継承できないクラスです。
なお、Inheritsキーワードを記述しなかった場合、そのクラスはObjectクラスを継承しているものとみなされます。ですから、すべてのクラスは必ずObjectクラスの機能を備えていることになります。ObjectクラスにはToStringやGetTypeなどの基本的な5つのプロパティが実装されています。すべてのクラスでToStringやGetTypeが使用できるのはさかのぼってみれば必ずObjectクラスを継承しているからなのです。
以下はObjectクラスのPublicメンバの一覧です。
メソッド | 読み方 | 機能 |
Equals | イコールズ | 2つの変数が同じインスタンスであるか判断します。「同じ」かどうかの判断基準はMSDNライブラリを参照してください。 |
GetHashCode | ゲットハッシュコード | ハッシュ値を生成します。 |
GetType | ゲットタイプ | インスタンスの型情報にアクセス可能なオブジェクトを生成します。 |
ReferenceEquals | リファレンスイコールズ | 2つの変数が同じインスタンスを指しているかを判断します。2つの変数が値型の場合は常にFalseを返します。 |
ToString | トゥーストリング | インスタンスを文字列で表現します。 |
■表2:ObjectクラスのPublicメンバの一覧
博士のワンポイントレッスン
|
継承を使って基底クラスにないまったく新しい機能を派生クラスに追加するには、単にメソッドやプロパティを記述するだけです。
次の例ではTextBoxを継承したTextBoxExクラスにFocusNextメソッドを追加しています。このメソッドは次のコントロールにフォーカスを移動します。FocusNextという名前のメソッドはTextBoxには存在しないので、まったく新しい機能の追加と言うことになります。
Public Class
TextBoxEx Inherits TextBox ''' <summary>次のコントロールにフォーカスを移動します。</summary> Public Sub FocusNext() Me.TopLevelControl.SelectNextControl(Me, True, True, True, True) End Sub End Class |
■リスト4
フォーム側では通常のメソッドと同じようにこのメソッドを呼び出すことができます。次の例ではTextBoxEx1に対してキー入力を行うと次のコントロールにフォーカスが移動します。2つ以上のコントロールを貼り付けて試してみてください。
Public Class
Form1 Private Sub TextBoxEx1_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TextBoxEx1.TextChanged TextBoxEx1.FocusNext() End Sub End Class |
■リスト5
なお、対象のバージョンは2005以降と表示していますが、2002、2003でもまったく同じように使用することができます。2002、2003の場合はForm1の宣言の下に自動生成されたコードがあるのだけが違いですが、この違いは継承機能にはまったく関係ありません。
同じ名前のメソッドやプロパティでも引数の型や数が異なるならば、そのまま記述することで機能を追加することができます。これは基底クラスのメソッドに対するオーバーロードになります。オーバーロードについては初級講座第48回 高度なメソッド・プロパティを参照してください。
ただし、同じクラス内でメソッドやプロパティなどのメンバをオーバーロードする場合、Overloadsキーワードは必須ではありませんが、派生クラス側で基底クラスのメソッドをオーバーロードする場合は必ずOverloadsを指定する必要があります。
次の例ではTextBoxのAppendTextメソッドのオーバーロードを作成しています。このバージョンのAppendTextでは、追加するテキストの数を指定することができるようにしています。
Public Class
TextBoxEx Inherits TextBox ''' <summary>テキストボックスの現在のテキストにテキストを追加します。</summary> ''' <param name="text">テキストボックスの現在の内容に追加するテキスト</param> ''' <param name="count">追加するtextの数</param> Public Overloads Sub AppendText(ByVal text As String, ByVal count As Integer) For i As Integer = 0 To count - 1 Me.AppendText(text) Next End Sub End Class |
■リスト6
次の例はフォーム側でこのAppendTextメソッドを呼び出す例です。この例では「VB」という文字列が5個追加されます。
Private Sub
Button1_Click(ByVal sender
As System.Object,
ByVal e As System.EventArgs)
Handles Button1.Click TextBoxEx1.AppendText("VB", 5) End Sub |
■リスト7
既存の機能とほとんど同じ動作で、少しだけ機能を追加したいような場合は次の「機能の変更」の説明を参照してください。
基底クラスの機能を変更するにはOverridesキーワードを使用します。この操作は「オーバーライド」や「機能の上書き」と呼ばれます。
次の例ではTextBoxのSelectionLengthプロパティの機能を変更します。SelectionLengthプロパティは選択されている文字数を返すプロパティですが、この機能を変更して選択されている文字 をShift-JISでエンコードしたときのバイト数を返すようにしています。
Public Class
TextBoxEx Inherits TextBox Public Overrides Property SelectionLength() As Integer Get Dim ShiftJIS As System.Text.Encoding = System.Text.Encoding.GetEncoding("Shift-JIS") Return ShiftJIS.GetByteCount(Me.SelectedText) End Get Set(ByVal value As Integer) MyBase.SelectionLength = value End Set End Property End Class |
■リスト8
なお、SelectionLengthプロパティに値をセットする場合の方は機能を変更していないので、バイト数ではなく文字の数でカウントされます。 この機能は基底クラスの機能とまったく同じであるため基底クラスのSelectionLengthプロパティを呼び出すだけで済みます。
基底クラスにアクセスするにはこの例のようにキーワードMyBaseを使用します。これは自分自身を表すMeと近い位置付のキーワードであると言えます。ただし、MeやMyBaseの使用法には注意すべき点もありその点については後述します。
なお、この例のように既にあるメンバを上書きする場合、上書きされる基底クラスのメンバのことをベースメソッド・ベースプロパティと呼びます。上記の例ではTextBox.SelectionLengthプロパティがベースプロパティです。
MyBaseを使用すると基底クラスの機能が呼び出せるので、「基底クラスとほとんど同じだけどちょっとだけ機能を追加したい」というときに便利です。
たとえば、次のSmallQueueクラスはEnqueueメソッドのにちょっとだけ機能を追加しています。Queueクラスはコレクションの一種で、通常Enqueueメソッドを使用するといくつでも項目を追加することができますが、すでに3つ追加されている場合は最初の項目をクリアしてから新しい項目を追加するように改造しています。
Public Class
SmallQueue Inherits Queue Public Overrides Sub Enqueue(ByVal obj As Object) If Me.Count = 3 Then Me.Dequeue() End If MyBase.Enqueue(obj) End Sub End Class |
■リスト9
このとき、実際に項目を追加する処理は基底クラスにまかせて自分で書くことはしません。基底クラスに処理を委ねるためにMyBase.Enqueueを使用しています。こうすることで、プログラマは追加の機能の部分だけを書けばよいことになります。もし、ちょっとでも機能を変更するならばメソッドのすべてを自分で書かなければいけないとすると大変な負担になってしまいますから、このようにMyBaseを使用することで大幅な省力化が図れることは重要です。
念のために上記のSmallQueueクラスをテストするコードも載せておきます。
Dim
q As New
SmallQueue q.Enqueue("アメンボ") q.Enqueue("イノシシ") q.Enqueue("ウマ") q.Enqueue("エリマキトカゲ") MsgBox(q.Dequeue) |
■リスト10
通常のQueueクラスであれば、最後のMsgBoxで「アメンボ」と表示されますが、SmallQueueクラスでは3つまでしか項目を持てないように改造しているので、4つ目のエリマキトカゲを追加した時点で「アメンボ」は削除されます。ですから、最後には「イノシシ」と表示されます。
熟練のプログラマは基底クラスのメンバをオーバーライドするコードを効率的に入力することができます。効率的に入力するためにはPublicやProtectedなどの適用範囲を入力しないで、いきなり「Overrides」と入力してスペースキーを押します。
■画像4:overridesとスペースを打つと継承可能なメンバの一覧が表示される。
そうすると、オーバーライド可能な基底クラスのメンバの一覧が表示されるので、後はいつものインテリセンスの要領で対象のメンバを選択して[TAB]か[ENTER」を押すだけです。
これだけで、最低限のコードが自動的に生成されます。
■画像5:一覧から項目を選択すると自動的に最低限のコードが生成される。
さて、基底クラスのメンバであればどれでもオーバーライドできるわけではありません。
たとえばPrivateで宣言されているメンバはそのクラス内でのみ使用されることが前提となっているため当然オーバーライドできません。Private以外の適用範囲の場合は適用範囲上はオーバーライド可能です。オーバーライドはできるようにしたいけど外部からはアクセスしたくない場合は適用範囲にProtectedを使用します。
Protectedで宣言された要素は自分のクラスの他にも派生クラスからしかアクセスできません。つまり、継承のためにある適用範囲なのです。
なお、ProtectedとFriendは競合しないためProtected Friendと記述して両方指定することも可能です。念のために補足しておくとFriendは同じプロジェクト内でのみアクセス可能な適用範囲です。
オーバーライド可能な条件は適用範囲だけではありません。ほかにも継承独特の制限があります。
継承独特の制限とは、基底クラスでOverridable付きで宣言されているメソッドかプロパティ、または基底クラスでOverridesされているメソッドかプロパティのみがオーバーライド可能と言う制限です。
たとえば、次のクラスを基底クラスとするときのことを考えています。
Public Class
Test1 Public Overridable Sub Sub1() ' End Sub |
Public Sub
Sub2() ' End Sub |
Public Overridable
Function Func1() As
Integer ' End Function |
Public Function
Func2() As
Integer ' End Function |
Public Overrides
Function ToString()
As String Return "ToStringのOverridesです。" End Function End Class |
■リスト11
少しわかりにくいかもしれませんが、このTest1クラスでは5つのメンバが実装されています。それぞれOverridableがあったりなかったり、SubだったりFunctionだったりと特色を付けてみました。
メンバ | 種類 | Overridable | Overrides |
Sub1 | Sub | あり | なし |
Sub2 | Sub | なし | なし |
Func1 | Function | あり | なし |
Func2 | Function | なし | なし |
ToString | Function | なし | あり |
■表3
ToStringメソッドは基底クラスのObjectから継承したものです。前述したようにInheritsでの指定がないクラスはすべてObjectクラスを継承していることになります。
さて、この5つのメンバの中で派生クラスでオーバーライドできるものはどれでしょうか?
答えは先ほどの法則通りです。つまりOverridableかOverridesがついているメソッドかプロパティがオーバーライド可能ですので、答えはSub1、Func1、ToStringの3つということになります。
このことを実際に確認するには、Test1クラスを継承するクラスを自分で作成してみましょう。
Public Class
Test2 Inherits Test1 End Class |
■リスト12
そして、「overrides」と入力してスペースを打ったときに一覧に表示されるものがオーバーライド可能なメンバです。
■画像6:オーバーライド可能なメンバの一覧
画像をみるとSub1、Func1、ToStringの他にEquals、GetHashCodeという2つのメソッドもオーバーライド可能になっています。これらはObjectクラスでOverridableで宣言されているクラスです。
なお、当然ですがオーバーライド可能を表すキーワードや構文は言語によって異なります。ここでは基底クラスがVBで記述されていることを暗黙の前提として説明しましたが、C#やDelphiなど他の言語で記述されているクラスを継承する場合は、OverridableやOverridesではなく言語ごとにあらかじめ既定されているキーワードや構文で記述されているものがオーバーライド可能になります。詳しくは対象の言語の仕様を調べる必要がありますが、継承可能なものの一覧は「overrides」とスペースを入力すれば見ることができますから特に困ることはないでしょう。
オーバーライドを使用する場合はメソッドやプロパティの名前を基底クラスのものと同じにする必要があるだけでなく引数の型や順序、戻り値の型、さらにはPrivateやPublicなどの適用範囲にいたるまで完全に一致させる必要があります。もし、名前は同じにしたいけど引数や戻り値の型を変更したい・適用範囲を変えたいと言う場合にはオーバーライドを使用することはできません。後述するShadowsを使用すればこのようなことも可能ですが、Shadowsは継承機能を台無しにする可能性を秘めているので 使用の際にはよく検討してください。
継承に関連したコンストラクタ、つまりSub Newの扱いは他のメソッドと比べると少し特殊です。
まず、コンストラクタはオーバーライドすることができません。同じ引数のコンストラクタを定義しても一種のシャドウとみなされます。当然Overridesキーワードを使用することもできませんが、明示的にシャドウを行うShadowsキーワードも使用できません。 シャドウについては次回説明します。
また、基底クラスに引数つきのコンストラクタが存在する場合で、かつ基底クラスに引数なしのコンストラクタが存在しない場合、派生クラスは必ずコンストラクタを実装する必要があります。そうしないと、コンパイラが派生クラスのインスタンスを生成する方法が分からなくなってしまうからです。
たとえば、次の継承を考えます。以下のプログラムは良くない例です。このプログラムはビルドエラーになるので実際には実行できません。
Public Class
TestA Public Sub New(ByVal X As Integer) End Sub End Class |
Public Class TestB Inherits TestA End Class |
■リスト13:この例はビルドエラーになる。
このとき、プログラムでTestBクラスのインスタンスを作成するために次のようなコードを書いたとしたらどうでしょうか?
Dim test As New TestB |
■リスト14
TestBクラスだけを見れば構文上引数なしのコンストラクタが使用可能ですからこの例の方法でのインスタンスの作成は可能なはずです。
しかし、継承関係を考えてみるとTestBをインスタンス化するためにはTestAがインスタンス化できる必要があります。ところがTestAをインスタンス化するためにはコンストラクタに引数が必要です。ですから、この例はビルドエラーになるのです。
この問題を解決する方法はいくつかあります。1つはTestBクラスでTestAクラスと同じコンストラクタを実装することです。
Public Class
TestB Inherits TestA Public Sub New(ByVal X As Integer) MyBase.New(X) End Sub End Class |
■リスト15
このとき重要なのはコンストラクタの最初の行で必ず基底クラスのコンストラクタを呼び出すことです。基底クラスのコンストラクタを呼び出すにはMyBase.Newを使用します。これによって、コンパイラは基底クラスをインスタンス化できるようになります。
かなり特殊なのですが、この基底クラスのコンストラクタの呼び出しは必ずコンストラクタの1行目に書く必要があります。何をおいてもまず基底クラスのインスタンス化ができなければ何も始まらないからです。
次の例はビルドエラーになります。
Public Class
TestB Inherits TestA Public Sub New(ByVal X As Integer) MsgBox("コンストラクタです。") MyBase.New(X) End Sub End Class |
■リスト16:この例はビルドエラーになる。
次の例は正常に動作します。
Public Class
TestB Inherits TestA Public Sub New(ByVal X As Integer) MyBase.New(X) MsgBox("コンストラクタです。") End Sub End Class |
■リスト17
このように内部の処理の順番にまで構文上の規則があるのは、このコンストラクタの呼び出しだけです。極めて特殊な構文と言えるでしょう。
なお、C#の場合は基底クラスのコンストラクタの呼び出しもコンストラクタの宣言に含めてしまいます。
//TestAを継承したTestBクラスの宣言 public class TestB : TestA { //コンストラクタ。C#ではクラス名と同じ名前のメソッドがコンストラクタ。 public TestB(int X) : base(X) { } } |
■リスト18
この例ではコンストラクタの宣言の後ろに「: base(X)」という記述が付いていますが、この部分がVBでいうとMyBase.New(X)に相当します。
C#の例まで持ち出して何が言いたいかと言うと、要するにVBでコンストラクタの1行目に必ず基底クラスのコンストラクタ呼び出しを書かなければいけないというととても奇異な感じがしますが、本来はこの仕様は構文的なものであるということなのです。
VBもC#みたいに構文で表現してもいい気はしますが、私がVBの設計者だったらどうしたでしょうか。悩みどころではあります。
最初にも書きましたが、この1行目で基底クラスのコンストラクタを呼ぶと言う仕様は、基底クラスに引数なしのコンストラクタがない場合の話です。基底クラスに引数なしのコンストラクタが存在する場合、コンパイラはいくらでも自由に基底クラスをインスタンス化できますからコンストラクタの呼び出しで苦労することはありません。
さて、引数付きのコンストラクタでは、基底クラスの異なるバージョン(オーバーロード)のコンストラクタを呼び出すこともできます。
その例を次に示します。
Public Class
ShiftJISReader Inherits System.IO.StreamReader Public Sub New(ByVal FileName As String) MyBase.New(FileName, System.Text.Encoding.GetEncoding("Shift-JIS")) End Sub End Class |
■リスト19
この例では対象ファイルをShift-JISで読み込むShiftJISReaderクラスを作成しています。ベースとなっている機能はStreamReaderから継承しており、コンストラクタを付け加えただけです。エンコード方式はShift-JISに決まっているのでコンストラクタではファイル名だけを指定すればよいようにしています。ただし基底クラスのコンストラクタはファイル名とエンコード方式の両方を指定するバージョン(オーバーロード)を呼び出しています。