今天寫bug的時候幫同事解決了一個有趣的問題,可能很多人都會答錯。分享給大家。
問題
請看以下例子,並回答問題。
var s1 = "12";
var s2 = "12";
//序列化方式1
var o3 = Newtonsoft.Json.JsonConvert.DeserializeObject<string>(Newtonsoft.Json.JsonConvert.SerializeObject(s1));
//序列化方式2
MemoryStream stream = new MemoryStream();
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
bf.Serialize(stream, s1);
stream.Seek(0, SeekOrigin.Begin);
var o4 = bf.Deserialize(stream);
//====分割線===================================================
var e1 = object.ReferenceEquals(s1, s2);
var e2 = o4 == s1;
var e3 = s1.Equals(o4);
var e4 = o3 == o4;
Console.ReadKey();
請回答分割線後e1, e2, e3, e4 值為true還是false。
人人都知道在.Net中字串是享元模式的經典範例。字串具有不變性。(至少在託管層,事實上可以在非託管層修改字串的值),但你真的能回答對上面的問題麼?
答案
e1 = true;
e2 = false;
e3 = true;
e4 = false;
要了解這個問題首先可以看下字串在記憶體中的佈局。
如何在visual studio中檢視變數的記憶體佈局
在VS中可以非常方便的檢視託管或非託管變數的記憶體值。方法如下。
- 依次在除錯模式下開啟 除錯 -> 視窗 -> 記憶體 -> 記憶體1(1~4均可) 開啟記憶體對話方塊。
- 在位址列中輸入變數名即可。
字串變數在記憶體中的佈局
在.Net中字串是以UTF-16格式在記憶體中儲存的。在本例中s1的記憶體如下。
00 00 00 00 00 00 00 00 98 d6 fc e5 fb 7f 00 00 02 00 00 00 31 00 32 00
這裡可能與你拿到的結果不一樣。你可能並沒有前8位0x00
,因為我把物件頭帶上了。下面依次解釋各段含義。
00 00 00 00 00 00 00 00
最開始的8位元是物件頭。其中,在64位下,高4位為0,低4位為一個不為0的數(這裡由於並沒有執行lock或Gethashcode操作,所以這裡為0,感興趣的自行實驗.)98 d6 fc e5 fb 7f 00 00
物件的MethodTable
,根據型別而不同,物件的引用指向的位置。02 00 00 00
字串長度,這裡是2。31 00 32 00
字串陣列* char
,注意都是小端模式。
拿以上s1 s2 o3 o4
分別實驗可以發現他們的記憶體一模一樣,其中s1 s2
直接就是同一塊記憶體地址,但剩下的記憶體地址都不一樣。
比較與解答
-
e1 = true;
通過記憶體看合情合理,畢竟都同一塊記憶體了。 -
e2 = false;
這裡如果用的VS的版本比較高的話,也能看出來。因為這裡VS會提示:可能非有意的引用比較。
既然是引用比較,記憶體地址都不一樣,肯定是false了。但是如果vs版本不高的話則迷惑性就較大了,其實這裡做的是
ReferenceEquals
的比較。 -
e3 = true;
這裡問題出在.Net程式碼裡。字串型別Equals
方法被過載了。
// Determines whether two strings match.
public override bool Equals([NotNullWhen(true)] object? obj)
{
if (object.ReferenceEquals(this, obj))
return true;
if (!(obj is string str))
return false;
if (this.Length != str.Length)
return false;
return EqualsHelper(this, str);
}
EqualsHelper
方法最終則呼叫如下。(在.Net 6下)
// Optimized byte-based SequenceEquals. The "length" parameter for this one is declared a nuint rather than int as we also use it for types other than byte
// where the length can exceed 2Gb once scaled by sizeof(T).
public static unsafe bool SequenceEqual(ref byte first, ref byte second, nuint length)
由於實現過於複雜(.Net framework 4.5.2下則較簡單,直接按長度比較char,有興趣的自行查閱),這裡就不貼具體實現了。我們很容易看出這裡比較的目的是比較兩段記憶體是否相等,顯然為true
。
e4 = false;
這裡是為了比較不同序列化方式的影響,和e2
類似,結果顯然是true
。
結論
雖然.Net中字串是享元模式建立的,但並不能保證同一字串在記憶體裡只有一份。比如序列化情況等例外情況。如果讀者知道其他情況也可以告訴我,提前說聲感謝