.NET/C# 編譯期間能確定的相同字元串,在運行期間是相同的實例

  • 2020 年 2 月 10 日
  • 筆記

我們知道,在編譯期間相同的字元串,在運行期間就會是相同的字元串實例。然而,如果編譯期間存在字元串的運算,那麼在運行期間是否是同一個實例呢?

只要編譯期間能夠完全確定的字元串,就會是同一個實例。


字元串在編譯期間能確定的運算包括:

  1. A + B 即字元串的拼接
  2. $"{A}" 即字元串的內插

字元串拼接

對於拼接,我們不需要運行便能知道是否是同一個實例:

private const string X = "walterlv is a";  private const string Y = "逗比";  private const string Z = X + Y;

以上這段程式碼是可以編譯通過的,因為能夠寫為 const 的字元串,一定是編譯期間能夠確定的。

字元串內插

對於字元串內插,以上程式碼我們不能寫成 const

錯誤提示為:常量的初始化必須使用編譯期間能夠確定的常量。

然而,這段程式碼不能在編譯期間確定嗎?實際上我們有理由認為編譯器其實是能夠確定的,只是編譯器這個階段沒有這麼去做而已。

實際上在 2017 年就有人在 GitHub 上提出了這個問題,你可以在這裡看討論:

但是,我們寫一個程式來驗證這是否是同一個實例:

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。

注意,其他情況都不能完全確定:

  1. 屬性內插時一定不確定;
  2. 靜態欄位內插時,無論是否是只讀的,都不能確定。(誰知道有沒有人去反射改掉呢?)

我們可以通過 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。

少年,使用指針修改字元串是很危險的!鬼知道你會把程式改成什麼樣!


參考資料

本文會經常更新,請閱讀原文: 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])