類型成員總覽

一個自定義類型,最終就是把一組彼此有關聯的成員收斂到同一個邊界裡。以 C# 來說,最常見的成員包含:

  • 常量
  • 字段
  • 構造方法
  • 實例方法與靜態方法
  • 操作符方法
  • 屬性
  • 事件

如果把這批筆記串起來看,下面幾篇可以互相對照:

這篇不展開每一種成員的語法細節,而是先整理「型別本身」在設計時最容易忽略的幾個問題。

類型可見性

類型本身也有可見性。最常見的兩個層級是:

  • public:其他程序集也能使用
  • internal:只有當前程序集內可見

如果只是想把內部型別開給指定測試程序集,可以使用 InternalsVisibleTo

[assembly: InternalsVisibleTo("MyFriendAssembly")]

這通常用在兩種情境:

  • 單元測試需要存取內部型別
  • 同一產品拆成多個程序集,但內部實作需要彼此協作

不過一旦大量依賴 InternalsVisibleTo,通常也表示模組邊界可能還不夠穩定,這件事本身值得回頭檢查。

成員可見性

類型裡的成員還有更細的訪問層級:

CLR 名稱C# 關鍵字說明
privateprivate只有當前型別可用
familyprotected當前型別與其衍生型別可用
assemblyinternal同一程序集內可用
family or assemblyprotected internal衍生型別或同程序集任一方可用
family and assemblyprivate protected必須同時是衍生型別,且位於同一程序集
publicpublic對所有呼叫端公開

實務上,一個簡單原則通常就夠用:

  • 不確定要不要公開,先不要公開
  • 不是外部使用者理解這個型別所必需的成員,就不要急著升成 public

公開 API 一旦被依賴,後續版本就會被它綁住。

靜態類別

有些型別並不代表「要被建立出來的物件」,只是拿來收斂一組工具函式。這時候就適合用 static class

public static class MathHelpers
{
    public static int Double(int value) => value * 2;
}

C# 對靜態類別有幾個直接限制:

  • 不能被實例化
  • 不能被繼承
  • 只能包含靜態成員
  • 不能宣告實例構造方法

這些限制的本質很單純:它不是物件模型,而是命名邊界。

從 IL 角度看,靜態類別其實是同時帶有 abstractsealed 的類別。換句話說,CLR 並沒有「靜態類別」這個獨立分類,這是 C# 在語言層幫我們做的限制。

程序集、型別與版本

我們平常說的類型,實際上都存在某個程序集裡。程序集不只是部署單位,也是一個版本邊界。

這代表幾件事:

  • 類型是否公開,會影響程序集對外承諾的 API 面積
  • 一個成員一旦變成 public,之後修改它的成本就會提高
  • 把很多暫時性的內部實作暴露出去,會讓未來重構變得困難

所以設計型別時,除了想「現在怎麼寫比較方便」,也要多想一步:這個成員是不是值得成為長期契約。

callcallvirt

方法呼叫在 IL 裡常見兩種指令:

  • call
  • callvirt

可以先這樣理解:

  • call:直接呼叫目標方法
  • callvirt:保留虛擬分派能力,必要時走多態

雖然 callvirt 名字看起來像只用於虛方法,但 C# 編譯器有時連一般實例方法也會產生 callvirt,因為它還順便提供了空參考檢查的效果。

如果是 base.Method() 這種語法,編譯器表達的是「呼叫基底版本」,那麼就不會再經過一般的虛擬分派路徑。

小結

自定義類型真正難的地方,通常不是語法,而是邊界:

  • 哪些成員應該存在
  • 哪些成員應該公開
  • 哪些工具應該收斂成靜態類別
  • 哪些設計會把未來版本綁死

這些判斷會直接影響可維護性,甚至比單一語法點更重要。