Swift基础知识(二)

411 阅读12分钟

一、Swift 属性观察器(Property Observers)

1. 核心概念

属性观察器用于监听存储属性值的变化,在值被修改前后执行自定义逻辑。包含两种观察器:

  • willSet:在值即将被设置前调用,默认提供 newValue 参数(新值)。
  • didSet:在值已被设置后调用,默认提供 oldValue 参数(旧值)。

2. 使用条件

  • 适用对象:仅用于 lazyvar 存储属性
    var score: Int = 0 {  // 正确:非 lazy 的存储属性
        willSet { print("新值:\(newValue)") }
        didSet { print("旧值:\(oldValue)") }
    }
    
    // lazy var data: [Int] = loadData() { willSet { } } // 错误:lazy 属性不支持
    
  • 不适用场景
    • 常量(let 属性)。
    • 计算属性(需通过 set 方法监听)。
    • 延迟存储属性(lazy var)。

3. 注意事项

  • 初始化不触发:属性在初始化赋值时不会调用观察器。
    struct User {
        var name: String = "Guest" { // 初始化赋值不会触发观察器
            didSet { print("名字被修改") }
        }
    }
    
  • 自定义参数名:可显式命名参数(如 willSet(newScore))。
  • 避免循环修改:在 didSet 中修改属性本身会再次触发观察器。

4. 使用场景

  • 数据验证:确保值在合法范围内。
    var age: Int = 0 {
        didSet {
            age = min(max(age, 0), 120)  // 限制年龄在 0~120
        }
    }
    
  • UI 同步更新:值变化时刷新界面。
    var isLoggedIn: Bool = false {
        didSet {
            updateLoginUI()
        }
    }
    
  • 日志记录:跟踪属性变化历史。
    var temperature: Double = 25.0 {
        willSet { print("温度将从 \(temperature) 变为 \(newValue)") }
        didSet { print("温度已从 \(oldValue) 更新为 \(temperature)") }
    }
    

5. 与计算属性的区别

特性属性观察器计算属性
用途监听存储属性变化通过计算动态获取值
触发时机属性值被修改前后每次访问时计算
存储能力需有存储空间(必须初始化)无存储空间(依赖其他属性)
语法willSet/didSetget + set(或只读 get

6. 总结

  • 核心作用:在属性值变化前后注入逻辑,提升代码灵活性和可维护性。
  • 适用场景:数据验证、日志记录、UI 同步等。
  • 限制:仅适用于非 lazyvar 存储属性,初始化不触发。

二、Swift 中的异常捕获

Swift 通过 Error 协议和 throws 关键字实现异常处理,提供多种方式捕获和处理错误。以下是主要的异常捕获方法:

1. do-catch 语句(详细错误处理)

  • 用法:捕获特定错误类型并处理。
  • 适用场景:需要根据不同错误类型执行不同逻辑。
  • 示例
    enum NetworkError: Error {
        case invalidURL
        case timeout(seconds: Int)
    }
    
    func fetchData() throws {
        throw NetworkError.timeout(seconds: 30)
    }
    
    do {
        try fetchData()
    } catch NetworkError.invalidURL {
        print("URL 无效")
    } catch NetworkError.timeout(let seconds) {
        print("请求超时:\(seconds) 秒")
    } catch {
        print("未知错误:\(error)")
    }
    

2. try?(转换为可选类型)

  • 用法:忽略具体错误,返回 nil
  • 适用场景:不关心错误细节,只需判断是否成功。
  • 示例
    let result = try? someThrowingFunction()
    if let data = result {
        // 成功
    } else {
        // 失败
    }
    

3. try!(强制解包,慎用)

  • 用法:假设函数不会抛出错误,若出错则触发运行时崩溃。
  • 适用场景:确信代码不会失败(如本地静态数据加载)。
  • 示例
    let data = try! loadLocalConfig() // 确定本地文件存在时使用
    

4. 向上传递错误(throws 关键字)

  • 用法:函数声明 throws,错误由调用者处理。
  • 适用场景:将错误传递给上层调用链。
  • 示例
    func processFile() throws {
        let content = try String(contentsOfFile: "path/to/file")
        // 处理内容
    }
    
    // 调用处需处理或继续传递
    do {
        try processFile()
    } catch {
        print("文件处理失败:\(error)")
    }
    

5. rethrows(传递闭包中的错误)

  • 用法:函数本身不产生错误,但可能传递闭包的异常。
  • 适用场景:高阶函数(如 mapfilter)中处理闭包可能抛出的错误。
  • 示例
    func customMap<T>(_ array: [T], _ transform: (T) throws -> T) rethrows -> [T] {
        var result = [T]()
        for item in array {
            try result.append(transform(item))
        }
        return result
    }
    
    let numbers = [1, 2, 3]
    let doubled = try? customMap(numbers) { num in
        if num == 2 { throw NetworkError.invalidURL }
        return num * 2
    }
    

6. 异步错误处理(async/await

  • 用法:在异步函数中使用 throwstry
  • 适用场景:异步操作中的错误处理。
  • 示例
    func downloadData() async throws -> Data {
        let url = URL(string: "https://example.com")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }
    
    Task {
        do {
            let data = try await downloadData()
        } catch {
            print("下载失败:\(error)")
        }
    }
    

总结

方法关键字特点适用场景
详细错误处理do-catch精准捕获错误类型需要区分不同错误逻辑
可选值简化try?忽略错误,返回 nil不关心错误细节
强制解包(高危)try!假设成功,崩溃风险高确定不会失败的场景
向上传递错误throws将错误传递给调用者多层调用链中的错误处理
闭包错误传递rethrows传递闭包中的错误高阶函数中的闭包操作
异步错误处理async throws结合 async/await 处理异步错误网络请求、文件读写等异步操作

最佳实践

  • 优先使用 do-catch 处理可预见的错误。
  • 谨慎使用 try!,确保代码绝对安全。
  • 异步操作中结合 async/await 提升可读性。

三、Swift 中 defer 关键字的详解

1. 基本概念

defer 用于定义一个代码块(延迟执行块),该代码块会在 当前作用域结束前 执行,无论作用域是通过正常返回、抛出错误还是其他控制流(如 returnbreak)结束的。
核心作用:确保清理或收尾逻辑(如资源释放)一定会执行,避免资源泄漏。


2. 使用场景

  • 资源管理:文件操作、网络请求、锁的获取与释放等。
  • 错误处理:在抛出错误前确保清理逻辑执行。
  • 代码可读性:将清理代码紧跟在初始化代码后,提高可维护性。

3. 基本用法

func readFile() {
    let file = openFile()
    defer {
        closeFile(file) // 作用域结束前执行
    }
    // 处理文件...
    if someCondition {
        return // 触发 defer
    }
    // 更多操作...
} // 函数结束,触发 defer

4. 执行规则

  • 逆序执行:同一作用域内的多个 defer定义顺序的逆序 执行(类似栈结构)。
    defer { print("1") }
    defer { print("2") }
    // 输出:2 → 1
    
  • 作用域限制defer 仅在 当前作用域 有效(如函数、循环、条件语句)。
    func example() {
        if condition {
            defer { print("if 块结束") }
            // ...
        } // 此处执行 defer
        // 函数后续代码...
    }
    

5. 常见应用示例

(1) 文件操作
func readFile(path: String) throws -> String {
    let file = try openFile(path)
    defer {
        closeFile(file) // 确保文件关闭
    }
    return try parseFile(file)
}
(2) 加锁与解锁
let lock = NSLock()
func criticalSection() {
    lock.lock()
    defer { lock.unlock() } // 确保锁释放
    // 执行关键代码...
}
(3) 循环中的资源清理
for url in urls {
    let data = try downloadData(from: url)
    defer { 
        cleanupTemporaryFiles() // 每次循环结束清理
    }
    process(data)
}

6. 注意事项

  • 禁止控制流操作defer 块中不能使用 breakreturn 或抛出错误。
  • 避免副作用:修改外部变量可能导致逻辑混乱。
    var value = 0
    func example() {
        defer { value += 1 } // 不推荐
        // ...
    }
    
  • 性能影响:频繁使用 defer 可能影响性能(极少数情况)。

7. 错误处理中的 defer

即使函数抛出错误,defer 仍会执行:

func riskyOperation() throws {
    let resource = allocateResource()
    defer { releaseResource(resource) } // 错误抛出前执行
    try mayThrowError()
}

8. 总结

场景示例作用
文件/网络资源释放defer { file.close() }防止资源泄漏
锁的获取与释放defer { lock.unlock() }避免死锁
错误处理中的清理defer { cleanup() }确保异常时资源释放
临时数据清理defer { removeTempFiles() }提升代码健壮性

最佳实践

  • defer 紧跟在资源获取代码后,提高可读性。
  • 避免在 defer 中执行耗时操作或修改外部状态。
  • 优先用于必须执行的清理逻辑,而非复杂业务代码。

通过合理使用 defer,可以显著提升代码的健壮性和可维护性,确保资源安全和逻辑清晰。


四、Swift 与 Objective-C 中的 Protocol 区别

1. 基本概念

  • Objective-C:Protocol 是一种定义方法列表的方式,用于声明接口,实现类必须遵守这些接口(除非标记为 @optional)。
  • Swift:Protocol 不仅定义接口,还支持属性、方法、关联类型、默认实现等,是面向协议编程(POP)的核心工具。

2. 核心区别

特性Objective-C ProtocolSwift Protocol
可选方法使用 @optional 标记可选方法默认无可选方法,可通过 @objc optional 兼容 OC,或通过协议扩展提供默认实现
默认实现不支持支持通过 协议扩展 提供默认实现
关联类型不支持支持 associatedtype,实现泛型协议
值类型支持仅适用于类(Class)适用于类、结构体(Struct)、枚举(Enum)
协议继承与组合单继承(只能继承一个协议)支持多继承(ProtocolA & ProtocolB
泛型支持支持泛型约束(where 子句)
属性定义只能定义方法可定义属性(需指定 { get }{ get set }
类型检查运行时检查(conformsToProtocol:编译时检查 + 运行时检查(isas?

3. 使用场景与示例

Objective-C Protocol
@protocol DataSource <NSObject>
@required
- (NSInteger)numberOfItems;
@optional
- (NSString *)titleForItemAtIndex:(NSInteger)index;
@end

// 类遵循协议
@interface ViewController : UIViewController <DataSource>
@property(nonatomic,weak)id <DataSource> delegate;
@end
  • 特点:主要用于委托模式(Delegate)、数据源模式(DataSource)。
Swift Protocol
protocol Drawable {
    func draw()
}

// 协议扩展提供默认实现
extension Drawable {
    func draw() { print("默认绘制") }
}

struct Circle: Drawable {} // 结构体遵循协议
class Square: Drawable {}  // 类遵循协议

// 关联类型
protocol Container {
    associatedtype Item
    var items: [Item] { get }
    mutating func add(_ item: Item)
}

// 泛型约束
func process<T: Container>(container: T) where T.Item: Equatable {
    // ...
}
  • 特点:支持面向协议编程,适用于值类型和引用类型,灵活扩展功能。

4. 协议扩展(Swift 独有)

Swift 允许通过扩展为协议添加默认实现,而 Objective-C 无法实现:

protocol Loggable {
    func log()
}

extension Loggable {
    func log() { print("日志记录") }
}

struct User: Loggable {} // 自动获得默认 log() 实现

5. 可选方法的实现方式

  • Objective-C:显式标记 @optional,调用时需检查 respondsToSelector:

    @protocol NetworkDelegate <NSObject>
    @optional
    - (void)didReceiveData:(NSData *)data;
    @end
    
    // 调用前检查
    if ([delegate respondsToSelector:@selector(didReceiveData:)]) {
        [delegate didReceiveData:data];
    }
    
  • Swift:通过协议扩展或 @objc optional(仅限类)实现可选方法。

    @objc protocol NetworkDelegate {
        @objc optional func didReceiveData(_ data: Data)
    }
    
    class Handler: NetworkDelegate {} // 可选实现方法
    
    // 调用时检查
    delegate?.didReceiveData?(data)
    

6. 总结

场景Objective-CSwift
接口定义委托、数据源等简单场景面向协议编程、泛型抽象、功能扩展
类型支持仅类类、结构体、枚举
灵活性基础功能,依赖运行时检查编译时安全,支持默认实现和关联类型
跨平台设计主要用于 iOS/macOS 开发跨平台(iOS/macOS/服务器/开源项目)

选择建议

  • Objective-C:在传统项目或需要与 OC 代码交互时使用,适合简单接口定义。
  • Swift:在新项目或需要高度抽象、复用和类型安全时使用,充分发挥面向协议编程的优势。

五、Swift 与 Objective-C 初始化方法(init)有什么不一样?

1. 核心设计理念

  • Swift:强调 安全性严格性,通过两段式初始化编译时检查确保对象完整初始化。
  • Objective-C灵活性优先,依赖开发者自觉管理初始化过程,缺少编译时强制约束。

2. 主要区别

特性SwiftObjective-C
初始化阶段两段式初始化(属性初始化 → 自定义操作)无明确阶段划分
安全检查强制所有非可选(non-optional)存储属性初始化无强制检查,未初始化的属性可能为 nil 或默认值
可选属性必须显式初始化或声明为 Optional默认允许 nil(引用类型)
初始化器类型支持 指定初始化器(Designated)和 便利初始化器(Convenience)无明确分类,但可通过 NS_DESIGNATED_INITIALIZER 标记指定初始化器
修饰符convenience(便利初始化器)、required(强制子类实现)、override(重写父类初始化器)无类似关键字
可失败初始化器支持 init?init!返回 nil 表示失败
继承与重写规则子类必须重写父类的指定初始化器或自动继承,规则严格子类可自由重写初始化器,无强制要求
值类型支持结构体(struct)、枚举(enum)可定义初始化器仅类(class)支持初始化器

3. 关键机制详解

(1) 两段式初始化(Swift 独有)
  • 阶段 1:确保所有存储属性被初始化。
  • 阶段 2:在属性初始化完成后,进一步自定义实例(如调用方法、访问 self)。
  • 优势:避免属性未初始化就被使用,提升安全性。
  • 示例
    class Person {
        var name: String
        var age: Int
        
        // 指定初始化器
        init(name: String, age: Int) {
            self.name = name // 阶段1:初始化属性
            self.age = age
            // 阶段2:可调用方法
            setup()
        }
        
        convenience init() {
            self.init(name: "Unknown", age: 0) // 必须调用指定初始化器
        }
        
        private func setup() { /*...*/ }
    }
    
(2) 强制属性初始化(Swift)
  • 所有非可选存储属性必须在初始化完成前赋值,否则编译器报错。
  • 示例
    class User {
        var id: Int    // 错误:未初始化
        var name: String?
        
        init(id: Int) {
            self.id = 1234 // 正确:非可选属性必须赋值
        }
    }
    
(3) 初始化器修饰符(Swift)
  • convenience:定义便利初始化器,必须调用同类中的指定初始化器。
    class Rectangle {
        var width: Double
        var height: Double
        
        init(width: Double, height: Double) {
            self.width = width
            self.height = height
        }
        
        convenience init(side: Double) {
            self.init(width: side, height: side) // 调用指定初始化器
        }
    }
    
  • required:强制子类实现该初始化器。
    class Vehicle {
        required init() { /*...*/ }
    }
    
    class Car: Vehicle {
        required init() { // 必须实现
            super.init()
        }
    }
    
(4) 可失败初始化器
  • Swift:通过 init? 返回可选实例,init! 返回隐式解包实例。
    struct Temperature {
        let celsius: Double
        init?(celsius: Double) {
            guard celsius >= -273.15 else { return nil }
            self.celsius = celsius
        }
    }
    
  • Objective-C:返回 nil 表示失败。
    @interface MyClass : NSObject
    - (instancetype)initWithValue:(NSInteger)value;
    @end
    
    @implementation MyClass
    - (instancetype)initWithValue:(NSInteger)value {
        if (value < 0) return nil;
        self = [super init];
        return self;
    }
    @end
    

4. 初始化器继承规则

  • Swift
    • 子类默认不继承父类初始化器。
    • 若子类未定义任何指定初始化器,则自动继承父类所有指定初始化器。
    • 若子类实现了父类所有指定初始化器,则自动继承父类便利初始化器。
  • Objective-C:子类自动继承父类所有初始化器,除非显式重写。

5. 总结

场景SwiftObjective-C
安全性高(编译时强制检查)低(依赖开发者自觉)
灵活性较低(严格规则限制)高(自由定义初始化逻辑)
代码复杂度高(需遵循两段式、修饰符规则)低(简单直接)
适用类型类、结构体、枚举仅类
错误处理可失败初始化器、异常抛出返回 nilNSError

选择建议

  • Swift:在需要高安全性、复杂类型设计的场景下使用,遵循严格初始化规则。
  • Objective-C:在维护旧项目或需要快速灵活初始化时使用,但需注意潜在风险。