人気ブログランキング | 話題のタグを見る
(.Net)Stringの仕様にやられた
事の発端は下記のように string 型のリストを匿名メソッドを使って処理しようとしたときです。
List<string> lst = new List<string>();
lst.Add("test1");
lst.Add("test2");
lst.Add("test3");
 
lst.ForEach(delegate(string s)
{
s = "hoge";
});
lst.ForEach(Console.WriteLine);

この時期待していたのはすべての文字列が hoge になることでしたが、結果は test1 , test2 , test2 でした。

もしかして...と思って下記のようなテストコード書いてみました。
private void button1_Click(object sender, EventArgs e)
{
string str = "test";
funcA(str);
Console.WriteLine(str);
}
 
private unsafe void funcA(String str)
{
str = "aaa";
Console.WriteLine(str);
}

引数に string 型変数を渡してメソッド内で書き換えてます。呼び出し元での Console.WriteLine の結果は "aaa" を期待してたんですが、"test" のままでした。
string って参照型だから、メソッドへも参照渡しされてメソッド内で変更した場合呼び出し元にも影響するんではという思いは儚くも消え去りました。

で、下記のMSDNをみると、.Net では String オブジェクトは、作成時点以降に値を変更できないことから、不変(変更不可)と呼ばれてるようです。
MSDN:String クラス
MSDN:文字列の使用 (C# プログラミング ガイド)

これで謎が解決しました。
つまり上記の例だと関数内で文字列を変更したときには新しい String オブジェクトが生成されており、それは呼び出しメソッドでは参照できないためこのような結果になったんでしょうね。

さらに、上記の例の時の各変数のアドレスを調べてみました。
private void button1_Click(object sender, EventArgs e)
{
unsafe
{
string str = "test";
fixed (char* p = str)
{
Console.WriteLine("呼び出し前strのポインタ: 0x" + ((int)p).ToString("x"));
funcA(str);
fixed (char* p2 = str)
{
Console.WriteLine("呼び出し後strのポインタ: 0x" + ((int)p2).ToString("x"));
Console.WriteLine(str);
}
}
}
}
 
private unsafe void funcA(String str)
{
fixed (char* p = str)
{
Console.WriteLine("引数strのポインタ(文字列変更前):0x" + ((int)p).ToString("x"));
str = "hoge"; //この時実際は新しい string オブジェクトが別メモリ空間に生成されている
fixed (char* p2 = str)
{
Console.WriteLine(str);
Console.WriteLine("引数strのポインタ(文字列変更後):0x" + ((int)p2).ToString("x"));
}
}
}

結果は下記のようになりました。

呼び出し前strのポインタ: 0x1392e14
引数strのポインタ(文字列変更前):0x1392e14
hoge
引数strのポインタ(文字列変更後):0x13a6438
呼び出し後strのポインタ: 0x1392e14
test

やはり、メソッド内で文字列変更すると、実際には別メモリ空間に新しい string オブジェクトが生成されているので、アドレスが変わってます。
つまり引数で渡された str 変数と文字列変更後の str 変数は別物となってるわけですね。
(また、== 演算子で文字列変更前の str と変更後の str を比較すると別インスタンスになるので false が返ります。)

後、ウォッチ式に &str (str変数のポインタが入ってるアドレス)を追加してて気づいたんですが、funcA に入ると &str の値が変わります。
つまり、文字列を指し示すポインタがコピーされたことになるようです。

わかりやすくするために図を書いてみました。
まず、呼び出し元で str に "test" が代入されたときです。
(.Net)Stringの仕様にやられた_e0091163_2165033.jpg


次に、funcAに入ったときです。
(.Net)Stringの仕様にやられた_e0091163_2171463.jpg

この時に、funcA内の引数で渡された str はどうやらポインタがコピーされているようなのです。
ただし、str のポインタの指し示す先は呼び出し元と同一のアドレスです。(値が"test")

そして、funcA内で str に新しい文字列を代入したときです。
(.Net)Stringの仕様にやられた_e0091163_2174175.jpg

この時、string の仕様として、インスタンスは新しいメモリに作成されるので、引数で渡された str ポインタの指し示す先も変更となります。
これで呼び出し元 str と funcA 内の引数で渡された str との間に差異が生じるわけですね。


結局、メソッドの引数に string オブジェクトを参照で渡したいときは ref キーワードをつけないといけないようです。
ref キーワードを付けると引数で渡された参照のポインタはコピーされないようです。
図で書くとこんな感じ。。。
呼び出し元で str に "test" が代入されたときです。
(.Net)Stringの仕様にやられた_e0091163_2181187.jpg


次に、funcAに入ったときです。
(.Net)Stringの仕様にやられた_e0091163_2183777.jpg

ref が点いてるため、ポインタのコピーが作成されません。(つまりこれが参照渡し)

そして、funcA内で str に新しい文字列を代入したときです。
(.Net)Stringの仕様にやられた_e0091163_2191037.jpg



よく、.Net では規定として値渡しを用いると言われてることがよく理解できました。
参照型を渡す場合でも、ref を付けないとポインタがコピー(値渡し)されてたんですね。


ちなみに、下記のようにクラスを生成し、そのメンバとして string オブジェクトを持たして、メソッド内で文字列変更するとどうなるでしょうか?
public class Form1 : Form
{
....
private void button1_Click(object sender, EventArgs e)
{
StringTest cls = new StringTest();
cls.Str1 = "test class";
funcB(str, cls);
Console.WriteLine(cls.Str1);
}
 
private void funcB(string str, StringTest cls)
{
cls.Str1 = "hoge class";
}
}
public class StringTest
{
public string Str1 { get; set; }
}

この場合、結果は "hoge class" となります。
この場合、funcB 内で引数で渡された cls ポインタ自体は呼び出し元の cls のコピー(値渡し)ですが、ポインタの値が同一、つまり、同じ StringTest インスタンスを指し示しているため、メソッド内で書き換えても値が保持されてたわけですね。
図で書くとこんな感じ。
(.Net)Stringの仕様にやられた_e0091163_2193923.jpg



今までなんでこれに気付かなかったのか不思議です。
やはり詳しい言語仕様しらないと怖いですね。。。
by jehoshaphat | 2010-02-26 21:09 | .Net開発


<< (.Net)ComboBoxで... (.Net)C#で変数のアドレ... >>