Visual Basic 初級講座 [改訂版] |
Visual Basic 中学校 > 初級講座[改訂版] >
2020/12/13
この記事が対象とする製品・バージョン
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 | × | 対象外です。 |
メソッドやコンストラクターなどの多くは呼び出すときに引数(ひきすう)を必要とします。
今回は引数の挙動を詳しく説明します。
引数には2つの側面があり、それぞれ「仮引数」(かりひきすう)と「実引数」(じつひきすう)と呼びます。
実引数とは、メソッドやコンストラクターの呼び出し時に指定する値のことです。
仮引数とは、メソッドやコンストラクターで定義されていて、実行時に呼び出し元から与えられる値を表している変数のことです。
別名・英語 | 意味 | ||
---|---|---|---|
引数 | 仮引数 | パラメーター Parameter |
呼び出し時に渡された値を表す変数。 Public Sub Test(x As Integer) の x |
実引数 | Argument(アーギュメント) ※紛らわしいことに、これを単に「引数」と呼ぶ場合があります。 |
呼び出し時に渡す値。 Me.Test(627) の 627 |
普段はプロのプログラマーでもこの違いを意識しておらず単に「引数」と呼びます。ただし、誰かが「パラメーターと引数」というように明らかにパラメータと引数を区別した発言をしたならば、パラメーターが仮引数のことなのでここで言う「引数」とは実引数を現していることになります。とは言え、紛らわしいので何の前提もなくこのような話をするプログラマーはまずいません。あまり神経質に区別しなくても良いのですが、必要なときには違いがあるということを思い出せるようにしておきましょう。
下記のプログラムを例に説明します。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim answer As Integer = Add(3, 4) Debug.WriteLine(answer) End Sub Public Function Add(paramX As Integer, paramY As Integer) As Integer Return paramX + paramY End Function |
3 と 4 は実引数で、paramX と paramY は仮引数です。
これを次のように表現する場合もあります。
『このプログラムの Addメソッドには、paramX と paramY という2つのパラメーターがあります。3と4を引数として、Addメソッドを呼び出しています。』
さて、変数を使って次のように引数を渡す場合を考えて見ます。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim argX As Integer = 3 Dim argY As Integer = 4 Dim answer As Integer = Add(argX, argY) Debug.WriteLine(answer) End Sub Public Function Add(paramX As Integer, paramY As Integer) As Integer Return paramX + paramY End Function |
実引数argXとargYが、仮引数paramXとparamYにそれぞれ渡されます。
つまり、argXからparamXへ、argYからparamYへ、何らかのコピーのような操作が行われています。
実験的にAddメソッド内で仮引数を変更してみることにしましょう。呼び出し側では呼び出し前後の値を出力するようにします。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim argX As Integer = 3 Dim argY As Integer = 4 Debug.WriteLine($"呼び出し前 argX={argX}") 'Debug.WriteLine("呼び出し前 argX=" & argX) '←VB2013以前の場合 Dim answer As Integer = Add(argX, argY) Debug.WriteLine($"呼び出し後 argX={argX}") 'Debug.WriteLine("呼び出し後 argX=" & argX) '←VB2013以前の場合 Debug.WriteLine(answer) End Sub Public Function Add(paramX As Integer, paramY As Integer) As Integer paramX = 627 '←ここで仮引数の値を変更 Return paramX + paramY End Function |
実行すると出力ウィンドウには次のように表示されます。
呼び出し前 argX=3
呼び出し後 argX=3
631
つまり、仮引数の値を変更しても、実引数には反映されないということです。
既定では実引数の値は、仮引数にコピーされており、コピー側をいくら変更しても元の値には影響しないというわけです。
この引数の引渡し方を「値渡し」(あたいわたし)と呼びます。
仮引数の直前に キーワード ByRef (読み方:ByRef=バイレフ)を付けると、この既定の動作は変更することができて、コピーではなく、実引数の値のそのものを仮引数に渡すこともできます。この引数の渡し方は「参照渡し」と呼びます。ByRefを指定した場合の動作は少し後で紹介します。
ByRefに対して既定の動作を行うキーワードとして ByVal (読み方:ByVal=バイバル)もありますが、書いても書かなくても意味が同じなので通常は書きません。Visual Studio 2010(SP1適用前)まではVisual Studioが生成するイベントプロシージャの引数にはByValがついていたのですが、その後は省略されるようになりました。
引渡し方法 | キーワード | 仮引数に何が渡されるか? |
---|---|---|
値渡し (既定の動作) |
ByVal | 実引数のコピーが渡される。 |
参照渡し | ByRef | 実引数そのものが渡される。 |
引渡し時にコピーされるのかそうではないかの違いは重要です。上記の例で見たように、コピーは値を変更しても呼び出し元には影響しないのに対して、コピーではない場合は、呼び出し元も変更されます。
話が難しくなるので、特に必要がなければコピーを渡すべきです。参照渡しを行う ByRef というキーワードは使用するべきではありません。そして、メソッドやコンストラクターの内部では仮引数の値を変更しないようにしましょう。このルールを守ってプログラムすれば、今回説明する難しいことをまったく気にしなくて良くなりますし、他にもプログラムの構造がシンプル化されるという利点があります。実際の開発現場でも ByRef は使用禁止 としているところもたくさんあるのではないかと思います。
守るべき2つのルール
ルール1.ByRef を使用しない。
ルール2.仮引数の値を変更しない。
ここでは説明のため、ルールを守らない場合(ByRefを使用したり、仮引数の値を変更したりする場合)どういうことが起こるのか見ていきます。ここから話が複雑になっていきます。
問題を複雑にする要素は3つあります。
要素1.2種類の引渡し方法(値渡し か 参照渡し か)
要素2.2種類の型 (値型 か 参照型 か)
要素3.2種類の変更操作 (仮引数を変更するのか 仮引数のメンバーを変更するのか)
これらの組み合わせで8個のケースに分かれます。
8個のケースを例1~例8として、実例で紹介します。
何が起こるか理屈を先に説明しておきます。
値型(=構造体)の変数の場合、変数自体が構造体の本体であるので、実引数は構造体本体です。
だから、値渡しをすると、仮引数には構造体本体のコピーが格納されます。参照渡しをすると仮引数には構造体本体がそのまま格納されます。
一方、参照型(=クラス)の変数の場合、変数のクラスの本体ではなく、本体は別途メモリ上のどこか別の場所に存在します。変数にはその場所をしめすアドレスが格納されているだけです。
だから、値渡しをすると、仮引数にはクラス本体の場所を示すアドレスのコピーが渡されます。参照渡しをすると仮引数にはクラス本体の場所を示すアドレスがそのまま渡されます。
要素1. 引渡し方法 |
要素2. 実引数 |
実引数の中身 | 仮引数に何が渡されるか? | 要素3.変更操作 | |
---|---|---|---|---|---|
仮引数を変更すると | 仮引数のメンバーを変更すると | ||||
値渡し (既定の動作) |
値型 | 構造体本体 | 構造体本体のコピー | 実引数には影響なし (例1) |
実引数には影響なし (例2) |
参照型 | クラスのアドレス | クラスのアドレスのコピー | 実引数には影響なし (例3) |
実引数のメンバーも変更される(例4) | |
参照渡し | 値型 | 構造体本体 | 構造体本体そのもの | 実引数も変更される (例5) |
実引数のメンバーも変更される(例6) |
参照型 | クラスのアドレス | クラスのアドレス | 実引数も変更される (例7) |
実引数のメンバーも変更される(例8) |
なお、通常の変数ではなく、定数や読み取り専用変数を実引数として使用した場合は、仮引数にどのような操作をしようと呼び出し元の定数や読み取り専用の変数は影響を受けません。そういう意味ではこれは4つ目の要素とも言えるのですが、定数や読み取り専用変数が変更されないという結論はわかりやすく、問題を複雑にしていないのでここでは取り上げません。
コピーが変更されるだけなので呼び出し元には影響しません。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim arg As New Point(3, 4) Debug.WriteLine($"呼び出し前 {arg.X}") '3 'Debug.WriteLine("呼び出し前 " & arg.X) '←VB2013以前の場合 Test(arg) 'コピーが変更されただけなので影響を受けません。 Debug.WriteLine($"呼び出し後 {arg.X}") '3 'Debug.WriteLine("呼び出し後 " & arg.X) '←VB2013以前の場合 End Sub Public Sub Test(p As Point) Dim newParam As New Point(6, 27) '仮引数の値を変更します。 p = newParam End Sub |
コピーが変更されるだけなので呼び出し元には影響しません。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim arg As New Point(3, 4) Debug.WriteLine($"呼び出し前 {arg.X}") '3 'Debug.WriteLine("呼び出し前 " & arg.X) '←VB2013以前の場合 Test(arg) 'コピーのメンバーが変更されただけなので影響を受けません。 Debug.WriteLine($"呼び出し後 {arg.X}") '3 'Debug.WriteLine("呼び出し後 " & arg.X) '←VB2013以前の場合 End Sub Public Sub Test(p As Point) '仮引数のメンバーの値を変更します。 p.X = 627 End Sub |
アドレスのコピーが変更されるだけなので呼び出し元には影響しません。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim arg As New Exception arg.HelpLink = "初期値" Debug.WriteLine($"呼び出し前 {arg.HelpLink}") '初期値 'Debug.WriteLine("呼び出し前 " & arg.HelpLink) '←VB2013以前の場合 Test(arg) 'アドレスのコピーが変更されただけなので影響を受けません。 Debug.WriteLine($"呼び出し後 {arg.HelpLink}") '初期値 'Debug.WriteLine("呼び出し後 " & arg.HelpLink) '←VB2013以前の場合 End Sub Public Sub Test(p As Exception) Dim newParam As New Exception newParam.HelpLink = "変更" '仮引数の値を変更します。 p = newParam End Sub |
アドレスのコピーを経由して本体が変更されるので、呼び出し元には影響します。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim arg As New Exception arg.HelpLink = "初期値" Debug.WriteLine($"呼び出し前 {arg.HelpLink}") '初期値 'Debug.WriteLine("呼び出し前 " & arg.HelpLink) '←VB2013以前の場合 Test(arg) 'アドレスのコピーを経由して本体が変更されるので、影響を受けます。 Debug.WriteLine($"呼び出し後 {arg.HelpLink}") '変更 'Debug.WriteLine("呼び出し後 " & arg.HelpLink) '←VB2013以前の場合 End Sub Public Sub Test(p As Exception) '仮引数のメンバーの値を変更します。 p.HelpLink = "変更" End Sub |
構造体本体が変更されることになるので呼び出し元に影響します。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim arg As New Point(3, 4) Debug.WriteLine($"呼び出し前 {arg.X}") '3 'Debug.WriteLine("呼び出し前 " & arg.X) '←VB2013以前の場合 Test(arg) '構造体本体が置き換えられたので影響されます。 Debug.WriteLine($"呼び出し後 {arg.X}") '6 'Debug.WriteLine("呼び出し後 " & arg.X) '←VB2013以前の場合 End Sub Public Sub Test(ByRef p As Point) Dim newParam As New Point(6, 27) '仮引数の値を変更します。 p = newParam End Sub |
構造体本体のメンバーが変更されるので呼び出し元に影響します。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim arg As New Point(3, 4) Debug.WriteLine($"呼び出し前 {arg.X}") '3 'Debug.WriteLine("呼び出し前 " & arg.X) '←VB2013以前の場合 Test(arg) '構造体本体のメンバーが変更されたので影響されます。 Debug.WriteLine($"呼び出し後 {arg.X}") '627 'Debug.WriteLine("呼び出し後 " & arg.X) '←VB2013以前の場合 End Sub Public Sub Test(ByRef p As Point) '仮引数のメンバーの値を変更します。 p.X = 627 End Sub |
クラスのインスタンス本体が変更されることになるので呼び出し元に影響します。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim arg As New Exception arg.HelpLink = "初期値" Debug.WriteLine($"呼び出し前 {arg.HelpLink}") '初期値 'Debug.WriteLine("呼び出し前 " & arg.HelpLink) '←VB2013以前の場合 Test(arg) 'インスタンス本体が変更されるので影響を受けます。 Debug.WriteLine($"呼び出し後 {arg.HelpLink}") '変更 'Debug.WriteLine("呼び出し後 " & arg.HelpLink) '←VB2013以前の場合 End Sub Public Sub Test(ByRef p As Exception) Dim newParam As New Exception newParam.HelpLink = "変更" '仮引数の値を変更します。 p = newParam End Sub |
アドレスを経由して、本体のメンバーが変更されるので呼び出し元には影響します。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim arg As New Exception arg.HelpLink = "初期値" Debug.WriteLine($"呼び出し前 {arg.HelpLink}") '初期値 'Debug.WriteLine("呼び出し前 " & arg.HelpLink) '←VB2013以前の場合 Test(arg) 'アドレスを経由して、本体のメンバーが変更されるので影響を受けます。 Debug.WriteLine($"呼び出し後 {arg.HelpLink}") '変更 'Debug.WriteLine("呼び出し後 " & arg.HelpLink) '←VB2013以前の場合 End Sub Public Sub Test(ByRef p As Exception) Dim newParam As New Exception newParam.HelpLink = "変更" '仮引数の値を変更します。 p = newParam End Sub |
ルールを守らない場合の例を見てうんざりしたことでしょう。もうこのことは忘れてByRefを使わない、仮引数を変更しないというルールを守ることで面倒なことから開放されるのが良いでしょう。
しかし、複数の戻り値を返したい場合に、ByRefを使用するプログラマーがいます。このケースではByRefを使用すべきではありません。
たとえば、わり算をして答えとあまりを返したい場合です。11 ÷ 4 = 2 あまり 3 です。他に昔(2000年ごろ?)よくあったのは、エラー情報と処理結果の両方を返したいというものです。
ByRefを使ってわり算の答えとあまりを返すメソッドを作成すると次のようになります。ルールを守っていないという意味で悪い例なのですが、機能上は問題なく動作します。
'これは悪い例です。 ''' <summary> ''' x ÷ y を実行して 答え と あまり を返します。 ''' 答えは戻り値として返します。あまりは 引数 pAmari を使って返します。 ''' </summary> ''' <param name="x">わられる数</param> ''' <param name="y">わる数</param> ''' <param name="pAmari">計算の結果返されるあまり</param> ''' <returns>あまり</returns> Public Function Warizan(x As Integer, y As Integer, ByRef pAmari 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) pAmari = amari '← ここで ByRef の引数を書き換える。 Return kotae End Function |
これを呼び出す側のプログラムは次のようになります。
Dim amari
As Integer Dim kotae As Integer = Warizan(11, 4, amari) Debug.WriteLine($"11 ÷ 4 = {kotae}・・・{amari}") 'Debug.WriteLine("11 ÷ 4 = " & kotae & "・・・" & amari) '←VB2013以前の場合 |
ByRefを使うと呼び出し元の値を書き換えられることを利用して、引数を戻り値のように利用する作戦というわけです。
複数の戻り値については第19回 メソッドの作成で扱っています。そことの繰り返しになりますが、VB2017以上であれば、タプルを使うことで簡単に複数の戻り値を扱うことができます。それ以前のバージョンの場合は戻り値を構造体にすることで少し手間はかかりますが同じようなことを実現できます。私は構造体を使う方法が好みです。(それらのサンプルは第19回を参照してください。)
ByRefを使ってこれを実現するプログラマーは、VB2017からタプルで実現できるという新しい情報を知らないか、構造体を使うための「少しの手間」を惜しんでいるプログラマーだと思います。一方、ByRefを使っても機能上の問題はないので、今回扱っている参照渡しや値渡しについてしっかりした知識があるのであれば、特に問題にならないことも多いです。それでも、プログラムは複雑化して手に負えなくなっていくもの、バグは付き物でです。チーム開発している他のプログラマーや、将来このプログラムを引き継ぐ担当者は、参照渡しや値渡しのことをよく理解していないかもしれません。この状況を少しでも良い方向に持っていくには、引数は渡される値、戻り値は返す値 というシンプルな原則論に従っておいたほういいのではないかというのが私の意見で、私はこれを世の中の大勢と感じています。
TryParseメソッド(読み方:TryParse=トライパース)はフレームワークのメソッドです。ByRefがうまく活用されていて便利です。
TryParseメソッドはIntegerやLong, Dateなどの基本型に実装されている静的メソッドで、文字列からの型変換を実行します。
戻り値は型変換に成功した場合True、失敗した場合Falseなので、型変換の成否は次のように If 文などで判断できます。
Dim i As
Integer 'ABCをInteger型に変換してみます。→失敗します。 If Integer.TryParse("ABC", i) Then Debug.WriteLine("型変換に成功") Debug.WriteLine("値は" & i) Else Debug.WriteLine("型変換に失敗") '←こちらが表示されます。 End If |
ユーザーからの入力など変換に成功するかどうかわからないときに使用すると便利です。
型変換の結果は、戻り値ではなく引数に格納されます。この例では i です。この引数は ByRef で定義されているため、このように呼び出し元の変数(i)の値を書き換えることができるわけです。
同じような型変換を実施する機能としてたとえば Parse メソッドもありますが、Parseメソッドの場合、変換できない場合 Formatxception の例外が発生します。そのため、Parseで同じことを実現しようとすると例外処理のTry ~ Catch と組み合わせて次のように記述する必要があります。
一見、If が Try になっただけで同じようにも見えますが、第23回で説明したように、Try はできるだけ使うべきではなく、Ifの代わりにTryを使うことは、実行速度の低下や、保守性の低下を招きます。VB2003まではこのケースはTryを使うしかなかったのですが、VB2005のときに TryParseが導入されて、プログラマーはこのケースでのTryから開放されたのです。 |
このケースでは ByRef を使っているのはフレームワークを作成したマイクロソフトのプログラマーなので、ルールを破ったデメリットのほとんどを負うのはあなたではなく、マイクロソフトのプログラマーです。それに、TryParseメソッドの使い方は洗練されていて、特に混乱するようなこともないので、積極的に使ってよいです。ByRefがうまく活用されている例だと思います。
参考に、ParseメソッドとTryParseメソッドの違いを簡単にまとめておきます。
Parse | TryParse | |
---|---|---|
型変換できない場合 | 例外が発生する | 例外は発生しない |
戻り値 | 型変換した結果 | 型変換できた場合 True 型変換できなかった場合 False |
型変換の結果 | 戻り値で受け取る | ByRefが指定された引数で受け取る |
こちらはかなりレアケースです。変数を交換したいときに次のようなByRefを利用したメソッドを作っておくことで簡単に実現するテクニックがあります。
Public Sub Swap(ByRef
o1 As String,
ByRef o2 As
String) Dim temp As String temp = o1 o1 = o2 o2 = temp End Sub |
このSwapメソッドを使用すると引数の内容が交換されます。
使用例は次の通りです。
この例では 変数strA と 変数strB が交換され、Swapの呼び出し前後でお互いの内容が入れ替わります。
Dim strA
As String = "あいうえお" Dim strB As String = "ABCDE" 'あいうえおABCDE Debug.WriteLine(strA & strB) Swap(strA, strB) 'ABCDEあいうえお Debug.WriteLine(strA & strB) |
ByRefの機能がうまく使われています。
このSwapメソッドは特別複雑なわけでもなく、実害はなさそうなので、ルールには違反しているものの、私がレビュー担当者であればダメ出しはしないレベルです。
とはいえ、変数を交換したいという場面はそれほど多くないので、この例のようにメソッドとして独立させないで処理の中に埋め込んでも十分です。処理自体も特別長くはありません。
最後に注意が必要な場合を指摘しておきます。
ここまでの説明をよく読めば気が付くことなのですが、ルールを守ってByRefを使用せず、引数の値を変更しない場合でも、参照型の引数の場合は、メンバーの変更は呼び出し元に影響します。
「ルールを守っていれば、引数に何をしても呼び出し元には影響しないんだぜー」というのは間違った理解です。
前述の例4のケースのことで、その理由も既に書いてあります、「アドレスのコピーを経由して本体が変更されるので、呼び出し元には影響します。」ということです。
ルールを守った場合に、呼び出し元に影響があるのはこの場合だけです。
引渡し方法 | 実引数 | 実引数の中身 | 仮引数に何が渡されるか? | 変更操作 | |
---|---|---|---|---|---|
仮引数を変更すると | 仮引数のメンバーを変更すると | ||||
値渡し (既定の動作) |
値型 | 構造体本体 | 構造体本体のコピー | 実引数には影響なし (例1) |
実引数には影響なし (例2) |
参照型 | クラスのアドレス | クラスのアドレスのコピー | 実引数には影響なし (例3) |
実引数のメンバーも変更される(例4) |
これは機械的に覚えようとすると面倒な例外のように感じられるかもしれませんが、直感的に合致しており、便利です。
たとえば、次の SetupOpenFileDialogメソッドは、引数に指定したOpenFileDialogクラスに対して、さまざまな値を設定します。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click 'フォームにOpenFileDialog1が配置されている前提です。 SetupOpenFileDialog(Me.OpenFileDialog1) Me.OpenFileDialog1.ShowReadOnly = True '読み取り専用ファイルとして開くチェックボックスを表示 Me.OpenFileDialog1.ShowDialog() End Sub ''' <summary> ''' ファイルを開くダイアログに初期状態を設定します。 ''' </summary> Public Sub SetupOpenFileDialog(dialog As OpenFileDialog) dialog.Title = "テキストファイル選択" dialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) dialog.Filter = "テキストファイル(*.txt;*.log)|*.txt;*.log|すべてのファイル|*.*" dialog.Multiselect = True '複数ファイル選択を許可する End Sub |
引数の観点で、このプログラムのポイントは、ByRefを使っていない dialog引数でも、メンバーの変更は呼び出し元に影響しているという点です。でも、これができないとしたらとても不便ですし、特にわかりにくくは感じないのですが、みなさんはどう感じるでしょうか?
SetupOpenFileDialogメソッドが呼び出し元に影響するというのがわかりにくいと感じるようでしたら、この感覚に慣れて使いこなすようにしていく必要があります。もしかすると、今回はややこしい例や説明が多かったので頭が疲れているだけかもしれません。