Swift 存取控制 (Access Control)

存取控制 (Access Control) 是限制程式碼(如類別、結構、屬性、方法)被其他部分程式碼使用的機制。

它的核心目的是 封裝 (Encapsulation):隱藏實作細節,僅暴露必要的介面 (Interface)。這能讓你的程式碼更安全、更易於維護,因為你可以自由修改內部的實作邏輯,而不用擔心影響到外部的使用者。

模組與原始檔 (Modules and Source Files)

在深入了解等級之前,必須先理解 Swift 的兩個基礎概念:

  • 模組 (Module):是一個獨立的程式碼分發單位。例如一個 Framework (如 UIKit, SwiftUI)、一個 Library,或者你的 App 主程式本身,都是一個獨立的模組。
  • 原始檔 (Source File):就是你專案中單個 .swift 檔案。

Swift 的存取控制就是基於這兩個概念來劃分邊界的。

五種存取層級

Swift 提供了五種存取層級,從最開放到最封閉依序為:

1. Open (open) - 最開放

  • 範圍:可以被任何模組存取。
  • 繼承/覆寫允許其他模組繼承 (Inherit) 或覆寫 (Override)。
  • 用途:通常用於開發 Framework 或 Library 時,你希望讓使用者可以繼承並自訂行為的類別或方法 (例如 UIViewController 就是 open 的)。

2. Public (public)

  • 範圍:可以被任何模組存取。
  • 繼承/覆寫不允許其他模組繼承或覆寫(但在定義它的模組內部可以)。
  • 用途:這是 Framework 對外暴露的主要介面。你想讓外部使用這個功能,但不希望他們隨意修改原本的行為。
Open vs Public 的差異open 允許外部模組繼承/覆寫,public 則鎖定為 final (對外部而言)。這讓 Library 作者更能控制程式碼的穩定性。

3. Internal (internal) - 預設層級

  • 範圍:只能在定義它的模組 (Module) 內部被存取。無法被外部模組看見。
  • 用途:這是 Swift 的預設值。如果你沒有寫任何存取關鍵字,就是 internal。這適合用於 App 內部的所有程式碼結構,讓不同檔案可以互相合作,但不用擔心被外部干擾。

4. Fileprivate (fileprivate)

  • 範圍:只能在當前原始檔 (.swift file) 內部被存取。
  • 用途:當你在一個檔案中定義了多個相關的實體 (例如主要的 Class 和它的 Extension,或是由多個私有 Class 組成的功能),而它們需要互相存取資料時,使用 fileprivate

5. Private (private) - 最封閉

  • 範圍:只能在定義它的宣告範圍 (Enclosing Declaration) 及其同檔案內的 Extensions 被存取。
  • 用途:絕對的私有細節。除了該類型自己,沒人能碰到這些資料。這是保護資料完整性最強的工具。

實際應用範例:銀行帳戶

讓我們用一個「銀行帳戶」的例子來講解為什麼需要這些層級。

我們希望外部能存款、提款,但絕對不能直接修改餘額(防止餘額被隨意竄改)。

// BankAccount.swift

public class BankAccount {
    
    // 1. Private: 餘額是最高機密,只能透過內部方法修改
    // 外部完全看不見這個變數
    private var balance: Double = 0.0
    
    // 2. Public: 帳戶持有人名稱是公開資訊,但不允許被隨意修改 (使用 private(set))
    // private(set) 表示:讀取是 public,但寫入是 private
    public private(set) var owner: String
    
    // 初始化方法設為 Public,讓外部能建立帳戶
    public init(owner: String) {
        self.owner = owner
    }
    
    // 3. Public: 提供公開的方法讓外部操作,我們可以在這裡加入檢查邏輯
    public func deposit(amount: Double) {
        if amount > 0 {
            balance += amount // 內部可以存取 private 的 balance
            recordTransaction(type: "存款", amount: amount)
        }
    }
    
    public func withdraw(amount: Double) -> Bool {
        if amount > 0 && balance >= amount {
            balance -= amount
            recordTransaction(type: "提款", amount: amount)
            return true
        }
        return false
    }
    
    // 4. Fileprivate: 這個方法只在這個檔案內有用 (例如被上面的方法呼叫)
    // 但不希望這個 Class 的使用者直接呼叫它
    fileprivate func recordTransaction(type: String, amount: Double) {
        // ... 記錄交易流水號的邏輯 ...
        print("紀錄交易: \(type) $\(amount)")
    }
}

繼承與存取層級

存取層級也遵循一個原則:「實體的開放程度不能高於它所依賴的型別」。 例如,你不能寫一個 public 的變數,但它的型別卻是 private 的類別(因為外部無法理解那個 private 類別是什麼)。

private class SecretFormula { ... }

// 錯誤!public 的變數不能回傳 private 的型別
public var myFormula = SecretFormula() 

單元測試 (Unit Test) 的存取

通常我們只想測試 public 的介面。但有時為了測試完整性,我們需要測試 internal 的方法。

Swift 提供了一個特技:@testable。 在測試檔案中,只要這樣 import,就能讓測試目標 (Test Target) 存取主程式 (App Target) 中所有的 internal 成員。

@testable import MyApplication

class BankAccountTests: XCTestCase {
    func testInternalLogic() {
        // 這裡可以存取 MyApplication 中 internal 的類別和方法
    }
}

最佳實踐

  1. 預設用 private:寫程式時,先從最嚴格的 private 開始。只有當你發現必須被其他地方呼叫時,才提升到 fileprivateinternal
  2. Explicit > Implicit:雖然 internal 是預設值,但在編寫 Framework 或重要的 API 介面時,明確寫出 public / internal 有助於閱讀與理解意圖。
  3. 封裝邏輯:盡量不要 public 變數讓外部直接修改。使用 private 變數搭配 public 的方法 (Getter/Setter) 或 private(set),這樣你才能掌控資料變更的邏輯(例如驗證數值是否合法)。