為什麼需要屬性

屬性(Property)本質上是用來包裝資料存取的一層語法。它讓呼叫端看起來像在讀寫欄位,但實際上背後執行的是方法。

這樣做的好處是:

  • 可以保留欄位風格的使用方式
  • 又能在讀寫時加入驗證、轉換或計算
  • 比直接公開欄位更容易維持封裝

靜態屬性作用在型別上,實例屬性作用在物件上。

最基本的屬性

private sealed class PropertiesClass
{
    private int _score;
    private bool _isFullCombo;

    public int Score
    {
        set { _score = value; }
    }

    public bool IsFullCombo
    {
        get { return _isFullCombo; }
    }

    public float FinalResult
    {
        get { return _isFullCombo ? _score * 1.5f : _score; }
    }
}

從語意上來看,屬性可以是:

  • 唯讀:只有 get
  • 唯寫:只有 set
  • 讀寫:同時有 get / set

CLR 怎麼看屬性

CLR 並不直接把屬性當成「特殊欄位」。對它來說,屬性是一組 accessor 方法外加一筆 metadata。

也就是說,一個屬性最終通常會對應成:

  • get_PropertyName()
  • set_PropertyName(...)
  • property metadata

當 C# 編譯器看到:

obj.Score = 100;
bool fc = obj.IsFullCombo;

它實際上會翻譯成對 set_Score(...)get_IsFullCombo() 的呼叫。

因此屬性的重點從來不是「少打一點字」,而是用欄位風格的語法,安全地包裝方法呼叫。

自動實作屬性

最常見的簡寫是 AIP(Automatically Implemented Property):

public string Name { get; set; }

這代表你把 accessor 的實作交給編譯器,編譯器會幫你生成:

  • 一個私有 backing field
  • 對應的 get_Name() / set_Name(...)

這種寫法特別適合:

  • 目前只是單純資料承載
  • 暫時不需要驗證邏輯
  • 想保留未來再改成手寫屬性的空間

自動屬性的優缺點

優點

  • 寫法短
  • 可讀性高
  • 後續若要補上邏輯,從呼叫端看起來不需要更改 API 形式

缺點

  • 沒辦法直接插入自訂的讀寫流程
  • 不能像手寫屬性那樣細緻控制 backing field 行為
  • 有些初始化與序列化情境下,需要更明確的建模方式

所以 AIP 適合「現在很單純」的資料,但不代表它一定是所有屬性的終點。

索引器

帶參數的屬性,在 C# 中通常叫索引器(Indexer)。

public string this[int index]
{
    get { return "Hello World"; }
}

對呼叫端來說,它看起來像是在用 [] 操作物件;對 CLR 來說,它仍然只是屬性的另一種形式。

預設名稱通常是 Item。如果需要,也可以透過 IndexerNameAttribute 改變 metadata 中的名稱,但這不會讓索引器變成真正的「運算符重載替代品」。

物件初始化器

屬性很常和 object initializer 一起出現:

Employee e1 = new Employee() { Name = "Kelvin", Age = 24 };
Employee e2 = new Employee { Name = "Catherine", Age = 24 };

它看起來像是語法層面一次完成「建構 + 設值」,但實際流程仍然是:

  1. 先呼叫建構函式建立物件
  2. 再逐一呼叫屬性 setter

概念上就是:

Employee e = new Employee();
e.Name = "Kelvin";
e.Age = 24;

如果型別同時實作了 IEnumerable 且提供 Add(...),集合初始化器也會以類似方式被展開成一連串 Add(...) 呼叫。

Dictionary<string, int> table = new Dictionary<string, int>
{
    { "Kelvin", 24 },
    { "Catherine", 24 },
    { "Aden", 1000 },
};

存取修飾與 accessor 可見性

屬性本身和 get / set 並不一定要擁有完全相同的可見性。常見情境是「外部可讀,但只能在內部寫」:

public string Name { get; private set; }

這種寫法很常用在:

  • 建構後不希望外部任意修改
  • 由型別內部控制狀態轉換
  • 想對外公開資料,但不公開寫入權限

可以把它理解成:屬性是 API 介面,而 accessor 則是更細粒度的權限控制點。

屬性與欄位的差別

最值得記住的不是語法,而是用途邊界。

欄位更像儲存空間

  • 沒有攔截邏輯
  • 沒有 validation
  • 不適合直接當穩定公開 API

屬性更像受控入口

  • 可以加入驗證、延遲計算與快取
  • 可以在不改呼叫語法的情況下調整內部行為
  • 更適合作為公開契約的一部分

因此實務上通常會偏向:

  • 私有狀態:欄位
  • 對外暴露:屬性

效能

屬性本質上是方法呼叫,但簡單 accessor 常常會被 JIT inline,因此在大多數一般場景裡,不需要把「屬性比欄位慢」當成主要設計依據。

真正該在意的是:

  • 這個 accessor 裡是否放了昂貴邏輯
  • 屬性名稱是否讓人誤以為是 O(1) 存取,實際卻很重

如果某個操作成本高、會做 I/O、或會造成明顯副作用,那它通常就不應該偽裝成普通屬性。

小結

屬性這章最值得留下的幾個點:

  • 屬性底層是 accessor 方法加上 metadata,不是特殊欄位。
  • 自動實作屬性適合簡單資料承載,但不是萬能解法。
  • 索引器本質上是帶參數的屬性。
  • 物件初始化器最後仍會落回建構函式與 setter 呼叫。
  • 欄位偏向內部儲存,屬性偏向公開且受控的資料入口。

參考資料

  • 《CLR via C#》