在看 Framework Design Guidelines 時,讀到官方把 Nullable<T> 改成 readonly 的過程中,曾意外引發一個不易察覺的問題。也因此我才注意到防禦性複製(defensive copy)這個議題,以及它帶來的陷阱。
相關討論串在 dotnet/corefx PR #24997
防禦性複製
詳見外部參考 C# struct 的防禦性複製(defensive copy)。
問題展示
以 GitHub 的案例,完整程式碼 Nullable<T> 裁剪後在 value 欄位加上 readonly 如下:
1 | public struct FakeNullable<T> where T : struct |
原因在 FakeNullable<T>.ToString() 這行:
1 | return value.ToString(); |
value 是 readonly field。對唯讀欄位呼叫 instance method 時,編譯器會先做一份防禦性複製,再對複本呼叫方法,概念上接近:
1 | var temp = value; |
所以 Counter.ToString() 改到的是複本 temp.Count,不是原本的 value.Count。這就是為什麼 counter.ToString() 執行後,counter.Value.Count 仍然是 0。
對照原本非唯讀版本產生的 IL Code,可以更清楚看出差異:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public struct FakeNullable<T> where T : struct
{
internal T value;
public FakeNullable(T value)
{
this.value = value;
}
public readonly T Value => value;
public override string? ToString()
{
/*
IL_0000: ldarg.0
IL_0001: ldflda !0 valuetype Program/FakeNullable`1<!T>::'value'
IL_0006: constrained. !T
IL_000c: callvirt instance string [System.Runtime]System.Object::ToString()
IL_0011: ret
*/
return value.ToString();
}
}
結論
防禦性複製是編譯器時期額外加入的操作,不特別留意的話很容易忽視。
參考
AI Tools
Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .Net Libraries, 3/e