C# や Visual Basic で表現する空文字列「""」と「String.Empty」。.NET Framework 1.x では同一のインスタンスだったが、.NET Framework 2.0 以降は別物になってしまった。
Console.WriteLine(ReferenceEquals("", String.Empty));
結果(.NET Framework 1.x)
True
結果(.NET Framework 2.0)
False
当然、文字列インターンプール絡みの問題だ。
基本的に、リテラル文字列は「全て」文字列インターンプールに置かれると考えて良いはずだ。"" も String.Empty もリテラル文字列なので、本来なら ReferenceEquals の結果は True になる。何故、.NET Framework 2.0 では "" と String.Empty は同一インスタンスではないのか。
アセンブリを Ngen してネイティブイメージを作成した場合、わざわざリテラル文字列をインターンプールに再配置しないで、ネイティブイメージに配置されている文字列をそのまま使用すれば効率が良い。その際に使用するのが、StringFreezingAttribute だ(※1)。StringFreezingAttribute がアセンブリに指定されていれば、文字列がネイティブイメージに固定化され、コードはネイティブイメージ内の文字列を使う。
.NET?Framework 2.0 では mscorlib.dll はネイティブイメージ化されているので、String.Empty が参照している "" はインターンプールに配置されず、ネイティブイメージに配置されている状態となる。対して、アプリケーションで使用する "" はインターンプールに配置されるので、"" と String.Empty が参照している空文字列は別物となるわけだ。
インターンプール絡みの問題はそれだけではない。
System.Runtime.CompilerServices.CompilationRelaxationsAttribute に System.Runtime.CompilerServices.CompilationRelaxations.NoStringInterning が指定されたアセンブリの場合、リテラル文字列がインターンプールに配置されるという事は一切なくなる。事実、C# コンパイラはこの属性をデフォルトで付与している。しかし、実際はリテラル文字列はインターンプールに配置されている。それは、 CLR がこの属性を完全に無視しているせいらしい(プログラミング .NET Framework より)。
文字列インターンプールは簡単な問題ではないので、議論をする際は「コンパイルした状況」「Ngen したかどうか」「CLR のバージョン(あるいは CLI の実装)」など、前提となる条件が多く必要となる。
- ※1 コンパイル時にはインターンプール化とはまた別のプール化が行われる事に注意。同じリテラル文字列がソースコード内にいくつもあった場合、コンパイラ(少なくとも csc.exe は)はそれらをまとめてメタデータに一度だけ出力するようにする。StringFreezingAttribute はその上で文字列を固定化する。