Visual Basic 初級講座 [改訂版] |
Visual Basic 中学校 > 初級講座[改訂版] >
2020/8/23
この記事が対象とする製品・バージョン
Visual Basic 2019 | ◎ | 対象です。 | |
Visual Basic 2017 | ◎ | 対象です。 | |
Visual Basic 2015 | ◎ | 対象です。 | |
Visual Basic 2013 | ◎ | 対象です。 | |
Visual Basic 2012 | ◎ | 対象です。 | |
Visual Basic 2010 | ◎ | 対象です。 | |
Visual Basic 2008 | ◎ | 対象です。 | |
Visual Basic 2005 | ◎ | 対象です。 | |
Visual Basic.NET 2003 | △ | 対象外ですがほとんどの説明があてはまるので参考になります。 | |
Visual Basic.NET (2002) | △ | 対象外ですがほとんどの説明があてはまるので参考になります。 | |
Visual Basic 6.0 | × | 対象外です。 |
目次
初級講座ではこれまでいろいろなサンプルを通してたくさんのメソッドを使用してきました。MsgBoxやToStringやDrawEllipseやWriteLineなどです。
今回は自分でメソッドを作成する方法を説明します。
メソッドを自作すると、1つの長いプログラムを複数の小さなプログラムに分割してシンプル化できますし、プログラムの中で同じ処理が何回も出てくる場合は、メソッドを呼び出すだけで済むようになり効率的です。
単純なメソッドを作るのは難しくありません。次のように記述するだけです。
この例では Add というメソッドを定義しており、引数には2つの数値 x と y を取ります。それらを合計した値を返します。
''' <summary> ''' 足し算します。 ''' </summary> Public Function Add(x As Integer, y As Integer) As Integer Return x + y End Function |
これをどこに書けば良いかと言うと クラス(Class) か モジュール(Module) か 構造体(Structure) の中に書きます。
クラスの作り方は次回説明しますが、プログラムの作成を開始した時点で、既に1つVisual Studioにより作成されています。 Windowsフォームアプリケーションの場合、Form1というクラスが最初からありますし、コンソールアプリケーションの場合ProgramモジュールかModuleモジュールが最初から含まれているので、この中にメソッドを記述するのが手軽です。どのクラスにどのメソッドを作成するかはアプリケーションを設計する上で重要なのですが、今回はメソッド作成のはじめなのでいまそこにあるクラスを使うことにしましょう。
Windowsフォームアプリケーションの場合、上記の Add メソッドを組み込むと次のようになります。
Public Class
Form1 ''' <summary> ''' 足し算します。 ''' </summary> Public Function Add(x As Integer, y As Integer) As Integer Return x + y End Function End Class |
''' からはじまるコメントは XMLコメント という特別なコメントで、プログラマー向けのヒントを提供します。
XMLコメントは最後に入力すると楽です。というのも、Visual Studioで ''' のように ' を3個連続で入力すると自動的に状況に応じた雛形を自動生成してくれるからです。
XMLコメントはあくまでコメントなので、省略することもできます。間違った書き方をするとVisual Studioが警告を表示する場合がありますが、それでもプログラムがエラーで実行できなくなるということはありません。
メソッドの 呼び出し方は自作のメソッドであっても、既定で存在するのフレームワークのメソッドと変わりません。
ためしにButton1_Clickから呼び出すプログラムを書くと次のようになります。
Public Class Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Dim result As Integer = Me.Add(3, 4) MsgBox(result) End Sub ''' <summary> ''' 足し算します。 ''' </summary> Public Function Add(x As Integer, y As Integer) As Integer Return x + y End Function End Class |
Me. というキーワードは「同じインスタンス内の」という意味です。このキーワードは省略可能ですが、Me. と入力すると入力可能なキーワードの一覧が表示される点が便利なので、私は記述することが多いです。
それから、ヒントとしてXMLコメントに入力した「足し算します。」というコメントが表示されているのもポイントです。XMLコメントとして訳に立つ情報を書いておけばプログラムが格段にやりやすくなります。
この画像で Add 以外に並んでいる選択肢は Form1 がもともともっているフォームのメンバーです。Form1はSystem.Windows.Forms.Formクラスを継承しているため、何もプログラムしていない状態でもFormクラスのメンバーが実装されている状態になっています。継承については別途詳しく説明しますが、このように他のクラスが持っている機能を引き継いで、自分が欲しいメソッドだけ追加したり、場合によっては引き継いだメンバーの機能を変更したりできるのでとても強力な機能です。
ところで「関数」という言葉もあり、メソッドの別名のように使われます。他にも「プロシージャ」という言葉を使うときもあります。
少し言葉を整理しておきましょう。これらの言葉はとてもよく似ています。
プロシージャとは何らかの「手順」という意味です。もっと親しみやすい言い方をするとプログラムの「処理」の集合に名前をつけてクラスまたは構造体のメンバーとしたものと思えばだいたい合っています。メソッド・プロパティ・コンストラクターはプロシージャです。他にも演算子の処理を記述するプロシージャもあります。メソッド・プロパティ・コンストラクターの意味については初級講座第6回 クラスの使い方 で説明していますのでここでは繰り返しません。
メソッドのうち、戻り値を返すもののことを特に「関数」と呼びます。英語の発音を使ってFunction(ファンクション)という場合も多いです。
これらの言葉は別のプログラミング言語では少し意味が違う場合があるので、話をしている相手は厳密にこの定義にしたがって言葉を使うとは限りません。だいたいの言葉の意味を押さえた上で、文脈に応じて理解するようにしてください。
たとえば、戻り値のあるメソッドを関数、戻り値のないメソッドをプロシージャと呼ぶ人もいます。 |
上の図で示した定義はマイクロソフトの公式ドキュメントに即しています。(このドキュメントではプロパティについてはGet, Setをプロシージャとしてあげていますが、図ではPropertyでまとめました。)
参考:Visual Basic におけるプロシージャ
https://docs.microsoft.com/ja-jp/dotnet/visual-basic/programming-guide/language-features/procedures/
戻り値のないメソッドの書き方は次の通りです。
赤い部分は必ず記述する必要があります。
Public Sub メソッド名(パラメーターリスト)
メソッドの内容
End Sub
メソッドの構文はいろいろなオプションがあるのでこれはほんの第一歩です。
パラメーターリストにはこのメソッドがどのような引数を受け取るかを記述します。後でまとめて説明します。
Publicはこのメソッドがどこから呼び出せるかというアクセスレベルを表しています。アクセスレベルについては次回説明します。
次のメソッドはHelloと出力するだけのシンプルなメソッドです。→ Debug.WriteLineで出力される場所
Public Sub Test() Debug.WriteLine("Hello!") End Sub |
このメソッドを呼び出すには次のように記述します。
Me.Test() |
Me. と ( ) は省略可能なので単に Test と書いても呼び出せます。( ) を省略するとVisual Studioが自動的に ( ) を付けます。
次のメソッドは受け取った文字列型の値を出力します。→ Debug.WriteLineで出力される場所
Public Sub Test(value
As String) Debug.WriteLine(value) End Sub |
このメソッドを呼び出すには次のように記述します。Me. は省略可能です。
Me.Test("OK!") |
少し長い例も紹介します。
次のメソッドは、C:\temp というフォルダーが存在すればその中に、VBLogTest.txt というファイルを作成し引数に渡されたログを出力します。
Public Class
Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Me.WriteLog("ログのテスト") Me.WriteLog("本日は晴天なり") End Sub Public Sub WriteLog(message As String) If Not IO.Directory.Exists("C:\temp") Then Return End If 'IO.File.AppendAllText("C:\temp\VBLogTest.txt", String.Format("{0} {1}{2}", Now, message, vbNewLine)) '←VB2013以前 IO.File.AppendAllText("C:\temp\VBLogTest.txt", $"{Now} {message}{vbNewLine}") End Sub End Class |
このメソッドでは C:\temp フォルダーが存在しなければ、書き込み命令を実行せずにメソッドの実行を終了します。それを実現しているのが Returnステートメント(読み方:Return = リターン)です。
Returnに実行が到達するとメソッドの実行はその時点で終了し呼び出し元に処理が戻ります。
Returnの代わりに Exit Sub というキーワードを使っても同じ効果があります。
しかし、Exit Subは1998年以前のVisual Basic(VB6)との互換性のためにあるキーワードであって、21世紀の今新しいプログラムを作るときには使うべきではありません。
Returnであればこの後説明する戻り値があるメソッドでも共通で使用できて便利ですし、C#など他の言語でもこの機能に return というキーワードを割り当てている場合も多く融通が利きます。
戻り値のあるメソッドの書き方は次の通りです。
赤い部分は必ず記述する必要があります。(厳密には Option Strict Offの場合、As 戻り値の型 は省略可能で、これを省略すると As Objectとみなされます)
Public Function メソッド名(パラメーターリスト) As 戻り値の型
メソッドの内容
Return 戻り値の値
End Function
パラメーターリストにはこのメソッドがどのような引数を受け取るかを記述します。後でまとめて説明します。
Publicはこのメソッドがどこから呼び出せるかというアクセスレベルを表しています。アクセスレベルについては次回説明します。
Return(読み方:Return = リターン)を記述する場所は、メソッドの一番最後である必要はなく、メソッドの中のどの行でも構いません。
ただし、Returnが実行された時点でメソッドの処理は終了し、呼び出し側に処理が戻ります。
次のメソッドは必ず 627 を返すメソッドです。練習以外の役には立ちません。
Public Function MyFunc()
As Integer Return 627 End Function |
このメソッドを呼び出して、戻り値を出力するには次のように記述します。→ Debug.WriteLineで出力される場所
Me. は省略可能です。
Dim returnValue
As Integer =
Me.MyFunc() Debug.WriteLine(returnValue) |
次のメソッドは文字列型の引数をさかさまにして返します。たとえば「あいうえお」を渡すと「おえういあ」を返します。
Public Function MyFunc(value
As String)
As String Return Strings.StrReverse(value) End Function |
このメソッドを呼び出して、戻り値を出力するには次のように記述します。→ Debug.WriteLineで出力される場所
Me. は省略可能です。
Dim returnValue
As String =
Me.MyFunc("あいうえお") Debug.WriteLine(returnValue) 'おえういあ と出力されます。 |
この例ではStringsモジュールの StrReverseメソッド (読み方:StrReverse = エスティーアールリバース)を使用しています。Stringsモジュールは(C#など他の.NET対応言語から呼び出すこともできますが)VB用に設計されたモジュールで、VBではとても簡単に呼び出せるのが特徴です。 しかし、C#など他の.NET言語のプログラマーが Stringsモジュールを使うことはほとんどありあません。StrReverseの場合は、その代わりにReverseメソッド(読み方:Reverse = リバース)を使用します。たとえば、この例では MyFunc内は Return value.Reverse() と書いても同じ結果になります。 このReverseメソッドはVB2008以上でないと使用できないため、この例では昔からあるStrings.StrReverseの方を使用しました。 |
途中でReturnする例も紹介しておきます。
この例のメソッド GetAmari はわり算を実行して あまりを返します。
Return が2つある点に注目してください。
''' <summary> ''' x ÷ y を実行して あまり を返します。 ''' 例 GetAmari(13, 4) は 13 ÷ 4 = 3 … 1 なので、1 を返します。 ''' </summary> ''' <param name="x">わられる数</param> ''' <param name="y">わる数</param> ''' <returns>あまり</returns> Public Function GetAmari(x As Integer, y As Integer) As Integer If y = 0 Then Return 0 End If Dim amari As Integer Dim kotae As Integer = Math.DivRem(x, y, amari) Return amari End Function |
1つ目のReturnは Return 0です。わり算では 0 でわることはできないので y が 0 の場合は、わり算を実行しないであまり 0 を返します。(エラーにする実装の方が良いかもしれませんがここでは説明の都合のためそうしています。)
このReturn 0 が実行されるとGetAmariの処理はここで終了し、ここから下を実行せずに呼び出し元に戻っていきます。
Returnの代わりにメソッド名を使って戻り値を指定する前世紀の遺物となっている古い書き方があります。念のため紹介しておきますが、これらの書き方は古いVB6との互換性のために存在するだけなので、今では使わないほうが良いです。
この書き方を使うと、たとえば、下記のように Return しているメソッドを、
Public Function MyFunc()
As Integer Return 627 End Function |
このように書き換えることができます。
Public Function MyFunc()
As Integer MyFunc = 627 End Function |
どちらも同じ意味です。
この書き方は、メソッド名を変数のように使うことができます。
次のように記述しても 627 が返ります。
Public Function MyFunc()
As Integer MyFunc = 600 MyFunc += 27 End Function |
Exit Functionと記述して途中でメソッドの実行を終了することもできます。
Exit Functionが実行されるか、メソッドの末端に到達した時点でその「メソッド名のような変数」に代入されていた値がそのメソッドの戻り値になるというわけです。
メソッドの宣言のカッコの中にはそのメソッドが受け取ることができるパラメーターを定義します。パラメーターには名前と型を指定し、引数名 As 型 と記述します。複数の引数を受け取る場合は , (カンマ) で区切ります。
参考
パラメーターリスト (Visual Basic)
https://docs.microsoft.com/ja-jp/dotnet/visual-basic/language-reference/statements/parameter-list
例1:String型の引数 value を受け取るMyMethodメソッド
Public Sub MyMethod(value As String) |
例2:Integer型の引数 number を受け取る MyMethodメソッド
Public Sub MyMethod(number As Integer) |
例3:String型の引数 value と Integer型の引数 number を受け取る MyMetho メソッド
Public Sub MyMethod(value As String, number As Integer) |
パラメーターはもっとたくさん定義することもできますが、あまり大量のパラメーターを定義すると使いこなすほうが大変なので、通常は多くても3つか4つくらいの数にとどめておくのが良いです。大量の情報の受け渡しが必要であれば専用の構造体を別途定義して、メソッドの引数にはその構造体を1つ渡せばよいようにするという手段を採ります。
なお、 パラメーターの最大数が明確に定義されている仕様書を発見できませんでしたが、20個や30個程度は使用できます。
発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。 キーワード ParamArray (読み方:ParamArray = パラムアレイ)を使用すると、パラメータの数を動的に変化させることができます。 たとえば、次のように使用します。
ParamArrayを使うとパラメーターを配列にする必要があります。この例ではnumbers As Integer ではなく、numbers() As Integer のように () が付いている点に注意してください。配列については初級講座でまだ扱っていませんが、この () がついているのが配列の証です。 呼び出し側は、そのようなことを気にする必要はなく、好きな数の引数を与えることができます。
|
発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。 パラメーターがたくさんあるメソッドはそれだけたくさんテストする必要があります。たとえば、数値型の引数を1つだけ取るメソッドは、その引数にいろいろな値を与えて正しく動作するか確認することになります。「いろいろな値」が具体的になんであるかはメソッドの仕様によって異なります。普通は少なくとも、3、4個程度のテストが必要なはずです。(境界値・中間値・異常値あたり) パラメーターが2つになると、それぞれのパラメーターの組み合わせが発生するのでテストケースは 2倍ではなく、2乗になります。つまり、単純に考えると9個から16個の組み合わせをテストする必要があります。パラメーターが3つだと3乗です。27個から64個の組み合わせのテストが必要になります。 パラメーターが増えると指定できる内容が増えるので、テストも増える、そうすると想像しなかったようなパラメーターの組み合わせでバグが起きるかもしれないので、いろいろな組み合わせのテストが必要になるということです。この理屈はパラメーターに限った話ではないのですが、メソッドのテストは、ソフトウェアのテストの中では最も粒度が細かい単体テストと呼ばれるところであり、他のテストの前提になってきます。便利便利と実際には使わないパラメーターをどんどん増やしてソフトウェアを複雑にしないことをお勧めします。 |
パラメーターはメソッドの中でローカル変数のように使用できます。ずばりパラメーターはローカル変数であると書いてあるドキュメントもあります。
なので、パラメーターを計算に使用したり、値を代入したりといった操作が可能です。
しかし、パラメーターに値を代入するプログラムは何をやっているのか後でわかりにくくなってしまうのでやらないことをお勧めします。
Public Sub MyMethod(value
As String, number
As Integer) 'パラメーターは計算に使用できます。 Dim vNumber As Integer = number * 2 'パラメーターに値を代入することもできます。(ただし、お勧めしません。) value = "ABC" 'Debug.WriteLine(String.Format("MyMethodが呼び出されました。value={0}, number={1}", value, number)) '←VB2013以前の場合 Debug.WriteLine($"MyMethodが呼び出されました。value={value}, number={number}") End Sub |
パラメーターの具体的な値は呼び出し時に引数として指定します。初級講座では既にいろいろなメソッドを呼び出しを実践しており、自作のメソッドであっても呼び出し方は変わりません。
たとえば、次のようにします。
Me.MyMethod("一条兼良", 123) |
この例では、第1引数に "一条兼良"、第2引数に 123 を渡しているので、対応するパラメーター value と number にはそれぞれ "一条兼良" と 123 が代入されます。
通常のメソッド同様に名前付きパラメーターとしても使用できます。
Me.MyMethod(value:="一条兼良", number:=123) |
引数は変数で渡すこともできます。
Dim kanpaku
As String =
"一条兼良" Dim bookCount As Integer = 123 Me.MyMethod(kanpaku, bookCount) |
値が渡っていく様子を模式的に示すと次のようになります。
メソッドの中でパラメーター value や number に値を代入しても呼び出し元の変数 kanpaku や bookCount は置き換わりません。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim author As String = "井伏鱒二" Test(author) Debug.WriteLine(author) '井伏鱒二 End Sub Private Sub Test(value As String) value = "ドストエフスキー" End Sub |
この例ではTestメソッド内でパラメーター value に "ドストエフスキー" を代入していますが、呼び出しもとの変数 author はこの影響を受けず、呼び出し後のDebug.WriteLineでは出力ウィンドウのデバッグに「井伏鱒二」と出力されます。→ Debug.WriteLineで出力される場所
この動作は ByRef キーワード (読み方:ByRef = バイレフ)で変更することができます。パラメーターの名前の前に ByRef を記述すると、そのパラメーターに値を代入すると呼び出しもとの変数の値も置き換わります。
次の例では、ドストエフスキーが出力されます。→ Debug.WriteLineで出力される場所
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim author As String = "井伏鱒二" Test(author) Debug.WriteLine(author) 'ドストエフスキー End Sub Private Sub Test(ByRef value As String) value = "ドストエフスキー" End Sub |
しかし、パラメーターを使って変数の値を置き換えるプログラムはわかりにくいので、ByRef を使用することはお勧めしません。私がレビューするプログラムに ByRef を発見したらほとんどの場合私は ByRef を使わないように書き換えるようコメントするでしょう。
たとえば、上の例で、Button1_Clickの開発者とTestメソッドの開発者が別々であることを想像してみてください。Button1_Clickの開発者はTestメソッドを呼び出すことで変数の値が変わってしまうことを予想できるでしょうか?優秀なプログラマーはパラメーター value の宣言に ByRef がついていることに気が付いて、値が変わる可能性を予見するかもしれませんが、そうでないプログラマーも多いはずです。また、ByRefを見れば注意が必要なことはわかりますが、どんな場合にどんな値に変わるのかは別途調べる必要があり難儀です。
メソッドが何か値を呼び出し元に渡したいのであれば戻り値を使うのが一般的です。上の例は次のように書く方がずっとわかりやすいプログラムになります。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim author As String = "井伏鱒二" Dim returnAuthor As String = Test(author) Debug.WriteLine(author) '井伏鱒二 Debug.WriteLine(returnAuthor) 'ドストエフスキー End Sub Private Function Test(value As String) As String Return "ドストエフスキー" End Function |
なお、ByRefに対し、ByVal (読み方:ByVal = バイバル)というキーワードもあります。これはパラメーターに値を代入しても呼び出し元には影響しないというデフォルトの動作を意味にするキーワードなので通常は省略します。古いバージョンのVBなどではVisual Studioが自動生成するプログラムにこの ByVal が含まれている場合があります。あってもなくても意味は同じです。
ByRefを使わない場合、パラメーターに値を代入しても呼び出し元に影響しませんが、パラメータの状態(プロパティなど)を変化させると呼び出し元にも影響します。
この理屈はなかなか難しく、おそらく初心者には理解できません。そこで、理屈がわからなくても困らない簡単なルールを下の方で紹介することにします。
まずは何が問題なのか説明しましょう。
たとえば、次のReddenメソッドはテキストボックスの背景色を赤くし、文字の色を濃い緑にします。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Redden(TextBox1) Redden(TextBox2) End Sub Private Sub Redden(targetTextBox As TextBox) targetTextBox.BackColor = Color.Salmon targetTextBox.ForeColor = Color.DarkGreen End Sub |
メソッドの中でパラメーターに対してプロパティを設定しているだけですが、この変更は呼び出し元に影響し、TextBox1とTextBox2のそれぞれの色が変化します。
もしかしたら、ちょっと混乱してきたかもしれないので、もう1回呼び出し元に影響しない例を紹介しておきましょう。
次のような書き方は呼び出し元に影響しません。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Redden(TextBox1) Redden(TextBox2) End Sub Private Sub Redden(targetTextBox As TextBox) Dim newTextBox As New TextBox newTextBox.BackColor = Color.Salmon newTextBox.ForeColor = Color.DarkGreen targetTextBox = newTextBox End Sub |
この例はパラメーター targetTextBox に値 newTextBox を代入しています。パラメーターへの値の代入は呼び出し元に影響しません。(ByRefが付いていれば影響します。)
一方、その前に紹介したほうの例はパラメーター targetTextBox に何かを代入しているわけではありません。パラメーター targetTextBox のプロパティに値を代入しているのです。パラメーター自体が変更されているわけではないので呼び出し元に影響します。
一体どんな理屈でこんな区別が発生しているのでしょうか?
引数をメソッドに渡すとき、Visual Basicは実際にはその引数のコピーを作成してメソッドに渡すか、または、引数の値が格納されているメモリー上の場所(アドレス)をメソッドに渡しています。どちらの場合でも、引数の値そのものがメソッドに渡されているわけではないというのがポイントです。
どちらの動作になるかはパラメーターの型によって決まります。IntegerやDateなどString以外のプリミティブ型と 構造体などは前者(コピーされる方)、それ以外StringやTextBoxなどは後者(メモリー上の場所を渡す方)です。この区別は値型が参照型かというもので、詳しくは別の機会に説明します。
前者の場合、メソッドが受け取っているのは所詮コピーなので値を代入しても呼び出し元には影響しないことは簡単に理解できます。
後者の場合、呼び出し元とメソッドは同じ実体を指しているので、プロパティを変更したり、メソッドを呼び出す場合、そのプロパティやメソッドは(コピーではなく)本物です。だからTextBoxのBackColorプロパティの値を変更すると呼び出し元も変化するというわけです。
しかし、メソッド側でパラメーターに独自の値を代入するという行為は、呼び出し元のメモリーは無傷でそのまま残り続け、それとは関係ないメモリー上になにか値を設定しているという行為なのです。ですから呼び出し元に何も影響しません。
newTextBoxの実体が新しく作成されるのは コンストラクターが呼び出される場合です。さきほどの例で New を使っている行がそれです。
以上が理屈の説明です。
ちゃんと理解して使いこなすのは初心者にはかなり難しいので、難しいことを考えなくていいように、簡単なルールを守るようにしましょう。
理屈がわからなくても困らない簡単なルール
ルール1.ByRef を使わない。
ルール2.メソッド内でパラメーターに値を代入しない。
この2つを守れば何も悩むことはありません。想像した通りに快適に動いてくれます。
企業向けの複雑なシステムやアプリケーションを作るプロの開発チームもこの2つのルールは常識的に使用しています。(よほど必要な場合は意図的にルールを破ることもありますが)
このルールのどちらかを破るときはここで説明したことを思い出して気をつけてプログラムしてください。
同じ名前のメソッドを複数定義することができます。
次のプログラムには WriteLog という名前のメソッドが2つ定義されていますが、有効です。
Public Class
Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Me.WriteLog("ログのテスト") Me.WriteLog("本日は晴天なり") End Sub ''' <summary> ''' 現在の日時でC:\temp\VBLogTest.txtにログを書き込みます。 ''' </summary> Public Sub WriteLog(message As String) WriteLog(Now, message) End Sub ''' <summary> ''' 指定した日時でC:\temp\VBLogTest.txtにログを書き込みます。 ''' </summary> Public Sub WriteLog(logDate As Date, message As String) If Not IO.Directory.Exists("C:\temp") Then Return End If 'IO.File.AppendAllText("C:\temp\VBLogTest.txt", String.Format("{0} {1}{2}", Now, message, vbNewLine)) '←VB2013以前 IO.File.AppendAllText("C:\temp\VBLogTest.txt", $"{logDate} {message}{vbNewLine}") End Sub End Class |
VBはメソッドを名前だけでなく、パラメーターの型・順序でも区別しています。(パラメーターの名前では区別しません。)
この例をよく見ると、1つ目のWriteLogメソッドはString型のパラメーターが1つなのに対し、2つ目のWriteLogメソッドはDate型とString型のパラメーターを取ります。だからVBはこの2つは違うメソッドであるとみなします。
このような違いを「シグネチャ」と呼びます。
まとめると、シグネチャが違うメソッドは名前が同じでもいくつでも定義できるということです。
シグネチャとはメソッドの名前・引数の型・引数の順番 および 型パラメーターの組み合わせのことです。
型パラメーターについてはまだ説明していないのでここでは割愛します。
たとえば、次のメソッドはシグネチャが異なります。(End Subは省略しています。)
Public Sub MyMethod()
Public Sub MyMethod(value As String)
Public Sub MyMethod(value As Integer)
Public Sub MyMethod(value As String, x As Integer)
Public Sub MyMethod(value As Integer, x As String)
一方、引数の名前はシグネチャを構成しないで次の2つのメソッドは同じシグネチャです。つまり、両方を実装するとエラーになります。
Public Sub MyMethod(valueA As String)
Public Sub MyMethod(valueB As String)
Functionでも事情は同じです。
Functionの場合、戻り値の型はシグネチャに含まれませんので、次の2つを両方実装するとエラーです。
Public Function MyFunc(x As Integer) As Integer
Public Function MyFunc(x As Integer) As Long
さて、次のように WriteLog メソッドを呼び出した場合何が呼び出されるでしょうか?
Me.WriteLog("ログのテスト") |
Stringの引数を1つだけ渡しているので名前がWriteLogでStringの引数が1つだけあるWriteLogメソッドが呼び出されます。
このようにシグネチャとは呼び出しを区別できる最小限の材料であるということです。
名前が同じ定義のことを「オーバーロード」と呼びます。
オーバーロードであることを明示するために定義にキーワード Overloads (読み方:Overloads = オーバーローズ)を記述することもできます。
Public Class
Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Me.WriteLog("ログのテスト") Me.WriteLog("本日は晴天なり") End Sub ''' <summary> ''' 現在の日時でC:\temp\VBLogTest.txtにログを書き込みます。 ''' </summary> Public Overloads Sub WriteLog(message As String) WriteLog(Now, message) End Sub ''' <summary> ''' 指定した日時でC:\temp\VBLogTest.txtにログを書き込みます。 ''' </summary> Public Overloads Sub WriteLog(logDate As Date, message As String) If Not IO.Directory.Exists("C:\temp") Then Return End If 'IO.File.AppendAllText("C:\temp\VBLogTest.txt", String.Format("{0} {1}{2}", Now, message, vbNewLine)) '←VB2013以前 IO.File.AppendAllText("C:\temp\VBLogTest.txt", $"{logDate} {message}{vbNewLine}") End Sub End Class |
同じクラス内でのオーバーロードの場合、Overloads キーワードは省略可能なので普通はつけません。つけても機能的には何も変わりません。
しかし、基底クラスのメソッドと名前が同じものを定義するときには Overloads キーワードが必須です。
たとえば、Form1 は Formクラスを継承しており、はじめからFormで定義されたさまざまなメソッドやプロパティが組み込まれています。
その中に Hide (ハイド)というメソッドがあり、呼び出すとフォームが非表示になります。このメソッドには引数はありません。
さて、次のように自分でForm1に引数つきの Hide というメソッドを定義するとシグネチャが違うにもかかわらずオーバーロードとはみなされずエラーになります。
Public Class
Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Me.Hide("さようなら") End Sub 'この例はエラーです。 Public Sub Hide(message As String) MsgBox(message) Me.Hide() End Sub End Class |
この例では次のような警告がエラー一覧に表示され、これが問題の本質です。
警告 BC40003 sub 'Hide' は、ベース class 'Control' で宣言されたオーバーロード可能なメンバーをシャドウします。ベース メソッドをオーバーロードする場合、このメソッドは 'Overloads' に宣言されていなければなりません。
『ベース class 'Control' で宣言』というのは実はFormクラスはさらに、別のクラスを継承しており多階層の継承関係になっています。たどっていくとその継承階層の中にControlというクラスがあり、実際に Hide メソッドが定義されているのはこのControlクラスであるため、警告にはこのように表示されます。
現段階では警告の内容はよく理解できないと思いますが、とにかく Overloads を付けろといわれており、この場合はそれが正解です。
同じクラス内でのオーバーロードの場合は、キーワード Overloads は不要ですが、基底クラスを巻き込んでのオーバーロードの場合は Overloads キーワードが必要です。
次のようにすると実行できるようになります。
Public Class
Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Me.Hide("さようなら") End Sub Public Overloads Sub Hide(message As String) MsgBox(message) Me.Hide() End Sub End Class |
これで実行すると「さようなら」というメッセージを表示した後フォームは見えなくなります。フォームが非表示になってもプログラムが終了するわけではないので、終了させるにはVisual Studioの停止ボタンを押してください。
さて、同じ名前のメソッドを定義できるというオーバーロードをどううまく活用すればよいでしょうか?
一般的にはオーバーロードで定義されたメソッドはどれも同じような機能で、ただ引数の指定方法が違うとか細かい動作を指定するためのオプションを追加で指定できるなどのバリエーションを実現でします。
たとえば、System.IO.Fileクラスにはファイルに文字列を書き込み機能の AppendAllText というメソッドがあり、2つのオーバーロードが定義されています。
Public Shared Sub AppendAllText (path As String, contents As String)
Public Shared Sub AppendAllText (path As String, contents As String, encoding As Encoding)
どちらも1つ目の引数は対象のファイルのパスで、2つ目の引数は書き込む内容です。これだけの情報があれば、書き込みは実行できるはずなのでこれが最小限です。これで書き込みを実行するのが1つめのオーバーロードです。
2つ目のオーバーロードはこれに加えて、第3引数で文字コードを指定することができます。多くの場合、文字コードはデフォルト(UTF-8)を使用すると思うので、常に文字コードを指定する必要があるとしたら面倒です。文字コードが不要なオーバーロードを用意することで、簡単に呼び出せるという便利さに加え、もし必要であれば文字コードの指定もできるという機能性を併せ持つことができるわけです。
別の例で、System.Mathクラスの Truncateメソッド (読み方:Truncate = トランケート)も紹介します。このメソッドは小数を切り捨てて整数にする機能です。たとえば、Truncate(56.78) は 56 を返します。これには2つのオーバーロードがあります。
Public Shared Function Truncate (d As Decimal) As Decimal
Public Shared Function Truncate (d As Double) As Double
このオーバーロードは型が違うだけでまったく同じ機能です。このように同じ機能を型ごとにオーバーロードで提供するという使い道もあります。
もし、もっとたくさんの型に対応する必要がある場合は、型ごとにオーバーロードを定義するのは大変なので、「ジェネリック」という機能の「型パラメーター」というものを使って型の違いを集約することが可能です。今回は説明を割愛しましたが、型パラメーターもシグネチャを構成する要因の1つです。ジェネリックについては別の機会に説明します。(このジェネリック機能はVB2005から導入されたため、それ以前に定義されている.NETのフレームワークに含まれるメソッドは、型が違うだけのたくさんのオーバーロードを持っている場合が良くあります。)
参考:File.AppendAllText メソッド
PublicやSubや引数の名前はシグネチャとは関係がないのでシグネチャに注目している場合、次のようにシグネチャを構成する部分のみのを抜粋してメソッドを記述することがあります。
MyMethod()
MyMethod(String)
MyMethod(Integer)
MyMethod(String, Integer)
MyMethod(Integer, String)
このような記述はVBの文法としてはエラーなので、実際のプログラムには使用できませんが、Microsoft Docsなどのドキュメントでは、このようにしてオーバーロードを記載しています。
オーバーロードがあるメソッドに対してはVisual Studioはオーバーロードの数を表示して、ヒントを切り替えることができます。
パラメーターを複数定義できように、VB2017以上であれば戻り値も複数返すことができます。
たとえば、さきほど紹介したわり算のあまりを返すメソッドでは、あまり だけを返しています。
''' <summary> ''' x ÷ y を実行して あまり を返します。 ''' 例 GetAmari(13, 4) は 13 ÷ 4 = 3 … 1 なので、1 を返します。 ''' </summary> ''' <param name="x">わられる数</param> ''' <param name="y">わる数</param> ''' <returns>あまり</returns> Public Function GetAmari(x As Integer, y As Integer) As Integer If y = 0 Then Return 0 End If Dim amari As Integer Dim kotae As Integer = Math.DivRem(x, y, amari) Return amari End Function |
これを、答えとあまりの2つを返すようにするには次のようにします。
呼び出し側のプログラムも含めて紹介します。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim result = GetAmari(20, 3) Debug.WriteLine(result.Item1) '6 と表示されます(1つ目の戻り値である答え) Debug.WriteLine(result.Item2) '2 と表示されます(2つ目の戻り値であるあまり) End Sub ''' <summary> ''' x ÷ y を実行して 答え と あまり を返します。 ''' 例 GetAmari(13, 4) は 13 ÷ 4 = 3 … 1 なので、3 と 1 を返します。 ''' </summary> ''' <param name="x">わられる数</param> ''' <param name="y">わる数</param> ''' <returns>答え と あまり</returns> Public Function GetAmari(x As Integer, y As Integer) As (Integer, Integer) If y = 0 Then '0でわろうとした場合エラーにします。 Throw New DivideByZeroException End If Dim amari As Integer Dim kotae As Integer = Math.DivRem(x, y, amari) Return (kotae, amari) End Function |
ポイントとなる部分を赤く強調しました。例を見ればすぐに理解できると思います。このように記述すると戻り値の実体は前回説明したタプルになります。タプルですので、呼び出し側ではItem1, Item2,・・・という名前で、複数の戻り値を取り出すことになります。
名前付きタプルも使用できます。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim result = GetAmari(20, 3) Debug.WriteLine(result.Kotae) '6 と表示されます(1つ目の戻り値である答え) Debug.WriteLine(result.Amari) '2 と表示されます(2つ目の戻り値であるあまり) End Sub ''' <summary> ''' x ÷ y を実行して 答え と あまり を返します。 ''' 例 GetAmari(13, 4) は 13 ÷ 4 = 3 … 1 なので、3 と 1 を返します。 ''' </summary> ''' <param name="x">わられる数</param> ''' <param name="y">わる数</param> ''' <returns>答え と あまり</returns> Public Function GetAmari(x As Integer, y As Integer) As (Kotae As Integer, Amari As Integer) If y = 0 Then '0でわろうとした場合エラーにします。 Throw New DivideByZeroException End If Dim amari As Integer Dim kotae As Integer = Math.DivRem(x, y, amari) Return (kotae, amari) End Function |
VB2015以前ではこの機能は使用できないので、どうしても複数の値を戻り値にしたい場合は、専用の構造体を定義していました。あくまでメソッドの戻り値は1つというわけです。次のようになります。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim result As GetAmariResult = GetAmari(20, 3) Debug.WriteLine(result.Kotae) '6 と表示されます(1つ目の戻り値である答え) Debug.WriteLine(result.Amari) '2 と表示されます(2つ目の戻り値であるあまり) End Sub ''' <summary> ''' x ÷ y を実行して 答え と あまり を返します。 ''' 例 GetAmari(13, 4) は 13 ÷ 4 = 3 … 1 なので、3 と 1 を返します。 ''' </summary> ''' <param name="x">わられる数</param> ''' <param name="y">わる数</param> ''' <returns>答え と あまり</returns> Public Function GetAmari(x As Integer, y As Integer) As GetAmariResult If y = 0 Then '0でわろうとした場合エラーにします。 Throw New DivideByZeroException End If Dim amari As Integer Dim kotae As Integer = Math.DivRem(x, y, amari) Dim result As New GetAmariResult result.Kotae = kotae result.Amari = amari Return result End Function Public Structure GetAmariResult Public Amari As Integer Public Kotae As Integer End Structure |
このようにするには別途構造体を定義する必要があり、手間は増えるのですが、どのような情報がやりとりされているか明確になるので私はこちらの方が好きです。