.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型でも、インスタンス比較なんて使用頻度の低い機能を殺して間違いが起きる可能性を減らしている。マイクロソフトもがんばるね。