淺談 C# Property (屬性) 語法糖與 NRT 機制演進
TLDR
- 屬性初始值設定項(Property Initializers)與 Expression-bodied 屬性在生命週期上不同,前者僅在物件初始化時執行一次,後者每次呼叫均會重新執行,誤用會導致邏輯錯誤。
- C# 14 引入的
field關鍵字,解決了自動屬性需為了簡單邏輯(如Trim())而退回手寫 Backing Field 的痛點。 - NRT (Nullable Reference Types) 機制需搭配
<WarningsAsErrors>nullable</WarningsAsErrors>才能發揮強制約束效果。 required與init語法解決了 DTO 在 NRT 機制下必須撰寫無意義null!預設值的問題。- 使用
[SetsRequiredMembers]可解決參數化建構子與required屬性之間的編譯衝突。
C# Property (屬性) 的語法演進與踩雷紀錄
C# 的屬性演進旨在簡化定義,但不同語法在執行時機上有顯著差異,若未釐清極易引發 Bug。
屬性初始值與 Expression-bodied 的區別
什麼情況下會遇到這個問題:在定義唯讀屬性或設定初始值時,混淆了「靜態快取」與「動態計算」的語法。
public string Name => "Default"(Expression-bodied):每次被呼叫時都會重新執行。public string Name { get; } = "Default"(Property Initializer):僅在物件實體化(new)時執行一次。
WARNING
災難情境: 若使用 public Guid OrderId => Guid.NewGuid();,每次讀取該屬性都會產生全新的 Guid,導致序列化或 Log 追蹤失效。應改用 public Guid CorrectOrderId { get; } = Guid.NewGuid(); 以確保狀態恆定。
使用 field 關鍵字簡化屬性邏輯
什麼情況下會遇到這個問題:當自動屬性需要加入簡單邏輯(如 Trim() 或 NotifyPropertyChanged),卻不想手寫冗長的 Backing Field 時。
C# 14 引入的 field 關鍵字允許直接存取編譯器產生的隱含欄位:
public class User {
public string Name {
get;
set => field = value.Trim();
}
}TIP
建議將邏輯處理放在 set 中,避免 get 頻繁呼叫帶來的額外開銷,並減少 Entity Framework Core 等框架直接存取 Backing Field 時可能產生的副作用。
NRT (Nullable Reference Types) 與檢查機制的補全
NRT 的核心在於透過 ? 標註明確宣告參考型別的空值可能性。若要強制執行合約,應在專案檔中設定:
<WarningsAsErrors>nullable</WarningsAsErrors>解決 DTO 在 NRT 下的初始化困境
什麼情況下會遇到這個問題:在 C# 8.0 至 10.0 時期,非 Null 屬性必須初始化,導致 DTO 必須撰寫 null! 或空字串來騙過編譯器,造成程式碼雜訊。
透過 init 與 required 關鍵字,可以確保屬性在初始化後不可變,且強制呼叫端必須給值,無需再寫無意義的預設值:
public class UserDto {
public required string UserName { get; init; }
}
// 呼叫端必須給值,否則編譯失敗
UserDto dto = new() { UserName = "Alice" };處理建構子與 required 的衝突
什麼情況下會遇到這個問題:當類別同時定義了參數化建構子與 required 屬性時,編譯器會因無法透過 { } 初始化而發出警告。
使用 [SetsRequiredMembers] 屬性可告知編譯器該建構子已完成所有必要欄位的賦值:
using System.Diagnostics.CodeAnalysis;
public class User {
public required string UserName { get; init; }
[SetsRequiredMembers]
public User(string userName) {
UserName = userName;
}
}required 在 Web API 的應用
在 ASP.NET Core 的 [FromBody] 序列化中,若屬性標記為 required,當前端漏傳該欄位時,系統會自動拋出 JsonException,這有助於明確區分「傳入預設值」與「未傳入欄位」的語意差異。
異動歷程
- 2026-03-30 初版文件建立。
