Swift 访问控制:从入门到实战

1. 引言

访问控制是 Swift 语言中一项非常重要的特性,它决定了代码中各个实体(如类、结构体、属性、方法等)可以被哪些作用域访问。合理使用访问控制,不仅能让代码的接口更加清晰,还能有效隐藏内部实现细节,降低模块之间的耦合度。本文将从基础概念讲起,结合丰富的代码实例,带你全面掌握 Swift 访问控制的用法。

2. 访问控制的基本概念

在 Swift 中,访问控制通过关键字来限制代码实体的可见范围。Swift 提供了五个级别的访问控制,从最开放到最严格依次是:openpublicinternalfileprivateprivate

访问级别可见范围适用场景
open模块内和模块外均可访问,且可被外部继承和重写框架的公共 API,允许外部子类化
public模块内和模块外均可访问,但外部不可继承和重写框架的公共 API,但不希望被外部继承
internal仅限当前模块内访问(默认级别)App 内部或框架内部使用的代码
fileprivate仅限当前源文件内访问同一文件内多个类型共享的辅助代码
private仅限当前声明及其同一文件内的扩展中访问隐藏类型内部的实现细节

如果不显式声明访问级别,Swift 默认使用 internal。这意味着在大多数 App 开发场景中,你写的代码默认可以在当前模块内任意位置访问。

3. 五个访问级别的代码示例

下面通过一个完整的示例来演示五个访问级别的具体用法。

// 定义一个公开的类,允许外部继承
open class Animal {
    // 公开属性,外部可读写
    public var name: String
    
    // 内部属性,仅当前模块可访问
    internal var age: Int
    
    // 文件私有属性,仅当前文件可访问
    fileprivate var secret: String
    
    // 私有属性,仅当前类型及其扩展可访问
    private var id: String
    
    public init(name: String, age: Int) {
        self.name = name
        self.age = age
        self.secret = "这是一个秘密"
        self.id = "唯一标识"
    }
    
    // 公开方法,外部可调用
    public func speak() {
        print("\(name) 在叫")
    }
    
    // 私有方法,仅内部使用
    private func generateID() {
        print("生成 ID: \(id)")
    }
}

// 在同一个文件中,可以访问 fileprivate 成员
let animal = Animal(name: "旺财", age: 3)
print(animal.secret) // 可以访问,因为 secret 是 fileprivate

// 在同一个模块中,可以访问 internal 成员
print(animal.age) // 可以访问,因为 age 是 internal

// 无法访问 private 成员
// print(animal.id) // 编译错误:'id' is inaccessible due to 'private' protection level

上面的代码展示了不同访问级别在同一个文件中的表现。需要注意的是,private 成员只能在声明它的类型内部访问,而 fileprivate 成员可以在同一个源文件中的任何位置访问。

4. open 与 public 的区别

openpublic 都允许外部模块访问,但有一个关键区别:open 允许外部模块继承和重写,而 public 不允许。下面通过一个跨模块的示例来说明。

// 假设这是框架模块中的代码
open class OpenBaseClass {
    open func openMethod() {
        print("open 方法")
    }
    
    public func publicMethod() {
        print("public 方法")
    }
}

public class PublicBaseClass {
    public func publicMethod() {
        print("public 方法")
    }
}

// 假设这是使用框架的 App 模块中的代码
class MySubClass: OpenBaseClass {
    // 可以重写 open 方法
    override func openMethod() {
        print("重写了 open 方法")
    }
    
    // 不能重写 public 方法
    // override func publicMethod() { } // 编译错误
}

// 不能继承 public 类
// class MyOtherClass: PublicBaseClass { } // 编译错误

在设计框架时,如果你希望外部开发者能够继承某个类并重写其方法,就应该使用 open;如果只是希望外部使用而不允许继承,则使用 public

5. private 与 fileprivate 的区别

privatefileprivate 都用于限制访问范围,但它们的粒度不同。private 限制在声明所在的类型及其同文件扩展中,而 fileprivate 限制在声明所在的整个源文件中。下面通过示例来区分。

struct Person {
    // private 属性,仅 Person 及其扩展可访问
    private var privateName: String = "私有名字"
    
    // fileprivate 属性,整个文件可访问
    fileprivate var fileprivateName: String = "文件私有名字"
    
    func showPrivate() {
        print(privateName) // 可以访问
    }
}

// 在同一个文件中定义扩展
extension Person {
    func showFromExtension() {
        print(privateName) // 可以访问,因为扩展与声明在同一文件
    }
}

// 在同一个文件中定义另一个类型
struct Helper {
    func showNames() {
        let person = Person()
        // print(person.privateName) // 编译错误:无法访问 private
        print(person.fileprivateName) // 可以访问,因为 fileprivate 在整个文件内可见
    }
}

从上面的例子可以看出,fileprivate 的访问范围比 private 更大。在 Swift 4 之前,private 的访问范围与 fileprivate 相同,但在 Swift 4 之后,private 被限制在了类型及其扩展内部。

6. 访问控制与属性、方法、初始化器

访问控制不仅适用于类型本身,还适用于类型的属性、方法和初始化器。下面通过一个实际例子来演示。

public class BankAccount {
    // 私有属性,外部无法直接访问
    private var balance: Double = 0
    
    // 公开只读属性,外部可以读取但无法修改
    public private(set) var accountNumber: String
    
    // 内部属性,仅模块内可访问
    internal var bankName: String
    
    public init(accountNumber: String, bankName: String) {
        self.accountNumber = accountNumber
        self.bankName = bankName
    }
    
    // 公开方法,外部可调用
    public func deposit(amount: Double) {
        balance += amount
        print("存入 \(amount),当前余额 \(balance)")
    }
    
    // 公开方法,外部可调用
    public func withdraw(amount: Double) {
        if amount <= balance {
            balance -= amount
            print("取出 \(amount),当前余额 \(balance)")
        } else {
            print("余额不足")
        }
    }
    
    // 私有方法,仅内部使用
    private func calculateInterest() -> Double {
        return balance * 0.03
    }
    
    // 公开方法,内部调用私有方法
    public func showInterest() {
        let interest = calculateInterest()
        print("当前利息为 \(interest)")
    }
}

let account = BankAccount(accountNumber: "123456", bankName: "示例银行")
account.deposit(amount: 1000)
account.withdraw(amount: 200)
account.showInterest()

// 可以读取 accountNumber
print(account.accountNumber)

// 无法修改 accountNumber
// account.accountNumber = "654321" // 编译错误

// 无法直接访问 balance
// print(account.balance) // 编译错误

在这个例子中,balance 被设置为 private,外部无法直接访问,只能通过公开方法进行存款和取款操作。而 accountNumber 使用了 public private(set),表示外部可以读取但不能修改,这在很多场景下非常实用。

7. 访问控制与扩展

在 Swift 中,扩展可以访问原类型中 privatefileprivate 的成员,前提是扩展与原类型在同一个文件中。这一特性让我们可以在扩展中补充功能,同时保持内部实现的隐藏。

struct User {
    private var password: String = "123456"
    
    func validatePassword(_ input: String) -> Bool {
        return input == password
    }
}

// 在同一个文件中扩展 User
extension User {
    // 可以访问 private 属性
    func resetPassword() {
        // 这里可以修改 password
        // 注意:由于 password 是 var,且扩展在同一文件,可以访问
        // 但实际修改需要通过方法实现
        print("密码已重置")
    }
    
    // 添加新的计算属性
    var passwordLength: Int {
        return password.count
    }
}

let user = User()
print(user.passwordLength) // 输出:6
print(user.validatePassword("123456")) // 输出:true

需要注意的是,如果扩展与原类型不在同一个文件中,那么扩展只能访问 internal 及以上级别的成员,无法访问 privatefileprivate 成员。

8. 访问控制与协议

协议中的访问控制与类型略有不同。协议本身可以声明访问级别,协议中的每个要求也会继承协议的访问级别。下面通过示例来说明。

// 定义一个公开协议
public protocol Greetable {
    func greet()
}

// 定义一个内部协议
internal protocol InternalProtocol {
    func doSomething()
}

// 公开类遵循公开协议
public class EnglishSpeaker: Greetable {
    public func greet() {
        print("Hello!")
    }
}

// 内部类遵循内部协议
class InternalWorker: InternalProtocol {
    func doSomething() {
        print("Doing something...")
    }
}

// 注意:协议要求的访问级别不能高于协议本身
// 如果协议是 internal,那么它的要求也不能是 public

在实现协议时,协议要求的实现方法的访问级别必须与协议要求的访问级别一致或更高。例如,如果协议要求是 public,那么实现方法也必须是 public

9. 访问控制与泛型

泛型类型和泛型函数的访问控制由其自身及其类型参数共同决定。下面通过一个泛型示例来演示。

// 公开的泛型结构体
public struct Stack<Element> {
    // 私有存储属性
    private var items: [Element] = []
    
    // 公开方法
    public mutating func push(_ item: Element) {
        items.append(item)
    }
    
    public mutating func pop() -> Element? {
        return items.isEmpty ? nil : items.removeLast()
    }
    
    public var count: Int {
        return items.count
    }
}

// 使用泛型类型
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
intStack.push(3)
print(intStack.count) // 输出:3
print(intStack.pop() ?? 0) // 输出:3

var stringStack = Stack<String>()
stringStack.push("Swift")
stringStack.push("访问控制")
print(stringStack.pop() ?? "") // 输出:访问控制

泛型类型的访问级别不能高于其类型参数的访问级别。例如,一个 public 的泛型类型,其类型参数也必须是 publicopen 级别。

10. 访问控制与嵌套类型

嵌套类型的访问级别不能高于其外部类型的访问级别。下面通过示例来演示嵌套类型的访问控制。

public struct OuterStruct {
    // 公开嵌套类型
    public struct PublicNested {
        public var value: Int = 0
    }
    
    // 私有嵌套类型
    private struct PrivateNested {
        var secret: String = "秘密"
    }
    
    // 内部嵌套类型
    struct InternalNested {
        var name: String = "内部"
    }
    
    // 公开方法,返回公开嵌套类型
    public func makePublicNested() -> PublicNested {
        return PublicNested()
    }
    
    // 内部方法,使用内部嵌套类型
    func makeInternalNested() -> InternalNested {
        return InternalNested()
    }
    
    // 私有方法,使用私有嵌套类型
    private func makePrivateNested() -> PrivateNested {
        return PrivateNested()
    }
}

let outer = OuterStruct()
let publicNested = outer.makePublicNested()
print(publicNested.value)

// 无法访问私有嵌套类型
// let privateNested = outer.makePrivateNested() // 编译错误

嵌套类型的访问级别不能高于其外部类型。例如,外部类型是 internal,那么嵌套类型就不能声明为 public

11. 访问控制与元组

元组的访问级别由其成员中访问级别最低的那个决定。下面通过示例来说明。

// 定义一个包含不同访问级别成员的元组
// 元组的访问级别由最低访问级别的成员决定
private struct PrivateData {
    var value: Int = 0
}

// 这个元组包含 private 成员,因此整个元组是 private
private let mixedTuple: (name: String, data: PrivateData) = (name: "示例", data: PrivateData())

// 在同一个文件中可以访问
print(mixedTuple.name)

// 如果元组中包含 private 成员,那么元组本身也只能是 private 或 fileprivate

在实际开发中,元组的访问级别通常由其成员自动推断,我们很少需要显式声明元组的访问级别。

12. 访问控制的最佳实践

合理使用访问控制可以让代码更加健壮和易维护。以下是一些最佳实践建议:

  • 默认使用 internal:在 App 开发中,大多数代码不需要跨模块访问,使用默认的 internal 即可。
  • 对外接口使用 public 或 open:如果开发框架或 SDK,对外暴露的接口应使用 public 或 open,并谨慎设计。
  • 隐藏实现细节:尽量将内部实现细节标记为 privatefileprivate,减少外部依赖。
  • 使用 private(set):对于需要外部读取但不需要修改的属性,使用 private(set) 可以很好地控制可变性。
  • 避免过度暴露:不要为了图方便把所有成员都设为 public,这会让接口变得臃肿且难以维护。
// 一个遵循最佳实践的示例
public struct Product {
    // 对外只读,对内可写
    public private(set) var name: String
    public private(set) var price: Double
    
    // 内部属性,模块内可访问
    internal var discount: Double = 0
    
    // 私有属性,隐藏内部状态
    private var stock: Int = 0
    
    public init(name: String, price: Double) {
        self.name = name
        self.price = price
    }
    
    // 公开方法
    public func displayInfo() {
        print("商品:\(name),价格:\(price) 元")
    }
    
    // 内部方法
    internal func applyDiscount(_ rate: Double) {
        discount = rate
    }
    
    // 私有方法
    private func checkStock() -> Bool {
        return stock > 0
    }
}

let product = Product(name: "iPhone", price: 5999)
product.displayInfo()
print(product.name) // 可以读取
// product.name = "iPad" // 编译错误:无法修改

13. 总结

本文详细介绍了 Swift 访问控制的五个级别:openpublicinternalfileprivateprivate,并通过丰富的代码示例展示了它们在不同场景下的用法。掌握访问控制,能够帮助你设计出接口清晰、内部实现隐藏的高质量代码。在实际开发中,建议根据代码的使用范围合理选择访问级别,遵循「最小暴露」原则,让代码更加健壮和易维护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值