ど~も、最近運動を怠り気味のはっし~だす。
前回に引き続き引数編です。今回はC#のみが対象となります。
前回、値型と参照型の引数の振る舞いを確認しました。
C#にはもう一つ(正確には2つ3つでした・・・)引数の振る舞いがあります。ref と out / inです。
前回のサンプルを少し変更して違いを確認してみましょう。
値型の引数(ref)
static ObjectIDGenerator objectID = new ObjectIDGenerator();
static bool firstTimeFlg;
private static void ArgTestIntRef() {
int a = 1;
Console.Write("関数前:a=" + a);
FuncIntRef(ref a);
Console.WriteLine(" 関数後:a=" + a);
Console.WriteLine("何かキーを押してください...");
Console.ReadKey();
}
private static void FuncIntRef(ref int a) {
a = 2;
Console.Write(" 関数内:a=" + a);
}
関数前:a=1 関数内:a=2 関数後:a=2
参照型(イミュータブル以外)と同様の動きですね!
これでもうrefの動きの半分は理解できましたね!
文字列の場合も見てみましょう。
文字列の引数(ref)
static ObjectIDGenerator objectID = new ObjectIDGenerator();
static bool firstTimeFlg;
private static void ArgTestStringRef() {
string str = "abc";
Console.Write("関数前:str=" + str + " ");
Console.WriteLine("strのID:" + objectID.GetId(str, out firstTimeFlg));
Func2Ref(ref str);
Console.Write("関数後:str=" + str + " ");
Console.WriteLine("strのID:" + objectID.GetId(str, out firstTimeFlg));
str = "abc123";
Console.Write("変更後:str=" + str + " ");
Console.WriteLine("strのID:" + objectID.GetId(str, out firstTimeFlg));
Console.WriteLine("何かキーを押してください...");
Console.ReadKey();
}
private static void Func2Ref(ref string str) {
Console.Write("関数内:str=" + str + " ");
Console.WriteLine("strのID:" + objectID.GetId(str, out firstTimeFlg));
str = "abcdef";
Console.Write("関数内:str=" + str + " ");
Console.WriteLine("strのID:" + objectID.GetId(str, out firstTimeFlg));
}
関数前:str=abc strのID:1
関数内:str=abc strのID:1
関数内:str=abcdef strのID:2
関数後:str=abcdef strのID:2
変更後:str=abc123 strのID:3
文字列の場合も同じ動きですね!
関数内の変更後と関数後が同じ参照先であることが確認できます。
前回の冒頭で書いたオブジェクト渡しが理解できたかと思います。「渡し」とつけるとコピーしてるみたいだから「オブジェクト参照」がいいんかな? ref も reference(参照)の略だと思うし。
「半分は理解できた」と言ったからにはまだ続きがあります。
参照型(List)の場合を確認しましょう。
参照型の引数(ref)
static ObjectIDGenerator objectID = new ObjectIDGenerator();
static bool firstTimeFlg;
private static void ArgTestListRef() {
List<string> list = new List<string> { "11", "22" };
Console.Write("関数前:list=" + string.Format("{0,-14}", string.Join(",", list)));
Console.WriteLine("listのID:" + objectID.GetId(list, out firstTimeFlg));
Func3Ref(ref list);
Console.Write("関数後:list=" + string.Format("{0,-14}", string.Join(",", list)));
Console.WriteLine("listのID:" + objectID.GetId(list, out firstTimeFlg));
list.Add("44");
Console.Write("変更後:list=" + string.Format("{0,-14}", string.Join(",", list)));
Console.WriteLine("listのID:" + objectID.GetId(list, out firstTimeFlg));
Console.WriteLine("何かキーを押してください...");
Console.ReadKey();
}
private static void Func3Ref(ref List<string> list) {
Console.Write("関数内:list=" + string.Format("{0,-14}", string.Join(",", list)));
Console.WriteLine("listのID:" + objectID.GetId(list, out firstTimeFlg));
list.Add("33");
Console.Write("関数内:list=" + string.Format("{0,-14}", string.Join(",", list)));
Console.WriteLine("listのID:" + objectID.GetId(list, out firstTimeFlg));
}
関数前:list=11,22 listのID:1
関数内:list=11,22 listのID:1
関数内:list=11,22,33 listのID:1
関数後:list=11,22,33 listのID:1
変更後:list=11,22,33,44 listのID:1
前回と同じ動きですね!
では少し変更してみましょう!
static ObjectIDGenerator objectID = new ObjectIDGenerator();
static bool firstTimeFlg;
private static void ArgTestListRef2() {
List<string> list = new List<string> { "11", "22" };
Console.Write("関数前:list=" + string.Format("{0,-14}", string.Join(",", list)));
Console.WriteLine("listのID:" + objectID.GetId(list, out firstTimeFlg));
Func4Ref(ref list);
Console.Write("関数後:list=" + string.Format("{0,-14}", string.Join(",", list)));
Console.WriteLine("listのID:" + objectID.GetId(list, out firstTimeFlg));
list.Add("44");
Console.Write("変更後:list=" + string.Format("{0,-14}", string.Join(",", list)));
Console.WriteLine("listのID:" + objectID.GetId(list, out firstTimeFlg));
Func5(list);
Console.Write("関数後:list=" + string.Format("{0,-14}", string.Join(",", list)));
Console.WriteLine("listのID:" + objectID.GetId(list, out firstTimeFlg));
Console.WriteLine("何かキーを押してください...");
Console.ReadKey();
}
private static void Func4Ref(ref List<string> list) {
Console.Write("関数内:list=" + string.Format("{0,-14}", string.Join(",", list)));
Console.WriteLine("listのID:" + objectID.GetId(list, out firstTimeFlg));
list = new List<string> { "33" };
Console.Write("関数内:list=" + string.Format("{0,-14}", string.Join(",", list)));
Console.WriteLine("listのID:" + objectID.GetId(list, out firstTimeFlg));
}
private static void Func5(List<string> list) {
Console.Write("関数内:list=" + string.Format("{0,-14}", string.Join(",", list)));
Console.WriteLine("listのID:" + objectID.GetId(list, out firstTimeFlg));
list = new List<string> { "55" };
Console.Write("関数内:list=" + string.Format("{0,-14}", string.Join(",", list)))
Console.WriteLine("listのID:" + objectID.GetId(list, out firstTimeFlg));
}
関数前:list=11,22 listのID:1
関数内:list=11,22 listのID:1
関数内:list=33 listのID:2 ←refありの関数内で仮引数を初期化した場合は
関数後:list=33 listのID:2 ←実引数にも影響する
変更後:list=33,44 listのID:2
関数内:list=33,44 listのID:2
関数内:list=55 listのID:3 ←ref無しの関数内で仮引数を初期化した場合は
関数後:list=33,44 listのID:2 ←実引数には影響しない
参照先の値を変更する分には変わりありませんが、参照が変わった場合の動きに差が出ますね!
通常の値渡しは参照のコピー、ref付きになると参照自体を共有する感じで考えればいいでしょうか?
オブジェクト渡し(もしくはオブジェクト参照)と言ったことが理解できたでしょうか?
引数(ref)のまとめ
参照自体に変更が発生する場合は、「ref」を付けるかどうかを検討する必要あり!
ref付きの場合に関数内で初期化し直したり、「null」を代入すれば当然のように関数外でも元の参照は無くなり、どこからも見放されたオブジェクトはガベージコレクタ(裏でしこしこと働いているメモリ管理職)が見つけたら破棄します。
なので、何となく使用していると思わぬバグを生む可能性があるので、動きを理解してコーディングする必要があります。
他のパラメータ修飾子(out/in)
refについては、理解できたでしょうか?
次は「out」「in」修飾子についてです。とりあえず以下の表を確認してください。
ref | out | in | |
---|---|---|---|
呼出し前に初期化が必要 | 必須 | 不要 | 必須 |
メソッド(関数)内の参照変更・初期化 | 可能 | 必須 | 不可 |
文字列から数値型へ変換するint.TryParse()メソッドがout修飾子を使用するのでサンプルで見てみましょう。
using System;
using System.Collections.Generic;
namespace ConsoleApp1 {
class Program {
static void Main(string[] args) {
OutTest();
}
private static void OutTest() {
string str = "1234";
// 初期化する必要が無いので、この書き方でOK
if(int.TryParse(str, out int outInt)) {
// 変換成功時の処理
Console.WriteLine(string.Format("変換成功:{0}", outInt));
} else {
// 変換失敗時の処理
Console.WriteLine(string.Format("変換失敗:{0}", str));
}
Console.WriteLine("何かキーを押してください...");
Console.ReadKey();
}
}
}
int.TryParseメソッドは文字列をint型へ変換した結果(True/False)を返し、成功時に第2引数のint型変数へ値を格納します。
結果を受け取るint型変数「outInt」は変換後の値を受け取るためのものなので、初期化する必要がありません。
「ref」や「in」では初期化していないといけないので、無駄な初期化が必要になりますが、「out」を使用した場合はスッキリしますね。
それぞれの特性を理解し、仕様に合わせて使い分けることで間違いの起こりにくいコーディングが可能になります。
引数編はこれで終了です。
次回は、配列・リスト・ディクショナリあたりを予定してますが、ちょっと先になりそうです。(今回も大分遅れましたが・・・)
次回のオンライン飲み会勉強会の資料(バージョン管理をやろうかと)を用意したりを先にやらないと間に合いそうにないです・・・。
でわでわ
C言語から学ぶC#とJava③ 引数編(値型・参照型) << Back |