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

C言語から学ぶC#とJava③ 引数編(値型・参照型)

どもども、やる気のないブログが突然更新しまくりなはっし~だす。
大体いつも仕事終わりに酒飲みながら書いてるんですが、おかしな文面や間違ってるところがあったら指摘お願いします。
※内容的には極力C言語から入った初級者に分かるように心掛けた内容です(多分・・・)

今回は引数編です。
※この回は長くなりそうなので、複数回に分けようと思います。

C言語では、値渡しとアドレス渡しの2種類を学んだと思いますがC#・Javaでもその応用で十分理解できます。
今回は結論から先に行きたいと思います。

C#・Javaの引数における結論

Javaの引数は値渡しのみ!!!
C#の引数は値渡しとオブジェクト渡し(勝手に命名)!!!

以上です。
いや、それじゃ終われない。いつものようにサンプルコードで確認してみましょう。

値型の引数

private static void ArgTestInt() {
	int a = 1;
	System.out.print("関数前:a=" + a);
	func1(a);
	System.out.println(" 関数後:a=" + a);
}
private static void func1(int a) { 
	a = 2;
	System.out.print(" 関数内:a=" + a); 
}
private static void ArgTest() {
    int a = 1;
    Console.Write("関数前:a=" + a);
    func1(a);
    Console.WriteLine(" 関数後:a=" + a);

    Console.WriteLine("何かキーを押してください...");
    Console.ReadKey();
}
private static void func1(int a) {
    a = 2;
    Console.Write(" 関数内:a=" + a);
}
関数前:a=1 関数内:a=2 関数後:a=1

C言語でも引数の課題で出てくるものですね。

値型の引数は値をコピー!すなわち値渡し!

その認識で問題ないです。

参照型(文字列)の引数

private static void ArgTestString() {				
	String str = "abc";
	System.out.print("関数前:str=" + str + "     ");
	System.out.println("strのID:" + System.identityHashCode(str));
	func2(str);
	System.out.print("関数後:str=" + str + "     ");
	System.out.println("strのID:" + System.identityHashCode(str));
	str = "abc123";
	System.out.print("変更後:str=" + str + "  ");
	System.out.println("strのID:" + System.identityHashCode(str));
}
private static void func2(String str) { 
	System.out.print("関数内:str=" + str + "     "); 
	System.out.println("strのID:" + System.identityHashCode(str));
	str = "abcdef";
	System.out.print("関数内:str=" + str + "  "); 
	System.out.println("strのID:" + System.identityHashCode(str));
}
関数前:str=abc     strのID:1779013548
関数内:str=abc     strのID:1779013548
関数内:str=abcdef  strのID:1535811418
関数後:str=abc     strのID:1779013548
変更後:str=abc123  strのID:293935269

ちょっと待った!!!
参照型なのに値型の挙動(値渡し)じゃないか!!!

そうなんです!挙動は値渡しなんです!
ですが、注目は関数内の変更前IDです。同じ参照先をちゃんと受け取っています。

つまり、参照をコピーしているが、変更する際に新しい参照先へ変更しているという事です。

ちょっと混乱してきましたか?
ですが大丈夫です。このブログのタイトルを思い出してください。C言語の挙動を思い出せば全く問題ありません。

#include <stdio.h>
void func_str(char* str) 
{
	printf("関数内:str=%-6s Addr=%d\n", str, str);
	str = "abcdef";
	printf("関数内:str=%-6s Addr=%d\n", str, str);
}
int main()
{
	char* str = "abc";
	printf("関数前:str=%-6s Addr=%d\n", str, str);
	func_str(str);
	printf("関数後:str=%-6s Addr=%d\n", str, str);
	str = "abc123";
	printf("変更後:str=%-6s Addr=%d\n", str, str);
	return 0;
}
関数前:str = abc    Addr=12036968
関数内:str = abc    Addr=12036968
関数内:str = abcdef Addr=12037572
関数後:str = abc    Addr=12036968
変更後:str = abc123 Addr=12037016

全く同じ挙動ですよね?

ではC#も見てみましょう

static ObjectIDGenerator objectID = new ObjectIDGenerator();
static bool firstTimeFlg;
private static void ArgTestString() {
	string str = "abc";
	Console.Write("関数前:str=" + str + "     ");
	Console.WriteLine("strのID:" + objectID.GetId(str, out firstTimeFlg));
	func2(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 func2(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=abc     strのID:1
変更後:str=abc123  strのID:3

C#も同じ挙動をしていますね。

覚えておくべきことは。C#・Javaでは文字列は参照型だけど値渡しと同じ挙動をしている! ではなく、

C#・Javaでは文字列は参照型だが値を変更するたびに初期化している!

C#・Javaでは(というかほとんどの言語で)文字列はイミュータブル(Immutable)です。
イミュータブルとはザックリ言うと値の変更ダメ!。なので変更時には新たに初期化が必要ってことです。

初期化すれば当然新しく参照先を代入するわけですよ。関数内の変数はあくまでコピーなので、呼び出し元の変数とは別物なんです。
新たな参照先を代入しても呼び出し元の変数には何の影響もなくて当然です。

C言語のポインタをイメージすれば問題なく理解できるはずです。
参照型の変数に直接=で代入するという事は参照を代入してるわけですよ、慣れればそうにしか見えなくなります。

参照型のサンプルとしてはちょっと分かりにくいのでListのサンプルを見てみましょう。

private static void ArgTestList() {	
	List<String> list = new ArrayList<String>();
	list.add("11");
	list.add("22");
	System.out.print("関数前:list=" + String.format("%-18s", list.toString()));
	System.out.println("listのID:" + System.identityHashCode(list));
	func3(list);
	System.out.print("関数後:list=" + String.format("%-18s", list.toString()));
	System.out.println("listのID:" + System.identityHashCode(list));
	list.add("44");
	System.out.print("変更後:list=" + String.format("%-18s", list.toString()));
	System.out.println("listのID:" + System.identityHashCode(list));
	//	結果:
	//	関数前:list=[11, 22]          listのID:492214695
	//	関数内:list=[11, 22]          listのID:492214695
	//	関数内:list=[11, 22, 33]      listのID:492214695
	//	関数後:list=[11, 22, 33]      listのID:492214695
	//	変更後:list=[11, 22, 33, 44]  listのID:492214695
}
private static void func3(List<String> list) { 
	System.out.print("関数内:list=" + String.format("%-18s", list.toString())); 
	System.out.println("listのID:" + System.identityHashCode(list));
	list.add("33");
	System.out.print("関数内:list=" + String.format("%-18s", list.toString())); 
	System.out.println("listのID:" + System.identityHashCode(list));
}
static ObjectIDGenerator objectID = new ObjectIDGenerator();
static bool firstTimeFlg;
private static void ArgTestList() {
	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));
	func3(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));
	//	結果:
	//	関数前: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
	Console.WriteLine("何かキーを押してください...");
	Console.ReadKey();
}
private static void func3(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));
}

参照型引数のサンプルとしてはこっちが分かりやすいでしょうか。

これを参照渡しと呼んでも差し支えないとは思いますが、参照型の変数を引数として渡すんだから参照しかやり取りできないんです。
なので順当に言えばこれも値渡しなんです。
そもそも参照先を全部コピーとか何も気にせずにできるんだったらClone()メソッドを実装する必要なんて無くなりますよ。

余談ですが、文字列型の+連結よりもStringBuilderの方が処理時間が速いというのも、この動きを理解すれば納得なのではないかと思います。なにせ変更するたびに全文字列分の処理が必要ですから。
※私個人的には正直体感できるほどのバカでかくてひたすら繰り返す連結とかほぼないし、どっちでもいいですけど・・・

おっと、ここから先は次回にします。
次回も引き続き引数について(主にC#の ref・out)です。

でわでわ

C言語から学ぶC#とJava② 文字列編 << Back Next >> C言語から学ぶC#とJava④ 引数編(ref/out/in)
LINEで送る
Pocket

コメントを残す

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