プログラミング言語のメソッド(関数)呼び出し時に引数を渡す方法には 値渡し(call by value)と参照渡し(call by reference)という2つの方法があります。
C# ではメソッドの引数は基本的に値渡しになります。
しかし、ref や out といったキーワードを使うことで参照渡しにすることが出来ます。
引数の値渡し(call by value)とは、メソッドを呼び出す際に値のコピーを渡すことを言います。 C# では普通にメソッドを定義すると、その引数は値渡しになります。 例えば、以下のようなプログラムがあったとします。
using System; class ByValueTest { static void Main() { int a = 100; Console.Write("{0} → ", a); Test(a); Console.Write("{0}\n", a); } static void Test(int a) { a = 10; // メソッド内で値を書き換える。 } }
Test メソッドの変数 a には Main メソッドの a のコピーが渡されています。
したがって、図1のように、
Test 内で変数 a を書き換えても
Main 内の a の値は変わりません。
そのため、このプログラムの実行結果は以下のようになります。
100 → 100
同様に、参照型の変数を値渡しする場合、図2, 3に示すように、参照情報をコピーして渡すことになります。
引数の参照渡し(call by reference)とは、メソッドを呼び出す際に値の参照情報を渡すことを言います。
C# では、以下の例のように、メソッドの引数に ref キーワードを付けることでその変数は参照渡しになります。
using System; class ByReferenceTest { static void Main() { int a = 100; Console.Write("{0} → ", a); Test(ref a); Console.Write("{0}\n", a); } static void Test(ref int a) { a = 10; // メソッド内で値を書き換える。 } }
Test メソッドの変数 a は Main メソッドの a に対する参照になっています。
したがって、図4のように、
Test 内で変数 a を書き換えた場合、
Main 内の a の値も同時に書き換わります。
そのため、このプログラムの実行結果は以下のようになります。
100 → 10
同様に、参照型の変数を値渡しする場合、図5に示すように、参照情報をさらに参照することになります。
ここで1つ注意しなければいけないのは、
メソッドの呼び出し側にも ref キーワードをつける必要があるということです。
参照渡しを行うと、メソッドの中で値が書き換えられる可能性があります。
(というよりも、書き換える必要があるから参照渡しにする。)
引数が参照渡しであることを知らずにメソッドを呼び出してしまうと、
プログラマの意図しないところで値が書き換わってしまう可能性があり、
これはバグの原因になります。
そのため、呼び出し側でも明示的に ref キーワードを付けなければならいないという制約をつけることによって、
知らないうちに参照渡しのメソッドを呼び出してしまう危険性をなくしています。
using System; class ByRefferanceTest { static void Main() { int[] array = new int[]{4, 6, 1, 8, 2, 9, 3, 5, 7}; BubbleSort(array); foreach(int a in array) { Console.Write("{0,3}", a); } } /// <summary> /// バブルソートを使って配列を整列する /// </summary> static void BubbleSort(int[] array) { for(int i=0; i<array.Length-1; ++i) for(int j=array.Length-1; j>i; --j) if(array[j-1] > array[j]) Swap(ref array[j-1], ref array[j]); } /// <summary> /// a と b の値を入れ替える /// </summary> static void Swap(ref int a, ref int b) { int tmp = a; a = b; b = tmp; } }
1 2 3 4 5 6 7 8 9
参照渡しを使うと、メソッド内でオブジェクトを初期化することが出来るようになります。 つまり、呼び出し元では変数に意味のある値を格納せず、 メソッド内で値を代入してもらうことで変数の初期化を行うことが出来ます。 しかし、C# では通常、変数は使用する前に必ず初期化していなければなりません。
そこで、メソッド内で変数を初期化する予定である場合、
以下のように out キーワードを用いて出力用の変数であることを明示してやります。
using System; class ByValueTest { static void Main() { int a; Test(out a); // out を使った場合、変数を初期化しなくてもいい Console.Write("{0}\n", a); } static void Test(out int a) { a = 10; // out を使った場合、メソッド内で必ず値を代入しなければならない } }
10
out キーワードを用いて宣言された引数は参照渡しになります。
ref キーワードとの違いは、上述のとおり、
メソッド呼び出し前に初期化する必要がなく、
メソッド内で必ず値を割り当てなければいけない点です。
メソッドで複数の値を返したい場合、 戻り値では1つしか値を返せないので出力変数を使います。
using System; class OutTest { /// <summary> /// コンソールから係数を入力して2次方程式の根を計算し、出力する。 /// </summary> static void Main() { string line = Console.ReadLine(); string[] token = line.Split(' '); double a = double.Parse(token[0]); double b = double.Parse(token[1]); double c = double.Parse(token[2]); Console.Write("{0}x^2 + {1}x + {2} = 0\n", a, b, c); double x, y; int type; CalcRoot(a, b, c, out type, out x, out y); if(type == 0) Console.Write("x = {0}, {1}\n", x, y); else if(type == 1) Console.Write("x = {0} ±i {1}\n", x, y); else Console.Write("x = {0}\n", x); } /// <summary> /// 2次方程式 ax^2 + bx + c = 0 の根を求める /// </summary> /// <param name="a">2次の係数</param> /// <param name="b">1次の係数</param> /// <param name="c">定数項</param> /// <param name="type">根のタイプ。0:実数根2つ、-1:重根1つ、1:虚数根</param> /// <param name="x">根1(虚数根の場合、根の実部)</param> /// <param name="y">根2(虚数根の場合、根の虚部)</param> static void CalcRoot( double a, double b, double c, out int type, out double x, out double y) { b /= 2; double d = b * b - a * c; if(d < 0) { type = 1; x = -b / a; y = Math.Sqrt(-d) / a; return; } if(d > 0) { type = 0; double t1 = -b; double t2 = Math.Sqrt(d); x = (t1 + t2) / a; y = (t1 - t2) / a; return; } type = -1; x = -b / a; y = x; } }