Int32.ToString 的小坑洞

這一篇文章聊一下 Int32.ToString() 在 .NET Core 3.0 的小小變更。

現象

過去在說明 C# 字串池效果的時候,我常常會寫一個這樣的範例說明在非使用字串常值的狀況下會在 Heap 產生不同的 string 型別物件:

 class Program
 {
     static void Main(string[] args)
     {
         int i = 1;
         string s1 = i.ToString();
         string s2 = i.ToString();           
         Console.WriteLine(object.ReferenceEquals(s1, s2)); 
         Console.ReadLine();
     }       
 }

這樣的範例在 .Net Framwrork  4.8 (含) 和 .NET Core 2.2(含) 以前都是符合預期且運作愉快,執行的結果是 false;但是在 .NET Core 3.0/3.1 的結果卻是 true。 這引起了我的好奇心,於是我稍微修改了賦予 i 變數的值:

 class Program
 {
     static void Main(string[] args)
     {
         int i = 99;
         string s1 = i.ToString();
         string s2 = i.ToString();           
         Console.WriteLine(object.ReferenceEquals(s1, s2));     
         Console.ReadLine();
     }       
 }

這個執行結果在 .NET Core 3.0/3.1 狀況下是 false。這下我更好奇了,i = 1 和 i = 99 居然會有相反的結果,為什麼?讓我們繼續看下去。

追根究柢

 為了得到解答,我追了一下 Int32.ToString() method 的原始碼,這個方法的內容如下:

 public override string ToString()
 {
     return Number.FormatInt32(m_value, null, null);
 }

繼續往內挖掘 Number.FormatInt32 method,在一個名為 UInt32ToDecStr(uint value, int digits)  的 internal method 中做了一個這樣的處理:

  int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(value));
  
  if (bufferLength == 1)
  {
      return s_singleDigitStringCache[value];
  }

簡單說就是當UInt32 數值轉出的字串的長度為 1 的時候,則直接返回內部 s_singleDigitStringCache 中的值,s_singleDigitStringCache 是 Nunmer class 中的一個靜態私有唯讀欄位 (private static readonly filed) ,其型別為 string[] (字串陣列)。這個陣列的內容很簡單:

 private static readonly string[] s_singleDigitStringCache = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };

這解釋了為什麼我用 i = 1 和 i = 99 會導致不同的結果,因為當賦予 i 的值為 0 ~ 9的時候,會直接從 s_singleDigitStringCache 取得對應的字串返回,也就是它永遠會返回同一個 string instance,使得執行結果為  true;但當  i > 9 或 i < 0 則沒有對應的機制,因此會產生不同的 string instance。

很微小的問題,但也許哪一天會被坑,隨手記錄一下。