値渡しと参照渡し (と参照の値渡し)
値渡しと参照渡しは、分かってしまえば何も難しいところはないんだけど、分かるまでにちょっとした壁があるというかなんとうか・・・
てことでちょっとまとめておきますねー
値渡し (call-by-value) と参照渡し (call-by-reference) の違い
値渡しと参照渡しの違いは、「呼出し元の値自体を変更できるかどうか」と説明されることが多い。
しかし、例えば Java ではミュータブルなオブジェクト *1 を渡した場合、呼出し元の値自体を変更できるという勘違いをする可能性があるため、この説明はあまり好ましくない。
そのため、参照渡しを「呼出し元の別名を渡している」と覚えるのが分かりやすいと思う。
値渡しは「何かの値をコピーして渡している」と覚える*2。
Java の場合
Java には値渡ししか存在しないが、「参照型」のためにややこしく感じる。
参照型は参照渡しとは無関係で、C や C++ の知識があるなら、むしろ「メンバに .(ドット) でアクセスするポインタ型*3」と考えた方が分かりやすい*4。
実際、Java言語仕様 第3版 (The Java Series) には、
参照値 (しばしば単に参照 (reference) とも呼ばれる) は, こういったオブジェクトへのポインタ (pointer) や, どのオブジェクトも参照しない特殊な null 参照となる。
Java 言語仕様 第3版 (The Java Series) (P.43)
とあるため、単に「参照」といった場合には参照値、すなわちポインタを表す*5。
また、参照型の変数はその値として参照値、すなわちポインタを保持する。
まとめると、
となる。
final class Hoge { int i; } public final class Main { static void method1(int[] a) { int[] other = { 10 }; // Java には値渡ししかないため、 // 引数に直接代入しても呼出し元には影響しない a = other; } static void method2(int[] a) { // 参照を値渡ししているため、 // その参照(ポインタ)が指す先は変更できる a[0] = 100; } static void method3(Hoge h) { Hoge other = new Hoge(); other.i = 10; // method1と同様、呼出し元には影響しない h = other; } static void method4(Hoge h) { // method2と同様、 // 参照を値渡ししているため、 // その参照(ポインタ)が指す先は変更できる h.i = 100; } public static void main(String[] args) { int[] array = { 0 }; System.out.println(array[0]); // => 0 method1(array); System.out.println(array[0]); // => 0 method2(array); System.out.println(array[0]); // => 100 Hoge hoge = new Hoge(); System.out.println(hoge); // => 0 method3(hoge); System.out.println(hoge); // => 0 method4(hoge); System.out.println(hoge); // => 100 } }
このように、引数に直接代入すると言うことは、引数の参照値を書き換えているにすぎないため、呼出し元に影響はない (method1 と method3)。
それに対して、引数自体ではなく、それが指すオブジェクトの内部状態は変更が可能である (method2 と method4) *6。
C++ の場合
C++ には参照*7があるため、これを使用することで参照渡しが出来る。
#include <iostream> struct hoge { int i; hoge() : i(0) {} }; void func1(hoge h) { hoge other; other.i = 10; // 値渡しなので呼出し元には影響しない h = other; } void func2(hoge& h) { hoge other; other.i = 100; // 参照渡しなので呼出し元に影響する h = other; } int main() { hoge hog; std::cout << hog.i << std::endl; // => 0 func1(hog); std::cout << hog.i << std::endl; // => 0 func2(hog); std::cout << hog.i << std::endl; // => 100 }
このように、func2 により呼出し元自体が書き換えられている。
func2 での h は呼出し元の main の hog の別名にすぎず、同じものであるため、h に代入することはつまり hog に代入することと同じであるため、呼出し元の hog が書き換わっている。
逆に、func1 では値渡しを行っているため、func1 での h は main での hog のコピーであり、同一のものではないため、呼出し元が書き換わらない。
C++ にはポインタもあるが、Java の参照と違い、ポインタの先をたぐり寄せることが出来るため、参照渡しと同じ事*8が出来る。
ただし、これは参照渡しではなく、ポインタの値を値渡ししているだけなので注意が必要。
void func3(hoge* h) { // h自体に代入しても、呼出し元には影響しない h = 0; } void func4(hoge* h) { hoge other; other.i = 10; // ポインタを「たぐり寄せる」ことで、呼出し元を操作できる // ただし、関数の引数は値渡ししているだけ *h = other; } void func5(hoge*& h) { // ポインタを参照渡しているため、呼出し元もNULLとなる h = 0; } int main() { hoge hog; std::cout << hog.i << std::endl; // => 0 // func3ではNULLポインタを代入しているため、 // ポインタが参照渡しされればエラーとなるが、 // ポインタは値渡しされるためエラーにならない func3(&hog); std::cout << hog.i << std::endl; // => 0 func4(&hog); std::cout << hog.i << std::endl; // => 10 hoge* p_hog = &hog; func5(p_hog); std::cout << (p_hog == 0 ? "NULL" : "NOT NULL") << std::endl; // => NULL }
C# の場合
C# の場合、Java と同様の参照型の他に、値型も持つが、基本的には Java と同じく値渡しとなる。
ただし、C# では ref キーワードにより参照渡しにも対応する*9。
using System; sealed class Hoge { public int i; } sealed class Program { static void Method1(Hoge h) { Hoge other = new Hoge(); other.i = 10; // デフォルトでは値渡しのため、 // 引数に直接代入しても呼出し元には影響しない h = other; } static void Method2(Hoge h) { // Java同様、参照を値渡ししているため、 // その参照が指す先は変更できる h.i = 100; } static void Method3(ref Hoge h) { Hoge other = new Hoge(); other.i = 10; // 参照渡ししているため、 // hそのものに代入すると呼出し元も影響を受ける h = other; } public static void Main() { Hoge hog = new Hoge(); Console.WriteLine(hog.i); // => 0 Method1(hog); Console.WriteLine(hog.i); // => 0 Method2(hog); Console.WriteLine(hog.i); // => 100 // 参照渡しには呼出し側でもrefが必要 Method3(ref hog); Console.WriteLine(hog.i); // => 10 } }
このように、デフォルトでは値渡しだが、ref を用いることで参照渡しも可能である*10。
この点は Java に比べ便利なのだが、C# ではユーザ定義の値型があるため、値型を参照型のつもりで扱ってしまう恐れがある。
例えば、上の Hoge クラスを Hoge 構造体に変えただけで、出力も変わってしまう。
// structに変えただけ struct Hoge { public int i; } // こちらは全く変更しない sealed class Program { static void Method1(Hoge h) { Hoge other = new Hoge(); other.i = 10; h = other; } static void Method2(Hoge h) { // Hogeが値型のため、呼出し元のhogと // Method2のhは全くの別物(ただしhはhogのコピー)となり、 // そのhのメンバに対して何かを代入しても、 // それが呼出し元に影響することはない h.i = 100; } static void Method3(ref Hoge h) { Hoge other = new Hoge(); other.i = 10; h = other; } public static void Main() { Hoge hog = new Hoge(); Console.WriteLine(hog.i); // => 0 Method1(hog); Console.WriteLine(hog.i); // => 0 Method2(hog); Console.WriteLine(hog.i); // => 0 Method3(ref hog); Console.WriteLine(hog.i); // => 10 } }
正直、よほどパフォーマンス要求が厳しくない限り、値型は使いたくない。
*1:内部状態 (要はフィールド) が変更可能なオブジェクトのこと。例えば、StringBuilder とか
*2:Java の場合参照値を渡しているし、C++ の場合値そのものやポインタの値を渡しているし、C# の場合は参照値もしくは値そのものを渡している
*3:ただしポインタ演算はできない
*4:ちなみに、C++ の「参照」とは全くの中別物なので注意が必要
*5:null 参照は厳密には NULL ポインタに対応するわけではないが、NULL ポインタと考えておいても基本的には問題ない
*6:もちろんこれは参照渡しではない
*7:Java での参照 (ポインタ) と違い、本当の参照 (別名)
*8:すぐ後にもあるが、これはポインタが参照渡しということではなく、あくまで「参照渡しと同じこと (呼び出し元の変数を操作できる) が実現できる」というだけ
*9:C++ のような参照は存在せず、メソッドの引数でのみ ref が使用できる
*10:出力引数用に out というキーワードも用意されている
*11:といっても C# と基本同じ。VB6 では ByRef がデフォルトだったけど、それ以降は ByVal がデフォルトになったことだけ注意すれば OK