Swift: Swift 扩展与访问控制:extension 和访问级别完全指南
扩展让你为已有的类、结构体、枚举或协议添加新功能,访问控制让你的代码接口更加清晰、封装更加严密。本课结合两者,让你写出更优雅的 Swift 代码。
1. 你将学到
- 使用
extension为已有类型添加方法和属性 - 通过扩展遵守协议
- 计算属性扩展
- Swift 的五种访问控制级别
- 访问控制的最佳实践
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 |
✅ |
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 提供五种访问控制级别,从最开放到最封闭:
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 开发,需要显式写 public 或 open 才能暴露给外部。Q open 和 public 有什么区别?
A
public 允许外部模块访问,但不允许继承和重写。open 允许外部模块继承类和重写方法。只有类(class)和类方法可以使用 open。安全原则:非必要不用 open。Q 什么时候该把代码拆到 extension 中?
A 当类型定义变得庞大时(超过 200 行),按功能分组到多个 extension 中(每个 extension 标 MARK 注释)。常见模式:核心定义、协议遵守、工具方法、便利初始化器。每个 extension 一个关注点。
📖 小节
extension可以为已有类型添加计算属性、方法、初始化器、下标和嵌套类型- extension 不能添加存储属性或覆盖已有方法
- 通过 extension 遵守协议是 Swift 的标准代码组织模式
- 五种访问级别:open > public > internal > fileprivate > private
public private(set)模式实现"公开只读、私有可写"- 同一文件中的 extension 可以访问 private 成员
📝 作业
- 基础题: 为
Int类型添加一个extension,增加squared计算属性和isEven计算属性。测试4.squared(输出 16)和3.isEven(输出 false)。 - 进阶题: 为
Date类型添加一个extension,增加formattedDate方法(返回 "yyyy-MM-dd" 格式字符串)和isToday计算属性。用extension遵守Comparable协议(如果尚未遵守)。 - 挑战题: 创建一个
SecureStore类,使用访问控制管理敏感数据。需求:public的save(key:value:)和read(key:)方法;internal的清除缓存方法;private的加密解密逻辑;fileprivate的日志方法。用 extension 将协议实现和工具方法分组到不同标记块中。