現在世の中で使われている多くのプログラミング言語において、関数は「引数」として情報を受け取る機能を有しています。この引数として渡される変数の「引き渡され方」は、大きく分けて変数そのものを渡す「値渡し」と変数に対するアドレス情報を渡す「参照渡し」が存在し、それらの振る舞いはプログラミング言語の種類により異なります。
そこで、このページでは値渡しと参照渡しについて、両者の処理方法の違いをあらためて掘り下げて考えてみたいと思います。
グレープシティ テクニカルエバンジェリスト
八巻 雄哉
2003年1月入社。PowerToolsシリーズのテクニカルサポートを担当するかたわら、製品開発やマーケティングにも従事。2006年から、.NETテクノロジーとPowerToolsシリーズ普及のためエバンジェリストとして活動中。
Microsoft MVP for Development Platforms -Client App Dev Jan 2009 -Dec 2009
デフォルトは「値渡し」
Visual Basicでは、値渡しはByVal、参照渡しはByRefというキーワードを引数に付加します。このキーワードは省略可能で、省略したときの振る舞いはVB6以前では参照渡し、.NET以降のVisual Basicでは値渡しと異なっています。
省略して記述されているソースコードを
Visual Basicアップグレードウィザードを使って.NETのVisual Basicコードへ変換した場合には、自動的にByRefキーワードが付加されるため大きな問題にはなりません。しかしながら、実際に省略して記述していたという人は .NETで振る舞いが変わることを理解しておく必要があるでしょう。
なお、.NETではVisual StudioでByVal/ByRefを省略して記述しようとするとIntelliSenceによって自動的にByValが補完されます。
単純に値渡しと参照渡し、つまり「ByValキーワードとByRefキーワードのどちらを使用するべきか?」という問いであれば、その答えは簡単です。
可能な限りByValキーワードを使用する。
このことは、先の省略した場合にIntelliSenceによって自動的にByValが補完されるという動作に加え、Visual Studio Team Systemのコード分析に下記のような規則が存在していることからも明らかです。
型を参照渡ししないでください
可能な限りByValキーワードを使用すべき理由は、呼び出し元のコードの変数がプロシージャによって変更されるのを防ぐことができるという点です。
これは「AさんからBさんへのビデオテープ(オリジナルムービー)の貸し出し」を例に考えてみると分かりやすいでしょう。
値渡しは撮影したマスタテープをビデオテープにダビングしてBさんへ貸し出す方法です。貸し出しているのは複製物であるため、誤ってBさんがそのビデオテープの内容を上書きしてしまった場合でもAさんのオリジナルムービーは無事です。一方、参照渡しはマスタテープの保管場所をBさんに教え、そのビデオテープをAさんとBさんで共有して視聴する方法になります。そのため、誤ってBさんがビデオテープを上書きしてしまった場合にはAさんは怒り心頭に発してしまうというわけです。
では、実際のコード例を見てみましょう。以下のコード例では、呼び出し元のプロシージャにおいて値渡しされた変数aの値はTestメソッドの実行後でも値が1のままとなっているのに対して、参照渡しされた変数bの値はTestメソッドの実行後に2に変更されています。
Sub Main()
Dim a As Integer, b As Integer
a = 1
b = 1
Test(a, b)
Console.WriteLine("a={0}", a)
Console.WriteLine("b={0}", b)
Console.ReadKey()
End Sub
Private Sub Test(ByVal a As Integer, ByRef b As Integer)
a += 1
b += 1
End Sub
実行結果 :
a=1
b=2
呼び出し元のコードの変数を変更できる参照渡しの場合、呼び出し元からは引数として渡した変数が変更されるのかどうかですら判断がつきません。このことはコードの可読性を落とすだけでなく、バグを作りこむ原因にもなります。
「参照渡し」のメリットは?
では、参照渡しが必要となるのはどのような場合でしょうか?参照渡しのメリットとして、一般的にはパフォーマンスが挙げられます。これは、変数の「参照」をコピーする参照渡しに比べて変数の「値」をコピーする値渡しのほうがオーバーヘッドは大きくなるという考えによるものです。
しかし .NETではパフォーマンスという観点で参照渡しを使用したほうが良いケースというのは稀です。.NETの変数のデータ型は値型と参照型の2種類に大別されます。MSDNライブラリの「
値型と参照型」の記載によると、下記のように分けられています。
値型
- ・すべての数値データ型
- ・Boolean、Char、および Date
- ・すべての構造体(メンバが参照型の場合でも)
- ・列挙型(基になる型が常に SByte、Short、Integer、Long、Byte、UShort、UInteger、または ULong であるため)
参照型
- ・String
- ・すべての配列(要素が値型の場合でも)
- ・クラス型(Form など)
- ・デリゲート
実は、値渡しをする場合であっても変数が値型か参照型かによって、その振る舞いは大きく異なります。たとえば以下のコード例の場合、値型である構造体SPersonのNameフィールドは”Yamaki”のままとなっているのに対して、参照型であるクラスCPersonのNameフィールドは”YamakiYuya”と変更されてしまいます。
Sub Main()
Dim s As New SPerson
Dim c As New CPerson
s.Name = "Yamaki"
c.Name = "Yamaki"
Test(s, c)
Console.WriteLine("SPerson.Name={0}", s.Name)
Console.WriteLine("CPerson.Name={0}", c.Name)
Console.ReadKey()
End Sub
Private Structure SPerson
Public Name As String
End Structure
Private Class CPerson
Public Name As String
End Class
Private Sub Test(ByVal s As SPerson, ByVal c As CPerson)
s.Name += "Yuya"
c.Name += "Yuya"
End Sub
実行結果 :
SPerson.Name=Yamaki
CPerson.Name=YamakiYuya
これは、同じ値渡しであっても値型はメモリ内(スタック)に値が格納されるのに対して、参照型は値を保持する別のメモリ(ヒープ)の場所を示すポインタが格納されるためです。つまり、参照型の変数を値渡しした場合、変数そのものを変更することはできなくとも変数が参照するインスタンスのメンバは変更することができるのです。
このことは、MSDNライブラリの「
引数の値渡しと参照渡しの違い」に記載されており、下記の表のようにその振る舞いを分類することができます。
| 要素の型 |
ByVal で渡す場合 |
ByRef で渡す場合 |
| 値型(格納されるのは値のみ) |
プロシージャは、変数およびそのメンバを一切変更できません。 |
プロシージャは、変数およびそのメンバを変更できます。 |
| 参照型(クラスまたは構造体のインスタンスへのポインタを格納) |
プロシージャは、変数を変更することはできませんが、変数が指すインスタンスのメンバを変更できます。 |
プロシージャは、変数および変数が指すインスタンスのメンバを変更できます。 |
つまり、参照型を値渡しした場合には変数の値ではなくデータを保持する別のメモリのポインタが値としてコピーされていることになるため、パフォーマンスへの影響はありません。
値型を値渡しした場合にはコピーにかかるオーバーヘッドを無視できない場合があります。おもに構造体などデータサイズの大きい値型の変数を扱う場合には参照渡しの使用を検討すべきです。
参照渡しが必要となるもう1つのケースは、異なる性質の結果を複数返す関数を実装したい場合です。このような実装は、.NET FrameworkクラスライブラリのTryParseメソッドなどで確認することができます。
TryParseメソッドは、変換の成功の可否をBoolean型の戻り値として返すほかに、第2引数が参照渡しとして実装されていることで変換結果も同時に受け取れるようになっています。このような実装は例外的と言えますが、このことからも「参照渡しを使ってはいけない」のではなく、「可能な限り値渡しを使用する」ということが見て取れます。
開発者はどう使い分けている?
関数を作る際の記述方法に関するアンケートを2009年10月22日に配信したPowerNews 290号で行いました。回答結果と皆さまからいただいたコメントを紹介します。
質問内容
実務で関数(メソッド)を作る際、どちらを多用しますか?
その1
---------------------------------------------
Sub Hoge(ByRef aStr As String)
<処理内容>
End Sub
---------------------------------------------
その2
---------------------------------------------
Function Hoge(ByVal aStr As String) As String
<処理内容>
End Function
---------------------------------------------
回答結果
有効回答数:66
アンケート実施日:2009/10/22
皆さんからいただいたコメント
その1(Sub + ByRef)を選んだ方のコメント
リターン値が不要ならSubで、リターン値が必要ならFunctionで。
その2(Function + ByVal)を選んだ方のコメント
処理結果が正常か異常かを返してほしい場合が多いので,以下のパターンもよく使います。
戻り値を使って、色々処理を行うロジックを書くクセがあるので。
SQL文みたいな処理の流れを掴みたい時に邪魔する長いのを外に逃がすって場合が多い。
( C言語はこの方式)。
私はたとえ返り値が必要ない関数であっても
処理が成功したかどうかくらいは返すべきだと思います。
(1)引数の渡し方
ほとんどが値渡しを使用する場面が多いですし、
.NETになってから、規定値がByValになりましたから、
そのまま使っても問題ないと思います。
ただ、ByRefを使うこともたまにはあります。
(2)戻り値について
Funcitonキーワードで作成しておけば、戻り値が必要で
無くなっても使わなければいいので、両方の状態が
可能な方が修正が楽です。
私の場合、メソッドの約8割以上がFunctionだと思います。
値を変更する関数なら、その意図を持ってInputとOutputを明確にする方がよいと思います。
なのでそれに沿って。