AXIS合同会社代表(熊本在住40代おっさん)が日々戯言を書きなぐるブログです。
C言語から学ぶC#とJava④ 引数編(ref/out/in)

C言語から学ぶC#とJava④ 引数編(ref/out/in)

ど~も、最近運動を怠り気味のはっし~だす。

前回に引き続き引数編です。今回は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」修飾子についてです。とりあえず以下の表を確認してください。

refoutin
呼出し前に初期化が必要必須不要必須
メソッド(関数)内の参照変更・初期化可能必須不可

文字列から数値型へ変換する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
LINEで送る
Pocket

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です