MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift访问控制与封装原则

2023-09-146.5k 阅读

Swift中的访问控制

在Swift编程中,访问控制是一项关键特性,它允许开发者控制代码中不同部分的可见性和可访问性。这对于构建模块化、安全且易于维护的软件至关重要。访问控制不仅可以保护代码的内部实现细节,防止外部代码的不当访问,还能帮助开发者清晰地定义模块之间的接口,提升代码的整体架构质量。

访问级别

Swift提供了几种不同的访问级别,每种访问级别决定了声明(比如类、函数、属性等)在程序的不同部分的可见性。

  1. 公开(publicpublic访问级别允许声明在模块内和模块外都能被访问。这通常用于定义供其他模块使用的API。例如,如果你正在开发一个供其他开发者使用的框架,框架中需要被外部访问的类、函数和属性等就可以设置为public
public class PublicClass {
    public var publicProperty: Int
    
    public init(publicProperty: Int) {
        self.publicProperty = publicProperty
    }
    
    public func publicMethod() {
        print("This is a public method.")
    }
}

在其他模块中,可以这样使用PublicClass

let publicObject = PublicClass(publicProperty: 10)
publicObject.publicMethod()
  1. 内部(internalinternal是默认的访问级别。具有internal访问级别的声明在定义它们的模块内可以被访问,但在模块外不可见。当你开发一个内部使用的库或者应用程序内部的模块时,internal访问级别很有用。例如:
class InternalClass {
    var internalProperty: String
    
    init(internalProperty: String) {
        self.internalProperty = internalProperty
    }
    
    func internalMethod() {
        print("This is an internal method.")
    }
}

在同一个模块内,你可以这样使用InternalClass

let internalObject = InternalClass(internalProperty: "Hello")
internalObject.internalMethod()

但如果在其他模块尝试访问InternalClass,编译器会报错。

  1. 文件私有(fileprivatefileprivate访问级别限制声明只能在定义它们的源文件内被访问。这对于将某些实现细节限制在特定文件中非常有用,防止其他文件中的代码意外访问。比如:
fileprivate func filePrivateFunction() {
    print("This is a file - private function.")
}

class FilePrivateClass {
    fileprivate var filePrivateProperty: Int
    
    init(filePrivateProperty: Int) {
        self.filePrivateProperty = filePrivateProperty
    }
    
    func accessFilePrivateProperty() {
        filePrivateFunction()
        print("File - private property value: \(filePrivateProperty)")
    }
}

在同一个源文件中,可以通过FilePrivateClass的实例访问filePrivateProperty和调用filePrivateFunction,但如果在其他源文件中尝试访问,编译器会报错。

  1. 私有(privateprivate访问级别是最严格的,它限制声明只能在定义它们的封闭声明(比如类、结构体或枚举)内被访问。这有助于隐藏类的内部实现细节,防止外部代码直接访问。例如:
class PrivateClass {
    private var privateProperty: String
    
    init(privateProperty: String) {
        self.privateProperty = privateProperty
    }
    
    private func privateMethod() {
        print("This is a private method.")
    }
    
    func callPrivateMethod() {
        privateMethod()
    }
}

PrivateClass外部,无法直接访问privatePropertyprivateMethod,只能通过callPrivateMethod间接调用privateMethod

访问控制与类型

  1. 类和结构体:类和结构体的访问级别决定了它们的实例在何处可以被创建,以及它们的成员(属性、方法等)在何处可以被访问。例如,一个private类只能在其定义的封闭声明内被实例化,其成员也只能在该封闭声明内被访问。
private class PrivateInnerClass {
    var innerProperty: Int
    
    init(innerProperty: Int) {
        self.innerProperty = innerProperty
    }
}

class OuterClass {
    private var privateInnerObject: PrivateInnerClass
    
    init() {
        privateInnerObject = PrivateInnerClass(innerProperty: 5)
    }
}

这里PrivateInnerClassprivate的,所以只能在OuterClass内部被实例化。

  1. 枚举:枚举的访问级别决定了枚举类型本身以及其成员在何处可以被访问。例如,一个public枚举及其成员在模块内外都可访问,而private枚举及其成员只能在其定义的封闭声明内访问。
public enum PublicEnum {
    case case1
    case case2
}

private enum PrivateEnum {
    case privateCase1
    case privateCase2
}

class EnumClass {
    func useEnums() {
        let publicEnumValue = PublicEnum.case1
        let privateEnumValue = PrivateEnum.privateCase1
    }
}

EnumClass外部,可以使用PublicEnum,但不能使用PrivateEnum

  1. 协议:协议的访问级别决定了哪些类型可以遵循该协议,以及协议的要求在何处可以被实现。例如,一个public协议可以被模块内外的类型遵循,而private协议只能被其定义的封闭声明内的类型遵循。
public protocol PublicProtocol {
    func publicProtocolMethod()
}

private protocol PrivateProtocol {
    func privateProtocolMethod()
}

class PublicProtocolClass: PublicProtocol {
    func publicProtocolMethod() {
        print("Implementing public protocol method.")
    }
}

class InnerClass {
    class InnerSubClass: PrivateProtocol {
        func privateProtocolMethod() {
            print("Implementing private protocol method.")
        }
    }
}

这里PublicProtocolClass可以在模块内外定义并使用,而InnerSubClass只能在InnerClass内部定义和使用来遵循PrivateProtocol

  1. 泛型:泛型类型参数的访问级别通常与包含它的声明的访问级别相同。例如,如果一个public函数有一个泛型参数,那么这个泛型参数在模块内外都可以被使用和推断。
public func publicGenericFunction<T>(value: T) {
    print("The value is of type \(type(of: value))")
}

publicGenericFunction(value: 10)

这里publicGenericFunctionpublic的,其泛型参数T在模块内外都可使用。

封装原则

封装是面向对象编程的重要原则之一,它与访问控制紧密相关。封装的核心思想是将数据(属性)和操作数据的方法(函数)包装在一起,并隐藏对象的内部实现细节,只向外部提供必要的接口。通过封装,对象的使用者不需要了解对象的内部工作原理,只需要知道如何通过公开的接口与对象进行交互。

封装的好处

  1. 数据保护:通过将属性设置为privatefileprivate,可以防止外部代码直接修改对象的内部状态,确保数据的完整性和一致性。例如,假设我们有一个表示银行账户的类:
class BankAccount {
    private var balance: Double
    
    init(initialBalance: Double) {
        balance = initialBalance
    }
    
    func deposit(amount: Double) {
        if amount > 0 {
            balance += amount
        }
    }
    
    func withdraw(amount: Double) -> Bool {
        if amount > 0 && amount <= balance {
            balance -= amount
            return true
        }
        return false
    }
    
    func getBalance() -> Double {
        return balance
    }
}

这里balance属性是private的,外部代码不能直接修改它。只能通过depositwithdraw方法来操作余额,这两个方法可以对操作进行必要的验证,如确保存款金额为正,取款金额不超过余额等。

  1. 代码模块化和可维护性:封装使得代码更加模块化,每个对象都有自己独立的职责。如果需要修改对象的内部实现,只要公开接口不变,对其他部分的代码影响就很小。例如,如果我们决定改变BankAccount类中余额的存储方式,从Double改为Decimal,只要depositwithdrawgetBalance方法的接口不变,使用BankAccount类的其他代码就不需要修改。

  2. 信息隐藏:封装隐藏了对象的内部复杂性,只向外部暴露简单易懂的接口。这使得其他开发者更容易使用该对象,而不需要了解其内部的复杂实现。比如,对于使用BankAccount类的开发者来说,只需要知道如何调用depositwithdrawgetBalance方法,而不需要知道余额是如何具体存储和管理的。

实现封装的方式

  1. 访问控制修饰符:如前文所述,Swift的访问控制修饰符(privatefileprivateinternalpublic)是实现封装的重要手段。通过合理设置属性和方法的访问级别,可以有效地隐藏内部实现细节。例如,将类的内部计算逻辑封装在private方法中,只将需要外部调用的接口设置为public
class MathUtils {
    private func square(_ number: Int) -> Int {
        return number * number
    }
    
    public func calculateSquareAndAdd(_ number1: Int, _ number2: Int) -> Int {
        let square1 = square(number1)
        let square2 = square(number2)
        return square1 + square2
    }
}

这里square方法是private的,外部代码无法直接调用,只能通过calculateSquareAndAdd这个公开方法来间接使用square方法的功能。

  1. 属性包装器:属性包装器是Swift 5.1引入的特性,它也可以用于实现封装。属性包装器允许开发者将属性的存储和逻辑封装在一个单独的类型中。例如,我们可以创建一个属性包装器来验证和限制属性的值:
@propertyWrapper
struct RangeValidator<T: Comparable> {
    private var value: T
    private let minimum: T
    private let maximum: T
    
    init(wrappedValue: T, minimum: T, maximum: T) {
        self.minimum = minimum
        self.maximum = maximum
        self.value = min(max(wrappedValue, minimum), maximum)
    }
    
    var wrappedValue: T {
        get {
            return value
        }
        set {
            value = min(max(newValue, minimum), maximum)
        }
    }
}

class Temperature {
    @RangeValidator(minimum: -40, maximum: 125)
    var currentTemperature: Int
    
    init(currentTemperature: Int) {
        self.currentTemperature = currentTemperature
    }
}

这里RangeValidator属性包装器封装了对currentTemperature属性值的验证和限制逻辑,外部代码在设置currentTemperature时,其值会自动被限制在指定的范围内。

  1. 嵌套类型:通过使用嵌套类型,可以将一些辅助类型封装在主类型内部,使其对外不可见。例如,在一个表示图形的类中,我们可以将表示图形内部点的结构体嵌套在类内部:
class Shape {
    private struct Point {
        var x: Int
        var y: Int
    }
    
    private var points: [Point] = []
    
    func addPoint(x: Int, y: Int) {
        let newPoint = Point(x: x, y: y)
        points.append(newPoint)
    }
}

这里Point结构体是private且嵌套在Shape类内部,外部代码无法直接访问Point结构体,只能通过Shape类的addPoint方法来间接操作Point对象。

访问控制与封装的实际应用场景

  1. 框架开发:在开发框架时,public访问级别用于定义框架的API,让其他开发者可以使用框架提供的功能。同时,通过privatefileprivate访问级别来隐藏框架的内部实现细节,确保框架的稳定性和安全性。例如,一个网络请求框架可能会公开一些用于发起请求、处理响应的public类和方法,而将网络连接的建立、数据解析等内部逻辑封装在privatefileprivate的类和方法中。
// 网络请求框架示例
public class NetworkClient {
    private let session: URLSession
    
    public init() {
        session = URLSession.shared
    }
    
    public func sendRequest(url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
        let task = session.dataTask(with: url, completionHandler: completion)
        task.resume()
    }
}

这里NetworkClient类及其sendRequest方法是public的,而session属性和创建dataTask的逻辑是private的。

  1. 应用程序开发:在应用程序内部,不同模块之间可以通过internal访问级别来共享代码,同时使用privatefileprivate来封装模块内部的实现细节。例如,在一个电商应用中,用户模块可能有一些internal的类和方法用于处理用户登录、注册等功能,而将用户数据的加密和解密逻辑封装在private的方法中。
// 电商应用用户模块示例
class UserModule {
    private func encryptPassword(_ password: String) -> String {
        // 简单的加密逻辑示例
        var encrypted = ""
        for char in password {
            encrypted.append("\(UnicodeScalar(char.asciiValue! + 1)!)")
        }
        return encrypted
    }
    
    internal func registerUser(username: String, password: String) {
        let encryptedPassword = encryptPassword(password)
        // 这里可以添加将用户名和加密后的密码保存到数据库等逻辑
        print("User \(username) registered with encrypted password \(encryptedPassword)")
    }
}

这里encryptPassword方法是private的,registerUser方法是internal的,其他模块可以调用registerUser方法,但不能直接调用encryptPassword方法。

  1. 单元测试:在编写单元测试时,访问控制和封装也起着重要作用。通常,测试代码和被测试代码在不同的模块中。被测试代码中的一些internal成员可能需要在测试模块中访问,以便进行更全面的测试。通过将这些成员设置为internal,并在测试模块中使用@testable import语句,可以在测试模块中访问被测试模块的internal声明。例如:
// 被测试代码
class Calculator {
    internal func add(_ a: Int, _ b: Int) -> Int {
        return a + b
    }
}

// 测试代码
@testable import YourModuleName

import XCTest

class CalculatorTests: XCTestCase {
    func testAddition() {
        let calculator = Calculator()
        let result = calculator.add(2, 3)
        XCTAssertEqual(result, 5)
    }
}

这里Calculator类的add方法是internal的,测试模块通过@testable import可以访问并测试该方法。

访问控制与封装的注意事项

  1. 过度封装:虽然封装很重要,但过度封装可能会导致代码变得过于复杂和难以维护。例如,如果将所有方法和属性都设置为private,可能会使得代码的复用性降低,因为其他部分的代码无法访问和利用这些功能。在设计时,需要在保护内部实现和提供必要的接口之间找到平衡。

  2. 访问级别一致性:在设计类、结构体、协议等时,要确保其成员的访问级别与整体的访问级别一致且合理。例如,如果一个类是public的,但它的所有方法和属性都是private,那么这个类对外部来说几乎没有实际用途。同样,如果一个private类有public的方法,这也不符合访问控制的逻辑。

  3. 文档说明:当使用不同的访问级别进行封装时,对于publicinternal的接口,应该提供清晰的文档说明,让其他开发者知道如何使用这些接口以及它们的功能和限制。这有助于提高代码的可理解性和可维护性,特别是在团队开发和框架开发中。

  4. 跨模块访问:在处理跨模块访问时,要注意模块之间的依赖关系和访问权限。确保public接口的稳定性,避免频繁修改,因为这可能会影响到依赖该模块的其他代码。同时,合理使用@testable import进行测试时,要注意测试代码对被测试模块内部internal成员的访问是否符合测试的目的和逻辑。

总之,Swift的访问控制和封装原则是构建高质量、可维护和安全的代码的重要工具。通过合理使用访问控制修饰符、遵循封装原则,并注意实际应用中的各种情况,可以开发出结构清晰、易于理解和扩展的软件系统。无论是开发小型应用程序还是大型框架,掌握这些概念和技术都是非常关键的。