類型成員總覽
一個自定義類型,最終就是把一組彼此有關聯的成員收斂到同一個邊界裡。以 C# 來說,最常見的成員包含:
- 常量
- 字段
- 構造方法
- 實例方法與靜態方法
- 操作符方法
- 屬性
- 事件
如果把這批筆記串起來看,下面幾篇可以互相對照:
這篇不展開每一種成員的語法細節,而是先整理「型別本身」在設計時最容易忽略的幾個問題。
類型可見性
類型本身也有可見性。最常見的兩個層級是:
public:其他程序集也能使用internal:只有當前程序集內可見
如果只是想把內部型別開給指定測試程序集,可以使用 InternalsVisibleTo:
[assembly: InternalsVisibleTo("MyFriendAssembly")]
這通常用在兩種情境:
- 單元測試需要存取內部型別
- 同一產品拆成多個程序集,但內部實作需要彼此協作
不過一旦大量依賴 InternalsVisibleTo,通常也表示模組邊界可能還不夠穩定,這件事本身值得回頭檢查。
成員可見性
類型裡的成員還有更細的訪問層級:
| CLR 名稱 | C# 關鍵字 | 說明 |
|---|---|---|
private | private | 只有當前型別可用 |
family | protected | 當前型別與其衍生型別可用 |
assembly | internal | 同一程序集內可用 |
family or assembly | protected internal | 衍生型別或同程序集任一方可用 |
family and assembly | private protected | 必須同時是衍生型別,且位於同一程序集 |
public | public | 對所有呼叫端公開 |
實務上,一個簡單原則通常就夠用:
- 不確定要不要公開,先不要公開
- 不是外部使用者理解這個型別所必需的成員,就不要急著升成
public
公開 API 一旦被依賴,後續版本就會被它綁住。
靜態類別
有些型別並不代表「要被建立出來的物件」,只是拿來收斂一組工具函式。這時候就適合用 static class:
public static class MathHelpers
{
public static int Double(int value) => value * 2;
}
C# 對靜態類別有幾個直接限制:
- 不能被實例化
- 不能被繼承
- 只能包含靜態成員
- 不能宣告實例構造方法
這些限制的本質很單純:它不是物件模型,而是命名邊界。
從 IL 角度看,靜態類別其實是同時帶有 abstract 與 sealed 的類別。換句話說,CLR 並沒有「靜態類別」這個獨立分類,這是 C# 在語言層幫我們做的限制。
程序集、型別與版本
我們平常說的類型,實際上都存在某個程序集裡。程序集不只是部署單位,也是一個版本邊界。
這代表幾件事:
- 類型是否公開,會影響程序集對外承諾的 API 面積
- 一個成員一旦變成
public,之後修改它的成本就會提高 - 把很多暫時性的內部實作暴露出去,會讓未來重構變得困難
所以設計型別時,除了想「現在怎麼寫比較方便」,也要多想一步:這個成員是不是值得成為長期契約。
call 與 callvirt
方法呼叫在 IL 裡常見兩種指令:
callcallvirt
可以先這樣理解:
call:直接呼叫目標方法callvirt:保留虛擬分派能力,必要時走多態
雖然 callvirt 名字看起來像只用於虛方法,但 C# 編譯器有時連一般實例方法也會產生 callvirt,因為它還順便提供了空參考檢查的效果。
如果是 base.Method() 這種語法,編譯器表達的是「呼叫基底版本」,那麼就不會再經過一般的虛擬分派路徑。
小結
自定義類型真正難的地方,通常不是語法,而是邊界:
- 哪些成員應該存在
- 哪些成員應該公開
- 哪些工具應該收斂成靜態類別
- 哪些設計會把未來版本綁死
這些判斷會直接影響可維護性,甚至比單一語法點更重要。