封裝 是物件導向三大支柱之一,核心精神是資訊隱藏:將物件內部實作細節(資料結構、演算法、狀態變化邏輯)隱藏起來,只對外暴露穩定、受控的公用介面。良好的封裝降低耦合、提升可維護性、確保資料完整性、便於單元測試。
| 修飾詞 | 同類別 | 衍生類別(同組件) | 衍生類別(不同組件) | 同組件其他類別 | 不同組件其他類別 |
|---|---|---|---|---|---|
private | ✓ | ✗ | ✗ | ✗ | ✗ |
private protected | ✓ | ✓ | ✗ | ✗ | ✗ |
protected | ✓ | ✓ | ✓ | ✗ | ✗ |
internal | ✓ | ✓ | ✗ | ✓ | ✗ |
protected internal | ✓ | ✓ | ✓ | ✓ | ✗ |
public | ✓ | ✓ | ✓ | ✓ | ✓ |
類別預設:internal;成員預設:private。
屬性提供存取子攔截讀寫,實現驗證、通知、延遲載入、計算衍生值:
public class BankAccount {
private decimal _balance; // 隱藏欄位
// 唯讀屬性:僅 getter
public decimal Balance => _balance;
// 完整屬性:驗證邏輯
public string AccountNumber {
get => _accountNumber;
private set { // 僅內部可設定
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("帳號不可空白");
_accountNumber = value;
}
}
private string _accountNumber;
// 自動實作屬性 (C# 6+ 唯讀建構子初始化)
public DateTime CreatedAt { get; }
// 初始化設定子 (C# 9+)
public string Owner { get; init; } // 建構/物件初始化後不可變
// 計算屬性
public bool IsOverdrawn => _balance < 0;
public void Deposit(decimal amount) {
if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount));
_balance += amount;
OnBalanceChanged();
}
protected virtual void OnBalanceChanged() { } // 衍生類可覆寫
}
public class OrderService {
private readonly IRepository _repository; // 不可變相依性
private readonly ILogger _logger;
public OrderService(IRepository repository, ILogger logger) {
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
}
// 公開契約:消費者只依賴介面
public interface IUserRepository {
Task GetByIdAsync(int id);
Task AddAsync(User user);
}
// 實作細節:內部類別、私有方法、資料庫連線字串
internal sealed class SqlUserRepository : IUserRepository {
private readonly string _connectionString;
public SqlUserRepository(string connectionString) => _connectionString = connectionString;
public async Task GetByIdAsync(int id) {
// 內部 SQL、對映邏輯完全隱藏
using var conn = new SqlConnection(_connectionString);
return await conn.QueryFirstOrDefaultAsync("SELECT * FROM Users WHERE Id=@id", new { id });
}
// 私有輔助方法,外部不可見
private User MapFromReader(SqlDataReader reader) { ... }
}
// C# 9+ record:內建不可變、值相等、with 運算式
public record UserProfile(
string UserId,
string Email,
string DisplayName,
DateTime CreatedAt
);
// 傳統類別實作不可變
public class ImmutableSettings {
public string ApiKey { get; }
public int TimeoutSeconds { get; }
public IReadOnlyList AllowedOrigins { get; }
public ImmutableSettings(string apiKey, int timeout, IEnumerable origins) {
ApiKey = apiKey;
TimeoutSeconds = timeout;
AllowedOrigins = origins.ToList().AsReadOnly(); // 防禦性複製
}
// with 風格建立新實例
public ImmutableSettings WithTimeout(int newTimeout)
=> new ImmutableSettings(ApiKey, newTimeout, AllowedOrigins);
}
public class Team {
private readonly List _members = new();
public IReadOnlyList Members => _members.AsReadOnly(); // 防止外部修改
public void AddMember(Member member) {
if (_members.Count >= 10) throw new InvalidOperationException("團隊已滿");
_members.Add(member);
}
public bool RemoveMember(string id) => _members.RemoveAll(m => m.Id == id) > 0;
}
private:預設選擇,所有內部欄位、輔助方法、實作細節。protected:極少用。只有確定衍生類必須存取/覆寫時。優先用 protected virtual 方法代替欄位。internal:組件內部共用功能、單元測試存取(搭配 InternalsVisibleTo)。protected internal:罕見。組件內 + 衍生類存取。private protected:極罕見。同組件衍生類存取。public:公開 API 契約。慎重設計,一旦公開難以變更。| 違反模式 | 問題 | 修正 |
|---|---|---|
公開欄位 public int Age; | 無法驗證、無法通知、無法改變實作 | 改為屬性 public int Age { get; private set; } |
| 回傳可變內部集合 | 呼叫端可竄改內部狀態 | 回傳 IReadOnlyList<T>、.AsReadOnly()、.ToArray() |
公開 SetXxx 方法 | 暴露內部狀態變更細節 | 封裝為語意操作 Activate()、Deactivate() |
衍生類直接存取基底 protected 欄位 | 破壞基底類別不變條件 | 提供 protected 方法/屬性封裝存取 |
private,需要時再放寬。readonly,常數用 const。internal/sealed。InternalsVisibleTo 讓測試專案存取 internal 成員,避免為測試放寬存取層級。protected 欄位?A: 衍生類直接修改欄位會繞過基底類別的驗證/不變條件/通知機制。改用 protected 屬性/方法封裝。
A: 不應直接測試私有方法。測試公開行為;若私有邏輯複雜需獨立測試,提取為獨立類別(internal)並透過介面注入。
internal vs public 如何決定?A: 組件外是否需要使用?若為內部實作細節、遷移風險高、API 不穩定 → internal。公開契約、穩定、有版本承諾 → public。