.NET FrameworkにおけるSystem.String型は参照型?
最近仕事で、若いコが書いたVB 2005のコードレビューをしていたんだけど、文字列の比較をString.Compareメソッドを使って書いていて「あー、固いなー、今風だなあー」とか思った。具体的にはこんな感じ。
If String.Compare(a,b) = 0 Then .... End If
でも、「= 0」が旧世代にとっては自明な感じがしないんだよね。私ならもっと直接的に…
If (a = b) Then .... End If
こう書く。でもここでフト不安になる。そういえばString型は参照型だって言ってたなと。ひょっとするとa = bは値比較になっていなくて、「同じインスタンスか?」という比較になってやしないか?と。で、速攻テストP書いて確認した。こんなコード。
Imports System.Console Module StringTester Sub Main() Dim original As String Dim samevalue As String Dim copy As String original = "hoge" samevalue = "hoge" copy = original WriteLine("original is {0}", original) WriteLine("samevalue is {0}", samevalue) WriteLine("copy is {0}", copy) WriteLine("(original = samevalue) is {0}", (original = samevalue)) WriteLine("(original = copy) is {0}", (original = copy)) WriteLine("(samevalue = copy) is {0}", (samevalue = copy)) original = "fuga" WriteLine("original changed!") WriteLine("original is {0}", original) WriteLine("samevalue is {0}", samevalue) WriteLine("copy is {0}", copy) WriteLine("(original = samevalue) is {0}", (original = samevalue)) WriteLine("(original = copy) is {0}", (original = copy)) WriteLine("(samevalue = copy) is {0}", (samevalue = copy)) End Sub End Module
家には2005の環境がなかったので、2008で検証。ここで、もし、VB 2005とVB 2008で挙動が違うぜ!なんてことがあったら困るんだけど、そこんところの挙動が変わったらたぶん今頃みんなが大騒ぎしてるはずなので、そんなことはないだろうということで話を進める。
これを実行すると出力結果はこうなった。
original is hoge samevalue is hoge copy is hoge (original = samevalue) is True (original = copy) is True (samevalue = copy) is True original changed! original is fuga samevalue is hoge copy is hoge (original = samevalue) is False (original = copy) is False (samevalue = copy) is True
ふむ。一見値型っぽくふるまっている。でもMSDNにも「Stringは参照型」と明記されているし、きっと内部では参照型なんだろう。では、なんでこんなことになるのか? という答えが下のサイトに載っていた。
http://homepage1.nifty.com/rucio/main/dotnet/shokyu/standard34.htm
これの「5.文字列型の挙動」というところ。この説明によると、私の検証コードの「original = "fuga"」は"fuga"という別のインスタンスへの参照をoriginalに代入しているので、影響がcopyには波及しない、ということらしい。で、比較演算では値比較として機能しているので最初の私の心配は杞憂ってことで終わりに。でも、なんかの言語でインスタンス比較になるのがあってヤバイって記憶がかすかにあるんだが。なんだっけなー、javaだったかな?
ま、それはさておき。
ちょっと気になるのは途中でoriginalをfugaに書き換える部分だ。これをなんとかしてcopyに影響を波及させる方法を考えたんだけど、どうもうまい手が思いつかなかった。ReplaceやInsertなんてメソッドがあるんだけど、どっちもString型を返している。そしてcopyの値は維持されたまま。たぶん、String型を操る手段のうち、よほど特殊な事をしない限り新しいインスタンスを生成して返すので、参照による影響が波及しないようになっているんだ。それはつまり、実際の使用においては、その振る舞いが値型と同じと見ていい、ということじゃないか?
もちろん内部的には参照型なので、いちいち新しいインスタンスを生成している事によるオーバーヘッドが生じる。だからStringBuilder型のが早いよ!って言われるわけだ。
ん、ということは、StringBuilder型なら参照型らしく振舞うんだろうか? テストしてみよう。まず最初に作ったのは下のコード。
Imports System.Console Imports System.Text Module StringBuilderTester Sub Main() Dim original As New StringBuilder Dim samevalue As New StringBuilder Dim copy As StringBuilder original.Append("hoge") samevalue.Append("hoge") copy = original WriteLine("original is {0}", original) WriteLine("samevalue is {0}", samevalue) WriteLine("copy is {0}", copy) WriteLine("(original = samevalue) is {0}", (original = samevalue)) WriteLine("(original = copy) is {0}", (original = copy)) WriteLine("(samevalue = copy) is {0}", (samevalue = copy)) original.Append("fuga") WriteLine("original changed!") WriteLine("original is {0}", original) WriteLine("samevalue is {0}", samevalue) WriteLine("copy is {0}", copy) WriteLine("(original = samevalue) is {0}", (original = samevalue)) WriteLine("(original = copy) is {0}", (original = copy)) WriteLine("(samevalue = copy) is {0}", (samevalue = copy)) End Sub End Module
これはなんとsyntax error! 「=」の比較演算子がStringBuilder型には実装されていない模様。仕方ないので違う方法に書き換える。
Imports System.Console Imports System.Text Module StringBuilderTester Sub Main() Dim original As New StringBuilder Dim samevalue As New StringBuilder Dim copy As StringBuilder original.Append("hoge") samevalue.Append("hoge") copy = original WriteLine("original is {0}", original) WriteLine("samevalue is {0}", samevalue) WriteLine("copy is {0}", copy) WriteLine("(original = samevalue) is {0}", original.Equals(samevalue)) WriteLine("(original = copy) is {0}", original.Equals(copy)) WriteLine("(samevalue = copy) is {0}", samevalue.Equals(copy)) original.Append("fuga") WriteLine("original changed!") WriteLine("original is {0}", original) WriteLine("samevalue is {0}", samevalue) WriteLine("copy is {0}", copy) WriteLine("(original = samevalue) is {0}", original.Equals(samevalue)) WriteLine("(original = copy) is {0}", original.Equals(copy)) WriteLine("(samevalue = copy) is {0}", samevalue.Equals(copy)) End Sub End Module
StringBuilder型のEqualsメソッドに書き換えた。説明によると値比較の結果を返すので、インスタンス比較による意図しない動作を引き起こす心配はない。結果はこんな感じに。
original is hoge samevalue is hoge copy is hoge (original = samevalue) is True (original = copy) is True (samevalue = copy) is True original changed! original is hogefuga samevalue is hoge copy is hogefuga (original = samevalue) is False (original = copy) is True (samevalue = copy) is False
うむ、参照によってoriginalへの変更がcopyに波及している。参照型らしい挙動だ。この動作でいいのなら、いちいち新しいインスタンスを返さなくていいので動作も速い。
なるほどなー。Stringは確かに参照型なんだけど、熟練度の低いプログラマが間違った事をしないような配慮がされているんだな。ReplaceやInsertメソッドが操作対象のインスタンスそのものを書き換えるんじゃなくて、値を返す関数になっている理由も、きっとその配慮の一環だ。そしてStringBuilder型でも、インスタンス比較なんて使用頻度の低い機能を殺して間違いが起きる可能性を減らしている。マイクロソフトもがんばるね。