メソッドに引数を渡す方法には、「値渡し」と「参照渡し」の2種類があります。ここでは両者の違いと、どのような時にどちらを使うかについて説明します。
まずは以下のコードをご覧ください。
Public Shared Sub DoSomething(ByVal arg1 As Integer) 'パタメータの値を変更する arg1 += 1 End Sub 'エントリポイント Public Shared Sub Main() Dim val1 As Integer = 0 'DoSomethingメソッドを呼び出す DoSomething(val1) '引数として渡した変数が変化したかを確認する Console.WriteLine(val1) '「0」と表示される Console.ReadLine() End Sub
public static void DoSomething(int arg1) { //パタメータの値を変更する arg1++; } //エントリポイント public static void Main() { int val1 = 0; //DoSomethingメソッドを呼び出す DoSomething(val1); //引数として渡した変数が変化したかを確認する Console.WriteLine(val1); //「0」と表示される Console.ReadLine(); }
「DoSomething」というメソッドで、パラメータ(arg1)の値を変更しています。このメソッドを呼び出すと、メソッドに引数として渡した変数(上の例では、val1)の値はどうなるでしょうか?
結果は、val1の値が0のままで変わりません。このように、メソッド内でパラメータの値を変更しても、呼び出し元の引数には全く影響がありません。これが、「値渡し」です。
注意:VB.NETでは「ByVal」を省略することもできますが、できるだけ省略しないでください。
補足:値渡しであっても、「arg1++」とすると、DoSomethingメソッドの中ではarg1は1増えた値になります。ただそれがval1には反映されないというだけです。
次に「参照渡し」を使ってみましょう。前のコードとの違いは、DoSomethingメソッドのパラメータ「arg1」に「ByRef」(C#では、「ref」)が付いた点です。それ以外の違いはありません。
なおC#の場合は、「ref」が付いているパラメータに引数を渡す時、引数にも「ref」を付けます。付け忘れた時はエラーになりますので、すぐに分かります。
Public Shared Sub DoSomething(ByRef arg1 As Integer) 'パタメータの値を変更する arg1 += 1 End Sub 'エントリポイント Public Shared Sub Main() Dim val1 As Integer = 0 'DoSomethingメソッドを呼び出す DoSomething(val1) '引数として渡した変数に変わりがないか確認する Console.WriteLine(val1) '「1」と表示される Console.ReadLine() End Sub
public static void DoSomething(ref int arg1) { //パタメータの値を変更する arg1++; } //エントリポイント public static void Main() { int val1 = 0; //DoSomethingメソッドを呼び出す DoSomething(ref val1); //引数として渡した変数に変わりがないか確認する Console.WriteLine(val1); //「1」と表示される Console.ReadLine(); }
今度はval1が1になりました。つまり、DoSomethingメソッド内でパラメータの値を変更すると、それが呼び出し元の引数にも反映されます。これが「参照渡し」です。
このように値渡しと参照渡しの違いは、メソッドに引数として渡した変数の値をメソッドが変更できないか、できるかです。値渡しで変数を渡せば、その変数の値が変わることはありません。しかし参照渡しで変数を渡した時は、その変数の値は変更されるかもしれません(変更されるものと思った方がよいでしょう)。
上記の例ではパラメータが値型でしたが、参照型でも基本的には同じです。ただ、値型と参照型の根本的な違いによる差異はあります。
以下の例では、メソッド内で参照型パラメータのデータ内容を変更しています。すると、このメソッドを呼び出す時に渡した引数のデータ内容も変更されてしまいます。なぜこのような結果になるかについては「値型と参照型の区別と違い」で詳しく説明していますので、分からないという方はまずそちらをご覧ください。
'Imports System.Text Public Shared Sub DoSomething(ByVal arg1 As StringBuilder) '値渡しパラメータのデータ内容を変更する arg1.Append("!") End Sub 'エントリポイント Public Shared Sub Main() '参照型のオブジェクトを作成する Dim ref1 As New StringBuilder("りんご") 'DoSomethingメソッドを呼び出す DoSomething(ref1) 'ref1の内容が変化したかを確認する Console.WriteLine(ref1) '「りんご!」と表示される Console.ReadLine() End Sub
//using System.Text; public static void DoSomething(StringBuilder arg1) { //値渡しパラメータのデータ内容を変更する arg1.Append("!"); } //エントリポイント public static void Main() { //参照型のオブジェクトを作成する StringBuilder ref1 = new StringBuilder("りんご"); //DoSomethingメソッドを呼び出す DoSomething(ref1); //ref1の内容が変化したかを確認する Console.WriteLine(ref1); //「りんご!」と表示される Console.ReadLine(); }
このようにパラメータが参照型ならば、値渡しであっても、メソッド内でパラメータのプロパティ値を変更すると、呼び出し元の引数のプロパティ値も変わります。ただし、引数として渡された変数の参照先を変更するためには、参照渡しにしなければなりません。
参照型を値渡しした時と参照渡しした時の違いが分かる例を以下に示します。
'Imports System.Text Public Shared Sub DoSomething(ByVal arg1 As StringBuilder, _ ByRef arg2 As StringBuilder) '値渡しパラメータのデータ内容を変更する arg1.Append("!") '値渡しパラメータの参照先を新しいデータにする arg1 = New StringBuilder("バナナ") '参照渡しパラメータの参照先を新しいデータにする arg2 = New StringBuilder("ぶどう") End Sub 'エントリポイント Public Shared Sub Main() '参照型のオブジェクトを2つ作成する Dim ref1 As New StringBuilder("りんご") Dim ref2 As New StringBuilder("みかん") 'DoSomethingメソッドを呼び出す DoSomething(ref1, ref2) '引数として渡した変数を確認する Console.WriteLine("{0} / {1}", ref1, ref2) '「りんご! / ぶどう」と表示される Console.ReadLine() End Sub
//using System.Text; public static void DoSomething(StringBuilder arg1, ref StringBuilder arg2) { //値渡しパラメータのデータ内容を変更する arg1.Append("!"); //値渡しパラメータの参照先を新しいデータにする arg1 = new StringBuilder("バナナ"); //参照渡しパラメータの参照先を新しいデータにする arg2 = new StringBuilder("ぶどう"); } //エントリポイント public static void Main() { //参照型のオブジェクトを2つ作成する StringBuilder ref1 = new StringBuilder("りんご"); StringBuilder ref2 = new StringBuilder("みかん"); //DoSomethingメソッドを呼び出す DoSomething(ref1, ref ref2); //引数として渡した変数を確認する Console.WriteLine("{0} / {1}", ref1, ref2); //「りんご! / ぶどう」と表示される Console.ReadLine(); }
この例のDoSomethingメソッドは、arg1とarg2の2つのパラメータを持っています。両方とも参照型(StringBuilder)ですが、arg1は値渡し、arg2は参照渡しです。
DoSomethingメソッド内では、arg1のデータ内容を変更し(末尾に「!」を付ける)、さらにarg1の参照先を変更しています。この時、引数として渡した変数(ref1)は、データ内容は変更されましたが、参照先は変わっていません。
一方arg2は、メソッド内で参照先を変更すると、引数として渡した変数(ref2)の参照先も変わります。
参照渡しが役に立つケースの一つが、メソッドが戻り値以外の値を返したい時です。場合によっては、メソッドの参照渡しパラメータを値を返す目的だけで使用したくて、メソッドが受け取る値はどうでもいい状況もあります。C#ではこのような時に、「ref」の代わりに「out」を使用できます。
「ref」の代わりに「out」を使用すると、引数に渡す変数を初期化する必要がなくなります。その代わりにメソッド内でoutパラメータの値を使用することができず、必ずoutパラメータには新しい値を代入しなければなりません。
先程のサンプルコードで、「ref」を「out」に書き換えてみましょう。
public static void DoSomething(out int arg1) { //パタメータの値を変更する arg1 = 1; } //エントリポイント public static void Main() { //outを使用する時は、初期化の必要がない int val1; //DoSomethingメソッドを呼び出す DoSomething(out val1); //引数として渡した変数の値を確認する Console.WriteLine(val1); //「1」と表示される Console.ReadLine(); }
「ref」を「out」に書き換えただけでは、「arg1++」の箇所で「割り当てのないパラメータ 'arg1' の使用です。」というエラーになります。「out」で渡されたパラメータの値は使用できないため、このようなエラーになってしまいます。ですので上の例ではこの箇所を「arg1 = 1」に変更して、arg1の値を使用しないようにしています。
VB.NETでは、ByRefパラメータに引数を値渡しで渡す方法があります。下の例のように引数を括弧 () で囲むことによって、値渡しを強制することができます。
Public Shared Sub DoSomething(ByRef arg1 As Integer) arg1 += 1 End Sub 'エントリポイント Public Shared Sub Main() Dim val1 As Integer = 0 '値渡しを強制して、DoSomethingメソッドを呼び出す DoSomething((val1)) Console.WriteLine(val1) '「0」と表示される Console.ReadLine() End Sub
メソッドを自分で作成する時、パラメータを値渡しにするか参照渡しにするか迷ったならば、値渡しにしてください。なぜなら、メソッド内で引数が変更されてしまう危険性を重視すべきだからです。無理をしてでも参照渡しにした方が良いケースはほとんどありません。
参照渡しを使う最もよくあるケースは、メソッドが戻り値以外の値を返したい時です。例えば、Double.TryParseメソッドは指定された文字列がDouble型に変換できるかを戻り値(Boolean型)で返しますが、それ以外にDouble型に変換された値を参照渡しのresultパラメータに格納します。そのため、Double.TryParseメソッドを一度呼び出せば、文字列がDouble型に変換できるかを調べ、同時に変換できた時はその値も取得することができます。
参照渡しの利点としてもう一つ、パフォーマンスの問題があります。値渡しでは値のコピーを作成します。そのため、大きなサイズの値型のデータを引数で渡す時は、値渡しよりも参照渡しの方がパフォーマンスがよくなる可能性があります。ただし、「Visual Basic .NET でのパフォーマンスの最適化」などでは、値渡しと参照渡しのパフォーマンスの差は重要ではなく、別の点を考慮すべきだとしています。