C++からC#に移ってきて一番最初にモヤっとするのが、outとrefの使い分け。
関数の引数にrefを付けた場合とoutとでどう違うのか、レファレンスを読んで頭で理解できても、具体的にどういうケースで使い分ければいいのか(あるいは使わないのか)は判断しづらい。
それというのも、大抵のレファレンスは、refとoutを付けた場合の動作を列挙しているばかりで使い分けに言及していなかったり、値型と参照型に絡めて説明していないから。
そこで今回は、refとoutの使い分けをわかりやすく説明する(お)。
refとoutその前に
どういうケースでref / outを使い分けるべきか?は、その修飾子を付けるオブジェクトの種類によって二分できる。 なので使い分けを説明する前に、その種類について説明しよう。
1. 値型
C#には値型と参照型という2つの種類があり、値型のオブジェクトは数値(int)や構造体(struct)を指す。
この種類のオブジェクトを関数へ渡すとき、そのコピーが渡される。
refとoutを理解する上で、ここが重要になる。
2. 参照型
一方の参照型は、クラスにあたるオブジェクトで、stringやList<T>など、C#のほとんどのオブジェクトがクラスタイプ。
参照型のオブジェクトを関数へ渡すと、その参照が渡され、コピーはされない。
refとoutの使い分け
コーディング中、refとoutのどちらを使う、あるいは使わない(修飾子なし)かを迷ったとき、まずはそのオブジェクトが値型か参照型かを考え、次に、渡す先の関数でオブジェクトをどうしたいかを考える。
値型か参照型か?の2パターンに対して、別関数内でオブジェクトを使いたいだけかor書き換えたいかor初期化したいか?の3パターンを掛けて6パターン分の使い分けがある。
オブジェクトの種類 | (呼び出し元のオブジェクトを)書き換える | 初期化する | 書き換えない |
値型 | ref | out | 修飾子なし |
参照型 | 修飾子なし(refでも同じ) | out | ※参照型の場合は書き換えられてしまう。 |
C++erがC#をやって戸惑うのが、関数の引数にconstを付けられないということ。
だから、参照型のオブジェクトを関数に渡し、その先で書き換えられないようにすることはできない。各自書き換えないよう気をつけるべし!
呼び出し元のオブジェクトを書き換える場合、参照型は修飾子なしでもrefでも同じ動作になる(どちらも書き換えられる)けれど、ここは修飾子なしでいこう。refを付けてると、値型と参照型の違いを知らないと思われる、かも?
値型のそれぞれ
値型のオブジェクトの3パターンの作例がこちら。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public static void Main(string[] args) { // For value type object. int valueTarget = 1; // [修飾子なし] 関数へはvalueTargetのコピーが渡されるから1のまま。 AddOne(valueTarget); Console.WriteLine("valueTarget : " + valueTarget.ToString()); // [ref] refをつけるとvalueTargetの参照を渡せるため、2に書き換わる AddOne(ref valueTarget); Console.WriteLine("valueTarget : " + valueTarget.ToString()); // [out] outは、別関数内で初期化する場合に使う。 int valueTarget2; Initialize(out valueTarget2); Console.WriteLine("valueTarget : " + valueTarget2.ToString()); } private static void AddOne(int target) { ++target; } private static void AddOne(ref int target) { ++target; } private static void Initialize(out int target) { target = 100; } |
参照型のそれぞれ
参照型オブジェクトだとこうなる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public static void Main(string[] args) { // For reference type object. var referenceTargets = new List<string>{ "a", "b", "c" }; // [修飾子なし] referenceTargetsの参照が関数に渡されるから、新たな要素が追加される。 AddText(referenceTargets); const string separator = ", "; Console.WriteLine( "referenceTarget : " + string.Join(separator, referenceTargets.Select(target => target.ToString()))); // [ref] 参照型のオブジェクトは関数へ参照を渡すようになっているから、refを付ける意味はない。 AddText(ref referenceTargets); Console.WriteLine( "referenceTarget : " + string.Join(separator, referenceTargets.Select(target => target.ToString()))); // [out] 参照型のオブジェクトで一番使うのがout。別関数内でオブジェクトを初期化したいときに使う。 Initialize(out referenceTargets); Console.WriteLine( "referenceTarget : " + string.Join(separator, referenceTargets.Select(target => target.ToString()))); } private static void AddText(List<string> targets) { targets.Add("No ref and out."); } private static void AddText(ref List<string> targets) { targets.Add("ref"); } private static void Initialize(out List<string> targets) { targets = new List<string>{ "Initialized by external method" }; } |
C#おすすめ図書
他の言語をやったことがあって、その言語の作法でC#を書いちゃったことのある人へ
他人にとって読みやすいコードを書くための永遠の名著