CodeGym /課程 /C# SELF /存取修飾詞與封裝的常見錯誤

存取修飾詞與封裝的常見錯誤

C# SELF
等級 25 , 課堂 2
開放

1. 前言

存取修飾詞就像你家裡的圍牆和鎖:它們決定誰能進哪個房間,誰只能從鑰匙孔偷看一下。提醒一下,C# 有這幾種修飾詞:

修飾詞 誰看得到
public
所有人
private
只有這個 class 裡面
protected
這個 class 跟繼承它的 class
internal
整個專案(組件/assembly)
protected internal
整個專案 + 繼承的 class
private protected
只有同一個專案裡的繼承 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,不要直接欄位——以後要改很方便。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION