1. 前言
存取修飾詞就像你家裡的圍牆和鎖:它們決定誰能進哪個房間,誰只能從鑰匙孔偷看一下。提醒一下,C# 有這幾種修飾詞:
| 修飾詞 | 誰看得到 |
|---|---|
|
所有人 |
|
只有這個 class 裡面 |
|
這個 class 跟繼承它的 class |
|
整個專案(組件/assembly) |
|
整個專案 + 繼承的 class |
|
只有同一個專案裡的繼承 class |
封裝 的目標,就是把 class 的實作細節藏起來,這樣別人就不能隨便用一個 obj.Field = 99999; 把你的物件搞壞,除非你自己願意。
2. 存取層級的經典錯誤
全部都 public 大開放
最常見的錯誤就是把 class 的所有欄位和方法都宣告成 public。想法大概是:「說不定以後會用到?讓大家想怎麼用就怎麼用!」這種做法一開始好像很方便,直到有人給你的物件塞進不合理的狀態,或是開始亂用你的內部細節,結果你改 code 都會怕。
錯誤範例:
public class BankAccount
{
public string Owner;
public decimal Balance;
}
現在任何人都可以這樣搞:
var acc = new BankAccount();
acc.Owner = "";
acc.Balance = -1000000M; // 突然變成負一百萬!
哪裡不對?
你把所有安全性和常識都丟掉了。就算你的銀行只存樹葉或大富翁的紙鈔,隨便讓人設負餘額也太奇怪了。
把 class 變成「沒有結構的 structure」
第二個常見錯誤是,不只把欄位設 public,連內部的屬性和方法也都公開,明明根本不該讓外面用。
public class Vault
{
public string DoorPIN = "1234";
public void OpenDoor()
{
// 這裡有點魔法
}
}
現在怎樣?就是這樣:任何人都能知道 PIN,然後開你的保險箱。就算只是「測試用保險箱」,過一陣子大家都忘了這個洞,遲早出事。
正確做法:
class 的介面要盡量小又安全。給別人用的才 public,自己用的就 private。要給繼承用的才 protected。
3. 封裝的錯誤
在 .NET 裡有個共識:class 的資料(狀態)絕對不能放在 public 欄位!全部都要 private,或至少包在 property 裡。直接存取欄位是經典的壞習慣,也是 bug 的溫床。
為什麼欄位要 private
欄位是物件內部狀態的根本。如果讓大家都能直接改,你就沒辦法控制什麼時候、什麼值會被寫進去。你的物件很容易被塞進奇怪的值,而且以後欄位格式一改,所有外部存取都會壞掉。
應該這樣:
欄位宣告成 private,外面只給需要的 property 或方法:
public class BankAccount
{
private decimal balance;
public decimal Balance
{
get { return balance; }
private set
{
if (value < 0)
throw new ArgumentException("餘額不能是負的");
balance = value;
}
}
public BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("不能存零或負的金額!");
Balance += amount;
}
}
大錯特錯:不能改的資料卻給 public set
有時候只需要 get,但工程師為了快,直接寫 public { get; set; }。結果任何人都能這樣:
account.Balance = 99999999; // 為什麼不行?
正確做法:
如果 property 只能在 class 裡設,就讓 set 加修飾詞:
public decimal Balance { get; private set; }
或是只能在建構時設定(C# 14):
public decimal Balance { get; init; }
4. protected 也有陷阱
對所有繼承 class 都大開方便之門
protected 是給繼承 class 用的沒錯,但有些資料連繼承 class 也不該碰!比如說,你不希望子類直接改掉關鍵欄位。
範例:
public class SecureVault
{
protected string secretCode = "1234";
}
現在任何繼承 class 都能這樣:
public class HackerVault : SecureVault
{
public void Hack()
{
secretCode = "0000"; // 輕鬆改掉密碼!
}
}
建議:
protected 只給真的需要繼承 class 用的東西。其他都 private,要給繼承 class 用就寫 protected 方法。
5. internal 跟組件搞混的錯誤
internal 看起來很方便:「反正專案內都能用」。但只要專案超過一個檔案或用到 library,就會出現衝突。學生常常不小心把東西設成組件內公開,其實應該完全藏起來。
範例:
internal class Logger
{
internal void Write(string msg) { /* ... */ }
}
現在專案裡任何地方都能呼叫 Logger.Write。如果以後你的 DLL 被別的專案用到?只要你有 public,所有「內部細節」都會被看到。
建議:
技術性 class 可以用 internal,但敏感細節還是要 private。
6. 實作練習:銀行 app
繼續用銀行 app 的例子,實際看看怎麼容易出錯,該怎麼修正。
錯誤範例 #1:Public 欄位
public class BankAccount
{
public decimal Balance;
public string Owner;
}
問題:任何人都能搞壞它。
用 property 修正
public class BankAccount
{
private decimal _balance;
private string _owner;
public string Owner => _owner; // 只讀
public decimal Balance
{
get => _balance;
private set
{
if (value < 0) throw new ArgumentException("餘額不能是負的");
_balance = value;
}
}
public BankAccount(string owner, decimal initialBalance)
{
if (string.IsNullOrWhiteSpace(owner))
throw new ArgumentException("持有人名稱必填");
_owner = owner;
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("存款金額必須是正數");
Balance += amount;
}
}
現在你如果這樣寫:
var acc = new BankAccount("Lena", 1000m);
acc.Balance = -700m; // 錯誤!set 是 private。
或這樣:
acc.Owner = "駭客"; // 編譯錯誤,沒有 set
——都不行。只能用建構子或方法。
錯誤:為了「好看」加 property
很多人會寫:
public int Age { get; set; }
但其實年齡應該只能用 IncreaseAge 方法(比如每年 1 月 1 號)來改,不能直接設。
7. 視覺提醒:錯誤與正確做法
graph TD
A[Public Fields] -->|任何 code| D[狀態失控]
B[Private Fields + Public Methods] -->|定義清楚| E[狀態受控]
F[Public Setters] -->|誰都能改| D
G[Private Setters] -->|只有 class| E
| 做法 | 狀態有保護嗎? | 能驗證嗎? | 好擴充嗎? |
|---|---|---|---|
| Public 欄位 | ❌ | ❌ | ❌ |
| Property(get/set public) | ❌ | 部分 | 可以,但不安全 |
| Private 欄位 + set private 的 property | ✅ | ✅ | ✅ |
8. 封裝寫法的風格建議
- 只公開 class 使用者真的需要的東西。
- 所有進來的資料都要驗證。
- 不要多此一舉:不該讓外面改的 property 就讓 set private。
- 有特殊邏輯就寫方法,不要讓人直接存取。
- 就算很想,也不要為了測試寫 public 欄位。
- 就算只是公開讀取,也用 property,不要直接欄位——以後要改很方便。
GO TO FULL VERSION