最近C#の話ばっかりだが、部のページでOOP特集を書いてるものでいろいろ調べてたんだ。
さて、そういえばC#の引数は値渡しという話があったがよくわからないのでいろいろ試してみた。
まずこんなクラスを作ってみた。
class myint : IFormattable {
private int v;
public myint(int v0) { v=v0; }
public void incl() { v++; }
public string ToString(string format, IFormatProvider formatProvider) { return v.ToString(format, formatProvider); }
}
中にintを1つ格納しただけのクラスだな。クラスというところは重要、構造体ではない。
だいたい動作は予想できると思うけどね。
その上でこんなものを動かしてみた
int i=10;
myint mi=new myint(i);
Console.WriteLine("{0}", i);
mi.incl();
Console.WriteLine("{0}", i);
Console.ReadKey(false);
さて、これを動かすと両方10が返ってきた。
手順としてはまずmyintのインスタンスを作成するが、このときiの中に入った10という値を変数vに代入した。
なのでこのとき変数vは10を示している。当然ここの値を1増やしたところでiには影響がない。
値渡しというのは仮変数(であってるのか)のv0は、引数に書いた変数、今回はi、を丸ごとコピーするということ。
だから、仮変数のv0と引数に書いた変数iは無関係になっていることはわかるとおもう。
さらに構造体(値型)の代入は中身を丸ごとコピーすることで行うので、コンストラクタでvに代入した時点で仮変数のv0とは無関係。
そのv0をinclメソッドでいじろうがこれも無関係。
だからinclメソッドを動かす前後でiの値は変わってない。これが構造体の特徴ですね。
さて、では中に入れる変数をクラスのインスタンスにしてみよう。
class mymyint : IFormattable {
private myint v;
public mymyint(myint v0) { v=v0; }
public void incl() { v.incl(); }
public string ToString(string format, IFormatProvider formatProvider) { return v.ToString(format, formatProvider); }
}
さっきとだいたい同じものだな。違うのはmyintを納めたものであるということ、
それにともなってinclメソッドがそこからさらにmyintのインスタンスのinclメソッドを呼び出している。
これでもってこんなものを動かしてみる。
myint mi=new myint(10);
mymyint mmi=new mymyint(mi);
Console.WriteLine("{0}", mi);
mmi.incl();
Console.WriteLine("{0}", mi);
Console.ReadKey(false);
まぁさっきとあまりかわらない。結果は前が10、後が11。
なぜこうなるのかという話だが、クラスのインスタンスの変数はCで言うところのポインタだ。
なので、miには1行目で、たとえば「甲地を見よ」と書いたものを代入されたようなものだ。
その上でmmiのコンストラクタに引数として渡した。値渡しなのでコピーされると言った。
しかしコピーされたのは、「甲地を見よ」ということだけ。
だからもしこのコンストラクタの中でその仮変数v0の中身にコンストラクタで全く新しいインスタンスを代入して、
たとえばv0が「乙地を見よ」に変わったとしても、値渡しだから、引数に書いたmiには反映されない。
けど今回はそんなことせず、そのv0をただvに代入しています。
こうなると、miとvはそれぞれ同じ場所を示していることがわかりますよね。
その上で、vのinclメソッドを動かします。すると、vの内容だけ変わります。vの値は変わりません!
その上、vもmiも指していることは同じ。だからmiの値は変わってないのに内容はさっきの動作で変わってしまっている。
その結果、miの内容を表示させたら、1増えた11になってたわけです。なるほど。
もしvの内容とmiの内容を無関係にしたければ、改めてvの内容を作って、miと同じ内容をコピーしないといけない。
このためにコピーコンストラクタというのが欲しくなるわけか。
さて、この特徴を知ったら、foreachで内容が変えられるような気がしてきました。
そこでこんなクラスを自作しました。完璧じゃないけどある程度は動く。
class changeable<T> {
private T _v;
public changeable(T v0) { _v=v0; }
public changeable(changeable<T> v0) { _v=v0._v; }
public void change(T v0) { _v=v0; }
public static implicit operator changeable<T>(T v0) { return new changeable<T>(v0); }
public static implicit operator T(changeable<T> v0) { return v0._v; }
}
Nulltable構造体を参考に作ってみました。まぁこれはクラスですが。
このクラスのインスタンスのchangeメソッドで中身を書き換えることができる。
というものなのだが、これがどう有用かは見て欲しい。
changeable<string>[] strs=new changeable<string>[] { "ABC", "DEF", "GHI" };
foreach (changeable<string> cstr in strs) { cstr.change(cstr+"!"); }
foreach (string nstr in strs) { Console.WriteLine(nstr); }
無理矢理一行で書いたから読みにくいね。
で、changeable<T>の配列を作りました。Tからchangeable<T>には暗黙的に変換されるのでこれでいいです。
この上で、foreachで1つづつ回してもらう。
foreach内でその仮変数に代入するということは許されない。まぁ意味ないからね。
普通のstringの配列は、「甲地を見よ」などがいくつか入っていて、
foreachでは毎度、「甲地を見よ」というようなことを仮変数に入れて回してくれる。
だから内容を読むことは自由自在だ。だけど、仮変数を変えても意味はないね。
そこで、この「甲地を見よ」というものを収納したchangeable<T>のインスタンスの配列。
そこには別の「乙地を見よ」などが書いてあって、これを毎度回してくれる。
その上で、乙地を見て、乙地に書いてある、「甲地を見よ」を「丙地を見よ」に書き換えたとする。
これがchangeメソッド。確かにこれは問題じゃない。だって仮変数は変えてないから。
それでこのforeachのループが終わった後、回した配列、今回ならstrsを改めて観察する。
当然だけど、「乙地を見よ」などと書いてあるのは変わってない。
だけど乙地を見てみると、「甲地を見よ」が「丙地を見よ」と内容が変わっている。
というわけで、指すところは変えてないのに、そこから指す場所を変えて、内容を変えてしまったと。
おもしろいね。foreachをこうやって使えるとは。