.NET/C# 編譯期間能確定的相同字元串,在運行期間是相同的實例
- 2020 年 2 月 10 日
- 筆記
我們知道,在編譯期間相同的字元串,在運行期間就會是相同的字元串實例。然而,如果編譯期間存在字元串的運算,那麼在運行期間是否是同一個實例呢?
只要編譯期間能夠完全確定的字元串,就會是同一個實例。
字元串在編譯期間能確定的運算包括:
A + B
即字元串的拼接$"{A}"
即字元串的內插
字元串拼接
對於拼接,我們不需要運行便能知道是否是同一個實例:
private const string X = "walterlv is a"; private const string Y = "逗比"; private const string Z = X + Y;
以上這段程式碼是可以編譯通過的,因為能夠寫為 const
的字元串,一定是編譯期間能夠確定的。
字元串內插
對於字元串內插,以上程式碼我們不能寫成 const
:

錯誤提示為:常量的初始化必須使用編譯期間能夠確定的常量。
然而,這段程式碼不能在編譯期間確定嗎?實際上我們有理由認為編譯器其實是能夠確定的,只是編譯器這個階段沒有這麼去做而已。
實際上在 2017 年就有人在 GitHub 上提出了這個問題,你可以在這裡看討論:
- [Discussion] Constant string interpolation · Issue #2077 · dotnet/csharplang
- String interpolation constants · Issue #384 · dotnet/csharplang
- [Discussion] Constant string interpolation · Issue #11259 · dotnet/roslyn
但是,我們寫一個程式來驗證這是否是同一個實例:
using System; namespace Walterlv.Demo { class Program { static void Main(string[] args) { Console.WriteLine(ReferenceEquals(A, A)); Console.WriteLine(ReferenceEquals(C, C)); Console.WriteLine(ReferenceEquals(E, E)); Console.WriteLine(ReferenceEquals(G, G)); Console.ReadKey(true); } private static string A => $"walterlv is a {B}"; private static string B => "逗比"; private static string C => $"walterlv is a {D}"; private static string D = "逗比"; private static string E => $"walterlv is a {F}"; private static readonly string F = "逗比"; private static string G => $"walterlv is a {H}"; private const string H = "逗比"; } }
以上程式碼的輸出為:
False False False True
也就是說,對於最後一種情況,也就是內插的字元串是常量的時候,得到的字元串是同一個實例;這能間接證明編譯期間完全確定了字元串 G。
注意,其他情況都不能完全確定:
- 屬性內插時一定不確定;
- 靜態欄位內插時,無論是否是只讀的,都不能確定。(誰知道有沒有人去反射改掉呢?)
我們可以通過 IL 來確定前面的間接證明(程式碼太長,我只貼出來最重要的 G 字元串,以及一個用來比較的 E 字元串):
.method private hidebysig static specialname string get_G() cil managed { .maxstack 8 // [22 36 - 22 56] IL_0000: ldstr "walterlv is a 逗比" IL_0005: ret } .method private hidebysig static specialname string get_E() cil managed { .maxstack 8 // [20 36 - 20 56] IL_0000: ldstr "walterlv is a " IL_0005: ldsfld string Walterlv.Demo.Roslyn.Program::F IL_000a: call string [System.Runtime]System.String::Concat(string, string) IL_000f: ret }
可以發現,實際上 G 已經在編譯期間完全確定了。
擴展:修改編譯期間的字元串
前面我們說到可以在編譯期間完全確定的字元串。呃,為什麼一定要抬杠額外寫一節呢?
下面我們修改編譯期間確定的字元串,看看會發生什麼:
static unsafe void Main(string[] args) { // 這裡的 G 就是前面定義的那個 G。 Console.WriteLine("walterlv is a 逗比"); Console.WriteLine(G); fixed (char* ptr = "walterlv is a 逗比") { *ptr = 'W'; } Console.WriteLine("walterlv is a 逗比"); Console.WriteLine(G); Console.ReadKey(true); }
運行結果是:
walterlv is a 逗比 walterlv is a 逗比 Walterlv is a 逗比 Walterlv is a 逗比
雖然我們看起來只是在修改我們自己局部定義的一個字元串,但是實際上已經修改了另一個常量以及屬性 G。
少年,使用指針修改字元串是很危險的!鬼知道你會把程式改成什麼樣!
參考資料
- $ – string interpolation – C# Reference – Microsoft Docs
- [Discussion] Constant string interpolation · Issue #2077 · dotnet/csharplang
- String interpolation constants · Issue #384 · dotnet/csharplang
- [Discussion] Constant string interpolation · Issue #11259 · dotnet/roslyn
本文會經常更新,請閱讀原文: https://blog.walterlv.com/post/same-strings-at-compile-time-are-the-same-instances-at-runtime.html ,以避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。
本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名 呂毅 (包含鏈接: https://blog.walterlv.com ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發布。如有任何疑問,請 與我聯繫 ([email protected]) 。