ここではC#のメソッドの値渡しと参照渡しの違いを解説します。初心者向けのわかりやすさ重視で書いてますので説明の厳密性には欠ける場合がありますので、ご注意ください。
値型と参照型
値渡しと参照渡しの前に、そもそも変数の型には値型と参照型があります。値型の変数には値が直接格納され、参照型の変数には参照情報が格納されます。参照型の実体は参照情報(ポインタ)の示すアドレスに格納されています。値型や参照型の変数の格納先をスタックと呼び、参照型の実体の格納先をヒープと呼びます。string型以外の組み込み型や構造体は値型で、string型や配列やクラスは参照型です。
- 変数には値型と参照型がある
- 値型の値はスタックに格納されている
- 参照型は参照情報がスタックに格納され、実体である値はヒープに格納されている
- 値型の代表的なものはstring型以外の組み込み型や構造体
- 参照型の代表的なものはstring型や配列やクラス
値渡しと参照渡し
このように変数の型にも2種類がありますので、メソッドの引数の受け渡しの方法としても、値渡しと参照渡しがあります。C#ではデフォルトでは値渡しになり、参照渡しでは関数の引数の前にrefを付けます。
次のメソッドは値渡しです。
コード
static void func(int x) //この関数は値渡しです。
{
//処理
}次のメソッドは参照渡しです。引数の前にはrefがつきます。
コード
static void func(ref int x) //この関数は参照渡しです。
{
//処理
}このように値型と参照型の変数に対して、値渡しと参照渡しの2種類のメソッドの形があります。ここからは①値型の値渡し、②値型の参照渡し、③参照型の値渡し、④参照型の参照渡し、の4つの形に分けて解説していきます。
値型の値渡し
まずは一番わかりやすい値型の値渡しです。値型の値渡しでは、呼び出し元の実引数が、メソッドの仮引数にコピーされます。したがってメソッド内で仮引数の値が書き換えられても、呼び出し元の実引数には影響しません。
コード
using System;
internal class Program
{
static void Main(string[] args)
{
int val = 1;
func(val);
Console.WriteLine("{0}={1}",nameof(val),val);
}
static void func(int x)
{
x = 2 * x;
}
}
出力結果
val=1
上記の例では、Mainメソッド内で値を2倍にする関数func()を呼び出していますが、呼び出し元の実引数のvalの値に変わりはなく、val=1が出力されます。
値型の参照渡し
次は値型の参照渡しです。参照渡しでは実引数のアドレスが仮引数に渡されます。値渡しとの違いは次のコードに示すように、メソッドの仮引数の定義の前にrefキーワードをつけることと、呼び出し元の実引数の前にもrefキーワードをつけることです。この結果、メソッド内での仮引数のアドレスと実引数のアドレスは同一となります。これによりメソッド内での仮引数の変更は、実引数の値にも反映されます。
コード
using System;
internal class Program
{
static void Main(string[] args)
{
int val = 1;
func(ref val);
Console.WriteLine("{0}={1}",nameof(val),val);
}
static void func(ref int x)
{
x = 2 * x;
}
}出力結果
val=2
上記の例でもMainメソッド内で値を2倍にする関数func()を呼び出しています。違いはメソッドの定義と呼び出し側の引数の前にrefがついている点です。これにより、funcメソッド内のxには、valのアドレスが渡されます。この結果、func内で2倍されたxは、そのままvalの値に反映されますので、出力結果はval=2となります。
参照型の値渡し
次は参照型の値渡しです。個人的にはこれが一番わかりにくい動作をします。参照型としてクラスを考えてみましょう。参照型の値渡しは、値型の値渡しと同じく呼び出し元の実引数が、メソッドの仮引数にコピーされます。しかし、ここでコピーされる実引数は、参照型ではアドレス情報になります。したがって、呼び出し元と呼び出されたメソッド内では同じアドレス参照が共有されているため、メソッド内で書き換えられた値は呼び出し元にも反映されます。しかし、メソッド内でアドレス参照自体が書き換えられると、その後にメソッド内で書き換えられた値は呼び出し元に反映されません。よくわからないと思うので、実際のコードを見てみましょう。
コード
using System;
internal class Person
{
public int age; //年齢
public int weight; //体重
public Person(int a, int w)
{
this.age = a;
this.weight = w;
}
}
internal class Program
{
static void Main(string[] args)
{
Person psn = new Person(20, 50);
func1(psn);
Console.WriteLine("func1:psn.age={0}, psn.weight={1}", psn.age, psn.weight);
func2(psn);
Console.WriteLine("func2:psn.age={0}, psn.weight={1}", psn.age, psn.weight);
}
static void func1(Person p)
{
p.age = 30; //呼び出し元にも反映される
}
static void func2(Person p)
{
p.weight = 60; //呼び出し元にも反映される
p = new Person(40, 70); //アドレス参照が書き換えられるので呼び出し元に反映されない
p.age = 100; //呼び出し元には反映されない
}
}
出力結果
func1:psn.age=30, psn.weight=50
func2:psn.age=30, psn.weight=60
このコードでは参照型の例としてPersonクラスを作成しています。Mainメソッド内では最初にfunc1を呼び出し次にfunc2を呼び出しています。
func1では仮引数として渡されたアドレス参照先のageの値が書き換えられます。この場合の仮引数は実引数と同じアドレス参照のため、呼び出し元でも値が変更されます。
次にfunc2が呼び出されますが、ここでは最初にp.weight=60;でweightの値が書き換えられますが、その次の行でpに別のインスタンスが代入されています。この時点で実引数から仮引数にコピーされたアドレス参照は書き換えられてしまいますので、この34行目以降の処理結果は、呼び出し元には反映されません。
参照型の参照渡し
次の参照型の参照渡しは、 値型と同じく実引数のアドレスが仮引数に渡されますが、ここで渡される実引数そのものが参照型のアドレスになります。以下に実際のコードを示します。参照型の値渡しのコードとの違いは、func1とfunc2の実引数および仮引数にrefキーワードがついているだけです。
コード
using System;
internal class Person
{
public int age; //年齢
public int weight; //体重
public Person(int a, int w)
{
this.age = a;
this.weight = w;
}
}
internal class Program
{
static void Main(string[] args)
{
Person psn = new Person(20, 50);
func1(ref psn);
Console.WriteLine("func1:psn.age={0}, psn.weight={1}", psn.age, psn.weight);
func2(ref psn);
Console.WriteLine("func2:psn.age={0}, psn.weight={1}", psn.age, psn.weight);
}
static void func1(ref Person p)
{
p.age = 30; //呼び出し元にも反映される
}
static void func2(ref Person p)
{
p.weight = 60; //呼び出し元にも反映される
p = new Person(40, 70); //アドレス参照が書き換えられても呼び出し元に反映される
p.age = 100; //呼び出し元には反映される
}
}出力結果
func1:psn.age=30, psn.weight=50
func2:psn.age=100, psn.weight=70
このコードでfunc1ではアドレス参照が参照渡しされます。したがって、func1で書き換えられたageの値はアドレス参照の参照渡しを通して、呼び出し元にも反映されます。また、func2の34行目でアドレス参照が変更されていますが、この変更も参照渡しのため、呼び出し元に反映されます。この点が参照型の値渡しと異なるところです。
outキーワード
refキーワードと同じように参照渡しをするキーワードがoutキーワードです。これはメソッドに値を渡すのではなく、メソッドから値を受け取るために使用します。メソッド内ではoutキーワードのついた仮引数は未割当の変数として扱われます。outキーワードに値型、参照型の区別はありません。
コード
using System;
internal class Person
{
public int age; //年齢
public int weight; //体重
public Person(int a, int w)
{
this.age = a;
this.weight = w;
}
}
internal class Program
{
static void Main(string[] args)
{
Person psn; //初期化不要
func(out psn);
Console.WriteLine("func:psn.age={0}, psn.weight={1}", psn.age, psn.weight);
}
static void func(out Person p)
{
p = new Person(20, 60); //メソッド内で仮引数に値が割り当てられる
}
}出力結果
func:psn.age=20, psn.weight=60
終わりに
本記事では、C#の値渡しと参照渡しについて説明しました。特にサンプルコードは単純ですので、違いがわかりやすかったのではないかと思います。特に参照型の値渡しは、初心者から見ると一見、参照渡しのような動作をしますので、参照渡しとの違いが明確になったのではないでしょうか。

