Visual Basic 初級講座 |
Visual Basic 中学校 > 初級講座 >
いろいろな タイプのメソッド・プロパティの作成方法を説明します。省略可能な引数や可変引数、多重定義、共有メンバ、静的変数などさまざまなバリエーションのメンバの作成方法・使用方法が登場します。
概要 ・呼び出し時に省略可能な引数を定義するにはOptionalを使う。
・呼び出し時に引数の数を可変で指定できるようにするにはParamArrayを使う。
・名前が同じ複数のメソッド・プロパティを定義することができる。これを多重定義と呼ぶ。
・多重定義をするときにはOverloadsを指定するとプログラムがわかりやくするなる。 ・共有メンバを定義するにはSharedを使う。 ・適用範囲を離れても値が保存されている静的変数を定義するにはStaticを使う。 |
今回はメソッドやプロパティの作り方に関するやや高度な5つの事柄を説明します。メソッド・プロパティの基本的な作成方法については初級講座ですでに説明しています。もし、基本的なことをまだ理解していないようならそちらの記事をはじめに読んだほうが良いかもしれません。
リンク
|
今回紹介する5つの事柄を簡単にまとめておきます。
省略可能引数 | Optional | 指定しても指定しなくても良い引数を持ったメソッド・プロシージャの作成方法 |
可変引数 | ParamArray | 引数の数があらかじめ定まっていないメソッド・プロシージャの作成方法 |
多重定義 | Overloads | 名前が同じで引数が異なるメソッド・プロシージャの作成方法 |
共有メンバ | Shared | インスタンスを経由せずに直接使用できるメソッド・プロシージャの作成方法 |
静的変数 | Static | 適用範囲を離れても値を保つ変数を使ったメソッド・プロシージャの作成方法 |
■表1:今回の5つのテーマ
博士のワンポイントレッスン
|
Optionalキーワード(読み方:Optional = オプショナル)を使用すると省略可能な引数を作ることができます。
たとえば、次のメソッドは挨拶を表示しますが、日本語・英語のどちらで表示するか引数で指定することができます。ただし、引数を省略した場合は日本語で表示します。
'■Hello ''' <summary>挨拶文を返します。</summary> ''' <param name="Language">0のとき日本語、その他の場合英語で挨拶文を作成します。</param> ''' <returns>挨拶文</returns> Public Function Hello(Optional ByVal Language As Integer = 0) As String If Language = 0 Then Return "こんにちは、みなさん!" Else Return "Hello, everybody!" End If End Function |
■リスト1:Optionalを使った省略可能引数のシンプルな例
この例では引数LanguageにOptionalがついているため 、メソッドの呼び出し時にこの引数を省略できるようになります。そして、引数の宣言の後ろに = 0 とあるのは省略された場合の既定値です。つまり、引数を省略した場合は0が指定されたものとみなされます。
このメソッドは通常通り引数を指定して呼び出すこともできます。
Dim St
As
String St = Hello(0) MsgBox(St) |
■リスト2:省略可能引数といえども省略しないで指定することももちろん可能
引数を指定しないで次のように呼び出すこともできます。
Dim St
As
String St = Hello() MsgBox(St) |
■リスト3:引数を省略して呼び出す例
引数が複数ある場合に、そのうちの1つの引数を省略可能にするときはそれより後ろにあるすべての引数を省略可能にしなければなりません。
Public
Sub Test(ByVal
X As
Integer,
Optional
ByVal Y
As
Integer = 100,
Optional ByVal Z
As
Integer = 1) End Sub |
■リスト4:複数の省略可能引数の例
上記の例では引数Yと引数Zは省略可能で、省略した場合はそれぞれ100と1が指定されたものとみなされます。ZのOptionalをはずすとビルドエラーになります。Zより前にあるYが省略可能だからです。
なお、念のために確認しておきますがこの例でYを省略してZに値を指定する呼び出しかたは次のようになります。
Call Test(6, , 20) |
■リスト5:途中の引数を省略して呼び出す例
発展学習 -
注意!Optional使用時の既定値は呼び出し側が保存します 発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。 簡単便利なOptionalではありますが、1つだけ重要な注意があります。引数を省略した場合にどのような既定値を採用するかはOptionalを定義している側に記述しますが、それがどのような値であったかは呼び出し側にひそかに記録されているのです。 このことをよく知っておかないと、複数のプロジェクトを使用している場合に後で既定値を変更しても変更した既定値が反映されなくなります。 たとえば、次のようなクラスを作ってdllを作成したとしましょう。dll名は仮にOptionalLibrary.dllとします。
■リスト6:OptionalLibrary.dllのプログラム そして、あるプロジェクトでこのdllを参照して、次のようにプログラムしたとしましょう。このプログラムは仮にOptionalTest.exeという名前でビルドされるものとします。
■リスト7:OptionalTest.exeのプログラム OptionalTest.exeを実行すると、もちろん「こんにちは」と表示されます。 次にdll側のプログラムで以下のように既定値を修正してビルドし直したとしましょう
■リスト8:OptionalLibrary.dllの修正版のプログラム この状態でOptionalTest.exeを実行すると、「いただきます」ではなく「こんにちは」と表示されます。OptionalTest.exeが作成された時点での既定値が「こんにちは」だったからです。これが「既定値はOptional側に記述しますが、その値を保存しているのは呼び出し側です。」ということです。 この場合は、OptionalTest.exeをリビルドすると「いただきます」と表示されるようになります。 |
発展学習 - C#とOptional 発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。 上記のようにVBでは手軽に扱える省略可能引数ですが、兄弟言語であるC#で扱うのはなかなか大変です。 まず、C#自体には省略可能引数というものが存在しないのでC#を使ってVBのOptionalと同じようなメソッドやプロパティを作成することはできません。ただし、後述するOverloadsを使った多重定義で代用することは可能です。 VBで作った省略可能引数をもったメソッドやプロパティをC#から呼び出すのもなかなか骨が折れます。この場合でもC#では引数を省略できないのです。 このような事情ですからC#との連携を前提としている場合はOptionalは使用しないで後述するOverloadsで代用するのが良いでしょう。C#とは無関係なプログラムをするのであれば便利なOptionalを思う存分使いましょう。 |
いくつでも引数を受け入れるようにメソッド・プロシージャを作成することもできます。これにはParamArrayキーワード(読み方:ParamArray = パラムアレイ)を使用します。
この可変引数はたとえば、数値の平均値を求める場合などに便利です。普通に作っていたのでは数値が2つの場合、数値が3つの場合、4つの場合…とたくさんのメソッドを用意しなければなりませんがいくつ指定しても良いと言うことであれば1つ用意するだけで済みます。
平均値を求めるメソッドは次のようになります。
'■Average ''' <summary>平均値を求めます。</summary> ''' <param name="Values">平均を求める複数の数値</param> ''' <returns>平均値</returns> ''' <remarks></remarks> Public Function Average(ByVal ParamArray Values() As Integer) As Double Dim Sum As Integer '合計値 For Each i As Integer In Values Sum += i Next Return Sum / Values.Length End Function |
■リスト9:平均値を求めるメソッド。可変引数の例。
ParamArrayを指定した引数はすべて配列になります。指定されたすべての引数は配列の要素になります。
このメソッドを使って平均値を求める呼び出し側のプログラムは次のようになります。
Dim Ave
As
Double Ave = Average(100, 50, 70, 2, -50, 40) MsgBox(Ave) |
■リスト10:可変引数のメソッドの呼び出し例
同じ名前で引数が違うメソッド・プロパティを複数作成することもできます。このことを多重定義、オーバーロードなどと言います。
なぜ同じ名前のメソッド・プロパティを複数作る必要があるのでしょうか。考えられる3つのケースを表にまとめてみます。
ケース | 多重定義の例 |
型は違うけれども同じ機能のメソッド・プロパティがある場合(ジェネリックで代用可能な場合がある) | Public Function Sample(X As Integer) Public Function Sample(X As Long) |
同じ内容だけだ形式が異なる引数を用意する場合 | Public Function Sample(X As Integer, Y As Integer) Public Function Sample(X As Point) |
引数の指定を省略できるようにする場合 (Optionalを使用することも可能じ) |
Public Function Sample(Name As String, Age As Integer) Public Function Sample(Name As String) |
■表2:多重定義のケース
1つ目のケースでは多重定義ではなくジェネリックを使用してメソッドやプロパティを作成できる場合もあります。ジェネリックはVB2005から導入された機能でいろいろな型を動的に扱う機能です。ジェネリックに関しては中級講座で扱います。3つ目のケースでは既に登場したOptionalを使用してメソッドやプロパティを作成することも可能です。
ここからはそれぞれのケースを順に詳しく見ていきます。
まずは型が違うだけで機能が同じメソッドの場合から紹介します。例として与えられた数値を2倍にするだけの小さなメソッドを考えて見ます。たとえば、次のようになります。
Public
Function Twice(ByVal
X As
Integer)
As
Integer Return X * 2 End Function |
■リスト11:値を2倍にするシンプルなメソッド
これだけで一応完成ですが型がIntegerなのが気になります。つまりこのメソッドはLongやDoubleやDecimalの値を2倍にすることはできないのです。そこで、昔まだVBで多重定義ができなかったころは複数の型に対応するためにInteger用にはTwiceIntegerメソッドをつくり、Double用にはTwiceDoubleメソッドをつくりというように個別にメソッドを作っていました。
もっともObject型などの汎用的な型を使用する方法もありますが、この場合はパフォーマンス・メンテナンス性の面で不利になります。
今では名前のことは気にしないで素直にメソッドを書くことができますから特にこのようなことを気にする必要がありません。たとえば、このメソッドのDouble版は次の通りです。
Public
Function Twice(ByVal
X As
Double)
As
Double Return X * 2 End Function |
■リスト12:上記の例で引数と戻り値がDouble型になったもの。
このDouble版を上記のInteger版と同じクラス内に作成してもエラーにはなりません。VBはメソッドやプロパティを区別するのに、その名前だけではなく、名前と引数の型、引数の順番を考慮しているのです。
ですから2つのTwiceメソッドは名前は同じだけれども引数の型が違うので別のメソッドとみなされるわけです。
なお、引数の名前は考慮されないので注意してください。引数の名前だけ違うメソッドを作ると同じメソッドとみなされてエラーになります。引数で重要なのは数と型と順番です。名前はVBにとってどうでもよいのです。
このメソッド・プロパティを特定する要素である「メソッド・プロパティの名前」、「引数の型」、「引数の順番」の組み合わせのことを「シグネチャ」と呼びます。エラーメッセージやドキュメントにはこの「シグネチャ」という言葉がしばしば登場して、「シグネチャが同じメンバは定義できない」などと表現されますので覚えておいてください。
また、シグネチャには戻り値の型は含まれていません。つまり戻り値の型だけしか違いがない場合は多重定義できません。
さて、このように特に何も考えなくても素直に書いて行くことでいくらでも同じ名前のメソッドが作れるのですが、やはり同じ名前のメソッドが複数あるとプログラム時に混乱を生む場合があります。
そこで、少しでもこのような混乱を少なくするためにOverloadsキーワード(読み方:Overloads = オーバーロード)が用意されています。
Overloadsはメソッドまたはプロパティが多重定義されていることを示すキーワードで単なる目印に過ぎません。ただし、1つのメソッド・プロパティにOverloadsをつけた場合は、同じ名前 で多重定義されている他のメソッド・プロパティにもOverloadsをつける必要があります。
これで多少はプログラムに目印がついて可読性・メンテナンス性が向上しますので多重定義する場合はできるだけOverloadsをつけましょう。後で修正したときに思わぬバグを生む危険が低減されます。
これだけの意味しかない目印代わりのキーワードなんていらない気がしますか?でも、ちょっと想像してみてください。あなたはプログラムを修正しなければなりません。そのプログラムはとても長いプログラムです。あなたは修正する必要があるメソッドを発見して無事に修正を終えました。さて、ひょっとしてそのメソッドは多重定義されていないでしょうか?もし多重定義されていたら他にも修正しなければならない箇所があるかもしれません。このときOverloadsがついていれば他にもありますよという目印になりますよね。
上述のTwiceメソッドにOverloadsをつけると次のとおりになります。
Public
Overloads
Function Twice(ByVal
X As
Integer)
As
Integer Return X * 2 End Function |
Public
Overloads Function
Twice(ByVal
X As
Double)
As
Double |
■リスト13:多重定義の例。2つのメソッドは名前が同じだが引数の型が異なるので多重定義できる。
多重定義されたメソッドの呼び出し側のプログラムも考えて見ます。次のコードを見てください。
Dim i
As
Integer = 100 Dim d As Double = 40 d = Twice(i) MsgBox(d) |
■リスト14:多重定義されたメソッドの呼び出し例
ここで呼び出しているTwiceメソッドがInteger版であるかDouble版であるかわかりますか?
答えはInteger版です。さきほど説明したようにVBはメソッドの名前と引数の型、引数の順番でメソッドを識別します。上記の呼び出しはTwiceという名前のメソッドをInteger型の1つの引数で呼び出していますからInteger版が呼び出されるわけです。戻り値はIntegerからDoubleに自動変換されます。
では、次の場合はどうでしょうか?
Dim dec1
As
Decimal = 100 Dim dec2 As Decimal dec2 = Twice(dec1) MsgBox(dec2) |
■リスト15:多重定義されたメソッドの呼び出し例。自動型変換が行われる。
この場合は引数にDecimal型の値を渡していますからInteger版にもDouble版にも当てはまりません。結論から言うとこのような呼び出しを行った場合はDouble版が呼び出されます。これはVBができるだけ値を変更しないで型を変換しようとした結果です。
このような自動的な変換はわかりにくいので嫌われることが多いです。本当に自動的な変換をやめたいならばOption Strict Onを使用することになります。
さしあたって呼び出されるメソッドを明示的に制御したいのであれば明示的な型変換を行います。次の例では明示的な型変換を利用してInteger版のTwiceメソッドを呼び出します。
Dim dec1
As
Decimal = 100 Dim dec2 As Decimal dec2 = Twice(CInt(dec1)) MsgBox(dec2) |
■リスト16:明示的に型を指定して多重定義されたメソッドを呼び出す例。
もう1つの多重定義の場合として同じ内容だけれども違う形式の引数の場合を考えて見ましょう。
次のプログラムはフォームの左上の方にでっぱった四角形を描画します。
Private
Sub Form1_Paint(ByVal
sender As Object,
ByVal e
As
System.Windows.Forms.PaintEventArgs)
Handles
Me.Paint Dim Rect As Rectangle Rect.X = 10 Rect.Y = 20 Rect.Width = 100 Rect.Height = 100 Call DrawEdge(e.Graphics, Rect) End Sub |
'■DrawEdge ''' <summary>でっぱった四角形を描画します。</summary> ''' <param name="g">描画に使用するGraphicsクラスを指定します。</param> ''' <param name="Rect">描画する範囲を指定します。</param> ''' <remarks></remarks> Private Sub DrawEdge(ByVal g As Graphics, ByVal Rect As Rectangle) ControlPaint.DrawButton(g, Rect, ButtonState.Normal) End Sub |
■リスト17:フォームの左上の方にでっぱった四角形を描画する例
注目していただきたいのはDrawEdgeメソッドです。このメソッドには描画する範囲を指定するためにRectangle型の引数を用意しました。Rectangle構造体は四角形の座標を表現する構造体で、このように範囲を表す場合にはよく使用します。
しかし、描画する範囲を指定する方法はRectangle構造体だけではありません。Left, Top, Width, Heightの4つの値を使用してもできますし、左上と右下の2点を使用する方法もあります。 他にも何通りも考えられますが代表的な3つの方法を表にまとめてみます。
描画範囲を指定する方法 | 引数の形式 |
範囲を四角形で指定 | 1つのRectangle構造体 |
範囲の左上のX座標とY座標、および範囲の縦幅、横幅を指定 | 4つのInteger型 |
範囲の左上の点と右上の点を指定 | 2つのPoint構造体 |
■表3:描画範囲の代表的な指定方法
どの方法でも引数の形式は異なりますが描画範囲の指定という内容は同じです。 このように同じ内容をいくつかの形式で指定できる場合にはメソッドを多重定義して好きな方式で指定できるようにしておくとその機能を使うプログラマには便利でしょう。その場その場にあった一番やりやすい形式を指定してよいと言うことですから柔軟なプログラムが可能になります。
さっそく例としてDrawEdgeメソッドの多重定義を紹介しましょう。
Private
Sub Form1_Paint(ByVal
sender As Object,
ByVal e
As
System.Windows.Forms.PaintEventArgs)
Handles
Me.Paint Call DrawEdge(e.Graphics, 10, 20, 100, 100) End Sub |
'■DrawEdge ''' <summary>でっぱった四角形を描画します。</summary> ''' <param name="g">描画に使用するGraphicsクラスを指定します。</param> ''' <param name="Rect">描画する範囲を指定します。</param> ''' <remarks></remarks> Private Overloads Sub DrawEdge(ByVal g As Graphics, ByVal Rect As Rectangle) ControlPaint.DrawButton(g, Rect, ButtonState.Normal) End Sub |
'■DrawEdge ''' <summary>でっぱった四角形を描画します。</summary> ''' <param name="g">描画に使用するGraphicsクラスを指定します。</param> ''' <param name="Left">描画する範囲の横位置</param> ''' <param name="Top">描画する範囲の縦位置</param> ''' <param name="Width">描画する範囲の幅</param> ''' <param name="Height">描画する範囲の高さ</param> ''' <remarks></remarks> Private Overloads Sub DrawEdge(ByVal g As Graphics, ByVal Left As Integer, ByVal Top As Integer, ByVal Width As Integer, ByVal Height As Integer) Dim Rect As Rectangle Rect.X = Left Rect.Y = Top Rect.Width = Width Rect.Height = Height Call DrawEdge(g, Rect) End Sub |
'■DrawEdge ''' <summary>でっぱった四角形を描画します。</summary> ''' <param name="g">描画に使用するGraphicsクラスを指定します。</param> ''' <param name="Point1">描画する範囲の左上を表す点</param> ''' <param name="Point2">描画する範囲の右下を表す点</param> ''' <remarks></remarks> Private Overloads Sub DrawEdge(ByVal g As Graphics, ByVal Point1 As Point, ByVal Point2 As Point) Dim Rect As Rectangle Rect.X = Point1.X Rect.Y = Point1.Y Rect.Width = Point2.X Rect.Height = Point2.Y Call DrawEdge(g, Rect) End Sub |
■リスト18:DrawEdgeメソッドの多重定義
この例ではどのオーバーロード(多重定義されたメソッド)を呼び出しても実際に描画を行うのはRectangle版のDrawEdgeメソッドになるようになっています。
同じ機能で複数の多重定義を作成する場合はこのように実際の機能を持っているのは1つだけで、他のオーバーロードからはこのメソッドを呼び出すように構成するのが普通です。
このようにしないと同じ内容だけでちょっとずつ違うプログラムがあっちこっちに存在することになってしまい、どこかでバグは発生したり、修正する必要があったりする場合に大変です。
一箇所にまとめておけばバグであろうが機能変更であろうがその一箇所を修正するだけで済みます。
多重定義の最後のパターンは省略可能な引数の組み合わせが複数存在する場合です。「省略可能な引数」というところがOptionalを使った場合とかぶっていて、実際のところOptionalでもオーバーロードでもどちらを使っても実現できると言う場合もあります。
さきほどのDrawEdgeメソッドにもう一度登場してもらいます。今度は背景色の指定ができるようにしてみましょう。オーバーロードがたくさんあると説明がやりにくいので前の例より少し省きます。
Private
Sub Form1_Paint(ByVal
sender As Object,
ByVal e
As
System.Windows.Forms.PaintEventArgs)
Handles
Me.Paint Dim Rect As Rectangle Rect.X = 10 Rect.Y = 20 Rect.Width = 100 Rect.Height = 100 Call DrawEdge(e.Graphics, Rect, Color.CornflowerBlue) End Sub |
'■DrawEdge ''' <summary>でっぱった四角形を描画します。</summary> ''' <param name="g">描画に使用するGraphicsクラスを指定します。</param> ''' <param name="Rect">描画する範囲を指定します。</param> ''' <remarks></remarks> Private Overloads Sub DrawEdge(ByVal g As Graphics, ByVal Rect As Rectangle) Call DrawEdge(g, Rect, Me.BackColor) End Sub |
'■DrawEdge ''' <summary>でっぱった四角形を描画します。</summary> ''' <param name="g">描画に使用するGraphicsクラスを指定します。</param> ''' <param name="Rect">描画する範囲を指定します。</param> ''' <remarks></remarks> Private Overloads Sub DrawEdge(ByVal g As Graphics, ByVal Rect As Rectangle, ByVal BackColor As Color) ControlPaint.DrawButton(g, Rect, ButtonState.Normal) Rect.Inflate(-2, -2) g.FillRectangle(New SolidBrush(BackColor), Rect) End Sub |
■リスト19:多重定義を使った省略可能引数の表現例
この2つのオーバーロードは背景色を指定する引数があるかないかが異なります。背景色の引数がない方のオーバーロードを呼び出すと背景色には自動的にフォームの背景色(=Me.BackColor)が指定されます。同じことはOptionalを使ってもできます。
さて、このように多重定義にはいろいろなパターンがあり中には多重定義ではなくOptionalを使って代用できる場合もあります。
多重定義は便利な機能なのですがこのような事情から無計画にどんどんオーバーロードして行くとどこでなにをやっているのやら、どこがどこを呼び出しているのやら混乱してしまうことになります。多重定義はここぞというときに使うようにしましょう。
また、OverloadsとOptionalを同時に使うとかなりわかりにくいプログラムになります。これは絶対やめておいた方がいいです。
次に共有メンバの作成方法を説明します。共有メンバとはインスタンス経由ではなくクラスから直接よびだされるメソッドやプロパティのことです。たとえば、プログラムを開始した位置をフルパスで取得できるApplicationクラスのStartupPathプロパティは共有メンバなので次のようにインスタンスを経由せずに呼び出すことができます。
Dim
Path As
String Path = Application.StartupPath |
■リスト20:共有メンバの呼び出し例
もし、StartupPathプロパティが非共有メンバであれば必ずインスタンス経由で呼び出す必要があるので次のようにプログラムする必要があったでしょう。
'このコードは実際に動作しません。 Dim App As New Application Dim Path As String Path = App.StartupPath |
■リスト21:インスタンス経由での共有メンバの呼び出し例。Applicationクラスの制約により実際には動作しない。
このような共有メンバを作成するのは簡単で、メソッドやプロパティを宣言するときにSharedキーワード(読み方:Shared = シェアード)を追加するだけです。
次の例では自作のクラスにWindowsフォルダのパスを表す読み取り専用のWindowsPathプロパティを記述します。
Public
Class WindowsInformation ''' <summary>Windowsフォルダのパスを取得します。</summary> Public Shared ReadOnly Property WindowsPath() As String Get Return Environment.GetEnvironmentVariable("WINDIR") End Get End Property End Class |
■リスト22;共有メンバの作成例
このプロパティはSharedで宣言されているので共有メンバです。ですから呼び出し側ではインスタンスを経由せずに次のように直接呼び出すことができます。
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As
System.EventArgs) Handles Button1.Click Dim Path As String Path = WindowsInformation.WindowsPath MsgBox(Path) End Sub |
■リスト23:自作の共有メンバの呼び出し
共有メンバはインスタンス経由でも呼び出すことができますがインスタンス経由での呼び出しは推奨されておらず、VB2005では緑の波線で警告が表示されます。
VB2005を使っている方もそうでない方もためしにインスタンス経由でWindowsPathプロパティを呼び出すコードを記述してみてください。
Dim
Info As New
WindowsInformation Dim Path As String Path = Info.WindowsPath MsgBox(Path) |
■リスト24:インスタンス経由での共有メンバの呼び出し
推奨されていないにもかかわらずこのコードが正常に動作することが確認できます。
この記述が推奨されていないのには2つの理由があります。1つは共有メンバがすべてのインスタンスで共有しているメンバであるにもかかわらずインスタンス経由で呼び出すということはあたかも特定のインスタンス専用のメンバのように見えてしまうという点です。インスタンス経由で呼び出しても共有メンバの動作が変わるわけではないのですがスタンスとして推奨されないということです。
もう1つの理由はちょっと小難しいので具体例で説明します。
まず、次の例を実行してコードが正常に動作するのを確認してください。
Private Sub
Button1_Click(ByVal sender
As System.Object,
ByVal e As System.EventArgs)
Handles Button1.Click Dim Info As WindowsInformation Dim Path As String Info = GetInfo() 'インスタンスをメソッドから取得 Path = Info.WindowsPath MsgBox(Path) End Sub |
Private Function GetInfo()
As WindowsInformation MsgBox("GetInfoが呼び出されました。") Return New WindowsInformation End Function |
■リスト25:メソッドの戻り値に由来するインスタンス経由での共有メンバの呼び出し
確認できたら、少し修正します。
Info = GetInfo()
Path = Info.WindowsPath
という記述は無駄なように思えるので、この部分を1行にまとめて次の通りにします。
Path = GetInfo.WindowsPath
つまり、全体では次のようになります。
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As
System.EventArgs) Handles Button1.Click Dim Info As WindowsInformation Dim Path As String Path = GetInfo.WindowsPath MsgBox(Path) End Sub |
Private Function GetInfo()
As WindowsInformation MsgBox("GetInfoが呼び出されました。") Return New WindowsInformation End Function |
■リスト26:メソッドが実行されない注意すべき共有メンバの呼び出し例
これを実行すると驚くべきことに「GetInfoが呼び出されました。」というメッセージが表示されなくなります。つまりGetInfoメソッドを呼び出しているにもかかわらずGetInfoメソッドが実行されないのです。
これはインスタンス経由で呼び出そうがクラスから直接呼び出そうが共有メンバの動作は同じなので、プログラマがインスタンス経由で共有メンバを呼び出しているにもかかわらずVBが気を利かせて直接共有メンバを呼び出すために起こる現象です。
つまり、GetInfoメソッドを実行しなくてもWindowsPathプロパティの実行が可能であるためVBの最適化機能によってGetInfoメソッドの実行が省略されてしまったのです。
あまり良くあるケースではありませんが、このような事情があるため推奨どおりインスタンス経由での共有メンバの呼び出しはやめておいた方が無難です。
さて、共有メンバはSharedをつけるだけで簡単に作成できるのですが、共有メンバの内部のプログラムには少し非共有メンバのプログラムとは違う特徴があります。
共有メンバの内部ではインスタンスを経由しないと非共有メンバを呼び出せないのです。たとえば次のプログラムはエラーです。
Public
Class WindowsInformation Public Count As Integer ''' <summary>Windowsフォルダのパスを取得します。</summary> Public Shared ReadOnly Property WindowsPath() As String Get Count += 1 '←ここで非共有メンバを参照しているのでビルドエラーになります。 Return Environment.GetEnvironmentVariable("WINDIR") End Get End Property End Class |
■リスト27:共有メンバの内部で非共有メンバを呼び出そうとするとビルドエラーになる
これは予想通りの動作でしょう。非共有メンバはインスタンスの数だけ別々に存在するので共有メンバから非共有メンバを参照しようとしてもどのインスタンスの変数だか特定できないからです。
インスタンスを経由すれば共有メンバからでもどのインスタンスのメンバであるか特定できるので非共有メンバの呼び出しが可能ですが、ほとんどの場合意味がありません。インスタンスの非共有メンバを使用する必要があるならそのメンバは事実上共有メンバではないからです。
とは言え文法上はこのようなことが可能です。すぐには思いつきませんが何かのときに使うかもしれないので紹介しておきます。もう一度断っておきますがこの例は文法上は正しいですが何の意味もありません。
Public
Class WindowsInformation Public Count As Integer ''' <summary>Windowsフォルダのパスを取得します。</summary> Public Shared ReadOnly Property WindowsPath() As String Get Dim Info As New WindowsInformation Info.Count += 1 '←意味はないがインスタンス経由なら非共有メンバを参照できる。 Return Environment.GetEnvironmentVariable("WINDIR") End Get End Property End Class |
■リスト28:共有メンバの内部でもインスタンス経由なら非共有メンバが参照できるがあまり意味はない。
このクラスの例で行くと強引にインスタンスを作るのではなくCount変数もSharedで宣言すればお互いに共有メンバということになるので参照可能です。こちらの方はよく使うでしょう。
Public
Class WindowsInformation Public Shared Count As Integer ''' <summary>Windowsフォルダのパスを取得します。</summary> Public Shared ReadOnly Property WindowsPath() As String Get Count += 1 Return Environment.GetEnvironmentVariable("WINDIR") End Get End Property End Class |
■リスト29:共有メンバの内部で共有メンバを呼び出すのは問題ない。
なお、非共有メンバから共有メンバの参照は自由に行うことができます。
最後に静的変数を紹介します。静的変数は適用範囲から離れても値を保ち続ける変数です。
これもわかりにくいので具体例を挙げて説明します。
次のDescriberクラスのNextNumberメソッドの内部ではNumber変数が静的変数として宣言されています。静的変数を宣言するにはStaticキーワード(読み方:Static = スタティック)を使用します。
Number変数の値が保存されるためNextNumberメソッドは呼び出されるごとにカウントアップした値を返します。
Public
Class Describer Public Function NextNumber() As Integer Static Number As Integer Number += 1 Return Number End Function End Class |
■リスト30:静的変数の例。この例ではNumber変数が静的変数として宣言されている。
呼び出し例と次の通りです。
Dim
Desc As New
Describer MsgBox(Desc.NextNumber) '1 MsgBox(Desc.NextNumber) '2 MsgBox(Desc.NextNumber) '3 |
■リスト31:静的変数の値が保持されているのが確認できる
この例では順に「1」、「2」、「3」と表示されます。もし、Number変数がStaticではなく普通にDimで宣言されていたらこの呼び出しでは「1」、「1」、「1」と表示されることになります。
静的変数はメンバの内部にのみ記述できます。また、上記の例ではNumber変数は非共有メンバの内部に記述されているためインスタンスごとに異なる値を保持します。
共有メンバの中に静的変数を記述するとプログラムの実行が終了するまでずっと値を保持し続ける変数ということになります。
なお、上述の例はStaticを使って静的変数として宣言しなくてもクラスのメンバとして宣言すれば同じことが実現できます。
Public
Class Describer Dim Number As Integer Public Function NextNumber() As Integer Number += 1 Return Number End Function End Class |
■リスト32:静的変数にしなくてもクラスレベルの変数にすれば値は保存される。
この例を改造してNumber変数をSharedで宣言すればNextNumberメソッドはすべてのインスタンスに共通してカウントアップを行う機能ということになります。
Public
Class Describer Shared Number As Integer Public Function NextNumber() As Integer Number += 1 Return Number End Function End Class |
■リスト33:クラスレベルの共有メンバにすればすべてのインスタンス経由で同じ値を保存することになる。
呼び出し側のコードも改造して複数のインスタンスを通じてカウントアップを行っていることを確認してみてください。
Dim
Desc1 As New
Describer Dim Desc2 As New Describer MsgBox(Desc1.NextNumber) '1 MsgBox(Desc1.NextNumber) '2 MsgBox(Desc1.NextNumber) '3 MsgBox(Desc2.NextNumber) '4 MsgBox(Desc2.NextNumber) '5 MsgBox(Desc2.NextNumber) '6 |
■リスト34:複数のインスタンス間でメンバ変数が共有され、値も保存されていることが確認できる。