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