Swift: Swift 扩展与访问控制:extension 和访问级别完全指南

扩展让你为已有的类、结构体、枚举或协议添加新功能,访问控制让你的代码接口更加清晰、封装更加严密。本课结合两者,让你写出更优雅的 Swift 代码。

1. 你将学到


2. 一个后端开发者的真实故事

(1) 痛点:无法给 Foundation 的 String 添加功能

Bob 在处理日期格式化时,发现 Date 没有直接转换为"多少分钟前"这种人性化格式的方法。他写了一个工具函数:

SWIFT
func timeAgo(from date: Date) -> String {
    let seconds = Date().timeIntervalSince(date)
    if seconds < 60 { return "\(Int(seconds))s ago" }
    if seconds < 3600 { return "\(Int(seconds / 60))m ago" }
    return "\(Int(seconds / 3600))h ago"
}
// 每次都要传参调用
print(timeAgo(from: someDate))

每次使用都要调用全局函数,代码不够自然。 Bob 想要让 Date 自己就能返回这个格式——就像自带的方法一样。

(2) extension 的解法

SWIFT
extension Date {
    func timeAgo() -> String {
        let seconds = Date().timeIntervalSince(self)
        if seconds < 60 { return "\(Int(seconds))s ago" }
        if seconds < 3600 { return "\(Int(seconds / 60))m ago" }
        return "\(Int(seconds / 3600))h ago"
    }
}
// 看起来就像 Date 自带的方法
print(someDate.timeAgo())

扩展让 Date 拥有了新的方法,无需继承,无需修改源码。

(3) 收益:自然语法 + 高内聚

维度 全局函数 Extension
调用方式 timeAgo(from: date) date.timeAgo()
代码组织 分散在各处 紧贴类型定义
发现性 需手动 import 工具模块 Xcode 自动补全
可维护性 全局命名空间污染 类型特定命名空间

3. 扩展(Extension)

(1) 扩展的语法

扩展可以为已有类型添加:

可添加内容 示例 限制
计算属性 var area: Double { ... } ❌ 不能添加存储属性
实例/类型方法 func reversed() -> String
初始化器 init?(json: [String: Any]) ❌ 不能添加 deinit
下标 subscript(idx: Int) -> T
嵌套类型 enum Status { ... }
协议遵守 extension Type: Protocol
100%
graph TB
    A["extension SomeType {"] --> B["计算属性"]
    A --> C["方法"]
    A --> D["初始化器"]
    A --> E["下标"]
    A --> F["嵌套类型"]
    A --> G["协议实现"]
    A --> H["}"]

▶ 示例:为 String 添加扩展

SWIFT
// ============================================
// 为 String 添加实用扩展
// ============================================
extension String {
    // 计算属性——是否为有效的邮箱
    var isValidEmail: Bool {
        return contains("@") && contains(".")
    }
    // 方法——截取前 N 个字符
    func prefix(_ maxLength: Int) -> String {
        guard count > maxLength else { return self }
        return String(self.prefix(maxLength)) + "..."
    }
    // 方法——反转字符串
    func reversed() -> String {
        return String(self.reversed())
    }
}
let email = "alice@example.com"
print("\(email) is valid: \(email.isValidEmail)")
let longText = "This is a very long string that needs truncation"
print(longText.prefix(20))
print("Hello".reversed())

输出:

TEXT 📖 仅展示
alice@example.com is valid: true
This is a very lon...
olleH

⚠️ 注意: 扩展不能覆盖已有的方法。如果扩展中定义的方法与已有方法同名,编译器会报错。

(2) 通过扩展遵守协议

将协议实现从类型定义中分离,通过扩展单独实现:

SWIFT
// ============================================
// 通过扩展遵守协议
// ============================================
// 1. 定义基础类型
struct User {
    let id: Int
    let name: String
    let email: String
}
// 2. 通过扩展遵守 Hashable(用于 Set 和 Dictionary 键)
extension User: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}
// 3. 通过扩展遵守 Equatable
extension User: Equatable {
    static func == (lhs: User, rhs: User) -> Bool {
        return lhs.id == rhs.id
    }
}
// 4. 使用
let users: Set<User> = [
    User(id: 1, name: "Alice", email: "alice@example.com"),
    User(id: 1, name: "Alice", email: "alice@example.com"),  // 重复——被去重
    User(id: 2, name: "Bob", email: "bob@example.com")
]
print("Users count: \(users.count)")  // 2

输出:

TEXT 📖 仅展示
Users count: 2

💡 提示: 将协议实现放在扩展中是为了代码组织——类型定义只包含核心数据,行为通过扩展添加。这是 Swift 标准库的常见模式。


4. 访问控制

(1) 五种访问级别

Swift 提供五种访问控制级别,从最开放到最封闭:

100%
graph TB
    A[访问控制金字塔] --> B["open — 最开放,任何人可访问/继承/重写"]
    A --> C["public — 任何人可访问,但不能继承/重写"]
    A --> D["internal — 当前模块内(默认级别)"]
    A --> E["fileprivate — 当前文件内"]
    A --> F["private — 当前声明的大括号内"]
级别 关键字 访问范围 可用于
最开放 open 任何模块,可继承重写 类、类方法
public 任何模块,但不可继承重写 类、方法、属性
默认 internal 当前模块(App 或 Framework) 所有类型
fileprivate 当前文件 类型、方法、属性
最封闭 private 当前声明作用域(花括号) 属性、方法

▶ 示例:访问控制实战

SWIFT
// ============================================
// 五种访问级别的使用
// ============================================
// 公开给所有模块使用的类
public class BankAccount {
    // 对外只读,对内可写
    public private(set) var balance: Double
    // 私有字段——仅当前类可访问
    private var accountNumber: String
    private var transactionHistory: [String] = []
    // 文件内可访问——用于日志
    fileprivate var lastTransactionDate: Date?
    // 模块内可访问——用于内部审计
    internal var branchCode: String
    public init(accountNumber: String, balance: Double, branchCode: String) {
        self.accountNumber = accountNumber
        self.balance = balance
        self.branchCode = branchCode
    }
    public func deposit(amount: Double) {
        balance += amount
        transactionHistory.append("Deposit: \(amount)")
        lastTransactionDate = Date()
    }
    // 私有辅助方法
    private func log(_ message: String) {
        print("[Private] \(message)")
    }
}
let account = BankAccount(accountNumber: "123-456", balance: 1000, branchCode: "BR-001")
print("Balance: \(account.balance)")    // public getter 可读
// account.balance = 2000               // 编译错误!public private(set) 不让外部写
// print(account.accountNumber)         // 编译错误!private 属性
// print(account.lastTransactionDate)   // 编译错误!fileprivate,不同文件不可访问
print("Branch: \(account.branchCode)")  // internal 同模块可访问

输出:

TEXT 📖 仅展示
Balance: 1000.0
Branch: BR-001

(2) 访问控制原则

原则 说明
最小权限 默认用 private,需要时逐步放开
接口稳定 public API 一经发布视为契约,修改需谨慎
模块边界 Framework 的 public 是模块边界,internal 是内部实现
getter/setter public private(set) 模式:公开读、私有写

5. 扩展与访问控制

(1) 同文件中的 private 共享

private 成员在同一文件中的 extension 内也可访问:

SWIFT
// ============================================
// private 在 extension 中的可见性
// ============================================
struct UserAccount {
    private var token: String = "secret-token"
    let name: String
    func getToken() -> String {
        return token
    }
}
// 同一文件中的 extension 可以访问 private 成员
extension UserAccount {
    func resetToken() {
        token = "new-token"  // 可以访问!同一文件中
    }
}
var account = UserAccount(name: "Alice")
print(account.getToken())
account.resetToken()
print(account.getToken())

输出:

TEXT 📖 仅展示
secret-token
new-token

⚠️ 注意: Swift 4+ 中同一文件的 extension 可以访问 private 成员。如果需要跨文件共享,使用 fileprivate

(2) 扩展的最佳组织模式

SWIFT
// 核心数据定义
struct Product {
    let id: Int
    let name: String
    let price: Double
}
// MARK: - 遵守 Equatable
extension Product: Equatable {
    static func == (lhs: Product, rhs: Product) -> Bool {
        return lhs.id == rhs.id
    }
}
// MARK: - 遵守 Codable
extension Product: Codable { }
// MARK: - 工具方法
extension Product {
    func formattedPrice() -> String {
        return "$\(String(format: "%.2f", price))"
    }
    var priceWithTax: Double {
        return price * 1.08
    }
}

▶ 示例:扩展添加嵌套类型与便利初始化器

扩展不仅可以添加方法和计算属性,还可以添加嵌套类型和初始化器:

SWIFT
// ============================================
// 扩展添加嵌套类型与便利初始化器
// ============================================
// 基础类型
struct Book {
    let title: String
    let author: String
    let year: Int
}
// 扩展:添加嵌套类型和便利初始化器
extension Book {
    // 嵌套枚举——分类
    enum Genre: String {
        case fiction = "小说"
        case nonfiction = "非小说"
        case science = "科学"
        case history = "历史"
    }
    // 嵌套结构体——阅读统计
    struct ReadingStats {
        var pagesRead: Int
        var totalPages: Int
        var progress: Double {
            totalPages > 0 ? Double(pagesRead) / Double(totalPages) * 100 : 0
        }
    }
    // 便利初始化器——从 JSON 字典创建
    init?(from dict: [String: Any]) {
        guard let title = dict["title"] as? String,
              let author = dict["author"] as? String,
              let year = dict["year"] as? Int else {
            return nil
        }
        self.init(title: title, author: author, year: year)
    }
}
// 使用
let book1 = Book(title: "Swift 编程", author: "Apple", year: 2024)
let genre = Book.Genre.science
print("\(book1.title) - 分类:\(genre.rawValue)")
let stats = Book.ReadingStats(pagesRead: 120, totalPages: 350)
print("阅读进度:\(String(format: "%.1f", stats.progress))%")
// 从字典创建
let dict = ["title": "Swift 进阶", "author": "Alice", "year": 2025] as [String: Any]
if let book2 = Book(from: dict) {
    print("从字典创建:\(book2.title) (\(book2.author), \(book2.year))")
}

输出:

TEXT 📖 仅展示
Swift 编程 - 分类:科学
阅读进度:34.3%
从字典创建:Swift 进阶 (Alice, 2025)

6. 完整示例:用户管理系统(Extension + 访问控制)

SWIFT
// ============================================
// 完整示例:用户管理系统
// 功能:extension + 访问控制综合运用
// ============================================
import Foundation
// 1. 核心用户类型
public class User {
    public let id: Int
    public let name: String
    public let email: String
    // 对外只读,对内可写
    public private(set) var score: Int = 0
    public private(set) var isActive: Bool = true
    // 模块内部可访问
    internal var lastLogin: Date?
    // 私有
    private var loginHistory: [Date] = []
    public init(id: Int, name: String, email: String) {
        self.id = id
        self.name = name
        self.email = email
    }
    public func login() {
        let now = Date()
        lastLogin = now
        loginHistory.append(now)
        isActive = true
    }
    public func addScore(_ points: Int) {
        // 内部校验
        guard points > 0 else { return }
        score += points
    }
}
// MARK: - 描述信息扩展
extension User {
    public func description() -> String {
        return "User(\(id): \(name), score: \(score), active: \(isActive))"
    }
    // 根据最后登录时间判断
    public var isOnline: Bool {
        guard let last = lastLogin else { return false }
        return Date().timeIntervalSince(last) < 300
    }
}
// MARK: - 活动日志扩展
extension User {
    // fileprivate——仅本文件可访问
    fileprivate func generateActivityReport() -> String {
        return """
        User Report - \(name)
        =================
        Score: \(score)
        Active: \(isActive)
        Last Login: \(lastLogin?.description ?? "Never")
        """
    }
    public func printReport() {
        print(generateActivityReport())
    }
}
// 3. 使用示例
let user = User(id: 1, name: "Alice", email: "alice@example.com")
user.login()
user.addScore(50)
user.addScore(30)
print(user.description())
print("Online: \(user.isOnline)")
user.printReport()
// user.score = 100        // 编译错误——public private(set)
// user.loginHistory        // 编译错误——private

输出:

TEXT 📖 仅展示
User(1: Alice, score: 80, active: true)
Online: true
User Report - Alice
=================
Score: 80
Active: true
Last Login: 2026-07-30 12:00:00 +0000

❓ 常见问题

Q extension 和继承有什么区别?
A extension 为「已有类型」添加功能(包括系统类型如 String、Date),不需要创建新类型。继承只能用于自己的类,且用于改变行为。extension 不能添加存储属性,继承可以。
Q 可以在 extension 中添加存储属性吗?
A 不能。extension 只能添加计算属性、方法、初始化器、下标和嵌套类型。存储属性必须在原始类型定义中。这是 Swift 语言设计决定——避免破坏已有类型的内存布局。
Q 默认访问级别是什么?
A internal。如果你不写任何访问控制修饰符,默认为 internal——当前模块内可访问。对于 App 开发(单模块),internal 相当于 public。对于 Framework 开发,需要显式写 publicopen 才能暴露给外部。
Q open 和 public 有什么区别?
A public 允许外部模块访问,但不允许继承和重写。open 允许外部模块继承类和重写方法。只有类(class)和类方法可以使用 open。安全原则:非必要不用 open
Q 什么时候该把代码拆到 extension 中?
A 当类型定义变得庞大时(超过 200 行),按功能分组到多个 extension 中(每个 extension 标 MARK 注释)。常见模式:核心定义、协议遵守、工具方法、便利初始化器。每个 extension 一个关注点。

📖 小节


📝 作业

  1. 基础题:Int 类型添加一个 extension,增加 squared 计算属性和 isEven 计算属性。测试 4.squared(输出 16)和 3.isEven(输出 false)。
  2. 进阶题:Date 类型添加一个 extension,增加 formattedDate 方法(返回 "yyyy-MM-dd" 格式字符串)和 isToday 计算属性。用 extension 遵守 Comparable 协议(如果尚未遵守)。
  3. 挑战题: 创建一个 SecureStore 类,使用访问控制管理敏感数据。需求:publicsave(key:value:)read(key:) 方法;internal 的清除缓存方法;private 的加密解密逻辑;fileprivate 的日志方法。用 extension 将协议实现和工具方法分组到不同标记块中。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏