首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> C# 入門

C# 封裝完整指南:存取修飾詞、屬性封裝、介面隔離與資訊隱藏最佳實踐

     編輯:C# 入門

C# 封裝完整指南:存取修飾詞、屬性封裝、介面隔離與資訊隱藏最佳實踐

封裝 是物件導向三大支柱之一,核心精神是資訊隱藏:將物件內部實作細節(資料結構、演算法、狀態變化邏輯)隱藏起來,只對外暴露穩定、受控的公用介面。良好的封裝降低耦合、提升可維護性、確保資料完整性、便於單元測試。

存取修飾詞完整對照

修飾詞同類別衍生類別(同組件)衍生類別(不同組件)同組件其他類別不同組件其他類別
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() { } // 衍生類可覆寫
}

進階封裝模式

1. 唯讀欄位 + 建構子注入

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));
    }
}

2. 介面隔離:公開介面 vs 實作細節

// 公開契約:消費者只依賴介面
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) { ... }
}

3. 不可變物件模式

// 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);
}

4. 封裝集合:回傳唯讀視圖

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。
  • 屬性而非方法:單純存取用屬性;有副作用/計算成本高用方法。
  • 建構子確保有效狀態:物件建立後即處於合法狀態,避免「初始化方法」模式。
  • 不可變優於可變:預設設計為不可變,需要時再加可變操作。
  • 介面定義契約:公開 API 盡量透過介面,實作類別標記 internal/sealed。
  • 單元測試友善:InternalsVisibleTo 讓測試專案存取 internal 成員,避免為測試放寬存取層級。

常見問題 FAQ

Q: 為什麼不建議用 protected 欄位?

A: 衍生類直接修改欄位會繞過基底類別的驗證/不變條件/通知機制。改用 protected 屬性/方法封裝。

Q: 如何測試私有方法?

A: 不應直接測試私有方法。測試公開行為;若私有邏輯複雜需獨立測試,提取為獨立類別(internal)並透過介面注入。

Q: internal vs public 如何決定?

A: 組件外是否需要使用?若為內部實作細節、遷移風險高、API 不穩定 → internal。公開契約、穩定、有版本承諾 → public。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved