はじめに
おそらくは、大部分の方は、感覚的に使い分けれていると思いますが、なぜそうなるかを理解できていないと、思わぬところで不具合を生み出します。
- クラスの比較は、原則、== ではなく、.Equals() で比較しないといけない。
- 関数の引数の基本は値渡しなのに、DataTable を引数で渡して、関数内で値を変更すると、関数の外でも変更結果が反映されている。
- DataRow row = data.Rows[0]; のように、別変数に代入した row の値を変更すると、代入元 data.Rows[0] の値も変更されている。
- 一方、計算結果の decimal 変数の値を別変数に代入した後に、計算結果の decimal 変数の値を変更しても、すでに代入した値は変更されない。
注意点
- 若手に説明するために作ったため、厳密にいえば正確ではない説明もあります。
- 誤ったコーディングにならない程度に理解するための説明として見ていただければと思います。
- コメントにて、内容に対する指摘、訂正、フォローしてくださっている方がいます。
- 本来なら、記事を編集していくべきなのですが、なかなかうまく反映しきれていないので、ぜひ、コメントも一読してください。
※誤ったコーディングにつながると感じる内容がありましたら指摘いただけると助かります。
概要
値型
- 数値型(int、long、float、double、decimal など)、bool、ユーザー定義の構造体(struct)。
- 値型の変数はデータを格納する。
- string は参照型だが、値型に近い動作をするように制御されているので、値型のイメージで捉えた方が理解しやすい。
int i = 3;
【イメージ】(i には 1 が格納される。)
i ----> 1
参照型
- クラス。
- 参照型の変数は実データへの参照を格納する。
var s = new StringBuilder("a");
【イメージ】(s には "a" が格納されているアドレスが格納される。)
i ----> [データのアドレス] ----> "a"
動作確認
値型の値変更
string 変数を別 string 変数に代入して値を変更した場合、それぞれの値はどのようになるでしょう?
実行ソース(1)
static void Main(string[] args)
{
string a1 = "aaa";
string a2 = a1;
a2 = "bbb";
Console.WriteLine(string.Format("{0} : {1}", nameof(a1), a1));
Console.WriteLine(string.Format("{0} : {1}", nameof(a2), a2));
}
実行結果(1)
a1 : "aaa"
a2 : "bbb"
解説(1)
【イメージ】
a1 ----> "aaa"
a2 ----> "aaa" ※ a1 の値("aaa")を a2 に設定。
a2 ----> "bbb" ※ a2 の値("aaa")を "bbb" に再設定。
↓
a1 ----> "aaa"
a2 ----> "bbb"
値型の場合は、変数そのものに値が格納されます。
ですので、個々の変数は完全に独立し、a2 の値を変更した場合は、a2 の値だけが変わります。
参照型の値変更
StringBulider 変数を別 StringBulider 変数に代入して値を変更した場合、それぞれの値はどのようになるでしょう?
実行ソース(2)
static void Main(string[] args)
{
var b1 = new StringBuilder("aaa");
var b2 = b1;
b2.Append("bbb");
Console.WriteLine(string.Format("{0} : {1}", nameof(b1), b1.ToString()));
Console.WriteLine(string.Format("{0} : {1}", nameof(b2), b2.ToString()));
}
実行結果(2)
b1 : "aaabbb"
b2 : "aaabbb"
解説(2)
【イメージ】
b1 -- [アドレスX] --> "aaa"
b2 -- [アドレスX] --> "aaa" ※ b1 のアドレス([アドレスX])を設定。
b2 -- [アドレスX] --> "aaabbb" ※ [アドレスX]に紐づく値に"bbb"を追加。
↓
b1 -- [アドレスX] --> "aaabbb"
b2 -- [アドレスX] --> "aaabbb"
参照型の場合は、変数には値に紐づくアドレスが格納されます。
ですので、b1 に b2 を代入した場合は、両方の変数には同一の値に紐づくアドレスが格納されるため、b2 の値を変更した場合は、b1 の値も変わります。
値型(関数の引数)
string 変数を、関数に「値渡し」と「参照渡し」で設定し、関数内で値を変更した場合、呼び出し元の変数の値はどうなるでしょう?
実行ソース(3)
static void Main(string[] args)
{
string c1 = "aaa";
string c2 = "aaa";
Test3(c1, ref c2);
Console.WriteLine(string.Format("{0} : {1}", nameof(c1), c1));
Console.WriteLine(string.Format("{0} : {1}", nameof(c2), c2));
}
private static void Test3(string pv, ref string pr)
{
pv = "bbb";
pr = "bbb";
}
実行結果(3)
c1 : "aaa"
c2 : "bbb"
解説(3)
【イメージ】
<値型:値渡し>
c1 -----> "aaa"
↓
pv -----> "aaa" ※ c1 の値("aaa")を設定。
pv -----> "bbb"
↓
c1 -----> "aaa"
<値型:参照渡し>
c2 --------> "aaa"
↓
pr -- c2 --> "aaa" ※ c2 そのものを pr として使用。
pr -- c2 --> "bbb"
↓
c2 --------> "bbb"
引数の値渡しの場合(c1→pv)は、受け取った値("aaa")を変数を引数に格納して処理されます。ですので、pv の値を変更しても、呼び出し元の c1 の値は変更されません。
一方、引数の参照渡しの場合(c2→pr)は、受け取った変数(c2)を引数に格納して処理されます。ですので、pr == c2 となり、pr の値を変更すると、c2 の値も変更されます。
参照型(関数の引数①)
StringBuilder 変数を、関数に「値渡し」と「参照渡し」で設定し、関数内で値を変更した場合、呼び出し元の変数の値はどうなるでしょう?
実行ソース(4)
static void Main(string[] args)
{
var d1 = new StringBuilder("aaa");
var d2 = new StringBuilder("aaa");
Test4(d1, ref d2);
Console.WriteLine(string.Format("{0} : {1}", nameof(d1), d1.ToString()));
Console.WriteLine(string.Format("{0} : {1}", nameof(d2), d2.ToString()));
}
private static void Test4(StringBuilder pv, ref StringBuilder pr)
{
pv.Append("bbb");
pr.Append("bbb");
}
実行結果(4)
d1 : "aaabbb"
d2 : "aaabbb"
解説(4)
【イメージ】
<参照型:値渡し>
d1 -- [アドレスX] -----> "aaa"
↓
pv -- [アドレスX] -----> "aaa" ※ d1 のアドレス([アドレスX])を設定。
pv -- [アドレスX] -----> "aaabbb"
↓
d1 -- [アドレスX] -----> "aaabbb"
<値型:参照渡し>
d2 -------- [アドレスY] --> "aaa"
↓
pr -- d2 -- [アドレスY] --> "aaa" ※ d2 そのものを pr として使用。
pr -- d2 -- [アドレスY] --> "aaabbb"
↓
d2 -------> [アドレスY] --> "aaabbb"
引数の値渡しの場合(d1→pv)は、受け取ったアドレスを変数を引数に格納して処理されます。ですので、d1 と pv は別変数ですが、同一アドレスの値と紐づくため、pv の値を変更すると、呼び出し元の c1 の値も変更されます。
引数の参照渡しの場合(d2→pr)は、受け取った変数(d2)を引数(pr)に格納して処理されます。ですので、pr == d2 となり、pr の値を変更すると、d2 の値も変更されます。
参照型(関数の引数②)
参照型の変数を関数の引数として設定し、関数内で new した場合、呼び出し元の参照型の変数値はどうなるでしょう?
実行ソース(5)
static void Main(string[] args)
{
var e1 = new StringBuilder("aaa");
var e2 = new StringBuilder("aaa");
Test5(e1, ref e2);
Console.WriteLine(string.Format("{0} : {1}", nameof(e1), e1.ToString()));
Console.WriteLine(string.Format("{0} : {1}", nameof(e2), e2.ToString()));
}
private static void Test5(StringBuilder pv, ref StringBuilder pr)
{
pv = StringBuilder("bbb");
pr = StringBuilder("bbb");
}
実行結果(5)
e1 : "aaa"
e2 : "bbb"
【イメージ】
<参照型:値渡し>
e1 -- [アドレスX] -----> "aaa"
↓
pv -- [アドレスX] -----> "aaa" ※ e1 のアドレス([アドレスX])を設定。
pv -- [アドレスA] -----> "bbb"
↓
e1 -- [アドレスX] -----> "aaa"
<値型:参照渡し>
e2 -------- [アドレスY] --> "aaa"
↓
pr -- e2 -- [アドレスY] --> "aaa" ※ e2 そのものを pr として使用。
pr -- e2 -- [アドレスB] --> "bbb"
↓
e2 -------> [アドレスB] --> "bbb"
解説(5)
引数の値渡しの場合(e1→pv)は、受け取ったアドレスを変数を引数に格納して処理されます。ですので、d1 と pv は別変数ですが、同一アドレスの値と一旦は紐づきます。しかし、pv を new して格納されるアドレスが新しくなるため、pv と d1 のつながりが断たれ、pv の値を変更しても、呼び出し元の e1 の値は変更されません。
引数の参照渡しの場合(e2→pr)は、受け取った変数(e2)を引数(pv)に格納して処理されます。ですので、pr == e2 となり、pr を new して格納するアドレスを新しくしても、e2 に格納されるアドレスもあわせて変更されるため、pr を変更すると、e2 も変更されます。
編集後記
ポインタの概念を理解している方は、まず問題ないです。Javaを知っている方は、String の比較で == を使うと意図しない動作をするので、感覚的に理解できている場合が多いです。C#から始めた若手のコードレビューしてたら、言葉は聞いたことがあるかも程度が結構いたので、今回まとめてみました。
が、自分自身も漠然としたイメージで捉えているようで、有益な指摘を頂いてます。ありがたいです。おおいに勉強させていただいてます。