Swift 基础知识(一)

874 阅读20分钟

一、Swift 中 类(class) 和 结构体(struct) 的区别,以及各自优缺点?

核心区别

特性类(Class)结构体(Struct)
类型引用类型(传递时共享内存)值类型(传递时复制新实例)
存储位置堆内存栈内存(若包含引用类型属性,其数据仍在堆内存)
继承支持(除非用 final 修饰)不支持
类型转换支持运行时类型检查(is/as仅编译时类型检查
内存管理引用计数(ARC)自动栈内存释放(无需引用计数)
方法派发动态派发(运行时确定方法地址)静态派发(编译时确定方法地址)
析构函数支持 deinit不支持
线程安全需手动处理(共享内存)天然线程安全(值隔离)

赋值行为的本质

  • 类(浅拷贝)
    赋值时复制指针,新旧变量指向同一内存地址。修改任一变量会影响所有引用。

    class Person {
        var name: String
        init(name: String) { self.name = name }
    }
    let p1 = Person(name: "Alice")
    let p2 = p1
    p2.name = "Bob"
    print(p1.name) // 输出 "Bob"
    
  • 结构体(深拷贝)
    赋值时复制值,新旧变量独立存储。修改一个不会影响另一个。

    struct Point {
        var x: Int
        var y: Int
    }
    var p1 = Point(x: 1, y: 2)
    var p2 = p1
    p2.x = 3
    print(p1.x) // 输出 1
    

优缺点分析

类(Class)的优点
  1. 共享与可变状态:适合需要多个对象共享同一数据的场景(如网络请求管理器)。
  2. 继承与多态:支持面向对象设计,复用代码逻辑。
  3. 运行时动态性:支持类型检查、方法重写和 KVO。
类(Class)的缺点
  1. 内存开销:堆内存分配和引用计数管理增加性能损耗。
  2. 线程安全隐患:共享内存需额外同步机制(如锁、GCD)。
  3. 复杂性:需处理循环引用(weak/unowned)。
结构体(Struct)的优点
  1. 高性能:栈内存分配速度快,无引用计数开销。
  2. 线程安全:值隔离特性天然避免共享内存冲突。
  3. 清晰所有权:深拷贝行为减少意外副作用。
结构体(Struct)的缺点
  1. 无法继承:不支持继承,复用代码需依赖协议和组合。
  2. 不适合复杂对象:频繁深拷贝大对象可能导致性能问题。

使用场景建议

场景推荐类型理由
轻量级数据模型(如坐标、颜色)结构体值类型更安全高效,无共享风险
需要继承或共享状态的组件利用面向对象特性实现复用和动态行为
高频创建的临时对象结构体栈内存快速分配释放,减少堆内存压力
需线程安全的数据容器结构体值隔离避免竞态条件

补充说明

  1. 结构体中包含引用类型
    若结构体的属性是类实例,深拷贝时仅复制引用(指针),数据仍共享:

    struct Container {
        var ref: NSMutableArray // 引用类型属性
    }
    var c1 = Container(ref: NSMutableArray())
    var c2 = c1
    c2.ref.add("Test") // c1.ref 也会被修改
    
  2. 协议与扩展
    结构体和类均可遵循协议并使用扩展,但结构体无法通过继承实现多态。

  3. Copy-on-Write 优化
    Swift 标准库中的集合类型(如 ArrayDictionary)对结构体实现了写时复制,避免不必要的深拷贝。


官方建议

Apple 在 Swift 官方文档 中明确推荐:

优先使用结构体,除非你需要类独有的特性(如继承、析构函数或引用语义)。

二、 Swift 中什么是可选类型?

1. 什么是可选类型?

可选类型(Optional)是 Swift 中一种特殊的数据类型,用于表示一个变量可能有值(如 IntString 等),也可能没有值(nil)。它的核心设计目的是强制开发者显式处理值缺失的情况,从而避免空指针异常(Null Pointer Exception),提升代码的安全性。


2. 可选类型的语法

  • 声明可选类型:在类型后添加 ?

    var name: String?  // 可能为 String 或 nil
    var age: Int?      // 可能为 Int 或 nil
    
  • 隐式解包可选类型:在类型后添加 !

    var forcedValue: String!  // 声明时允许为 nil,但使用时假设已赋值
    

3. 可选类型的本质

可选类型是一个泛型枚举,定义如下:

public enum Optional<Wrapped> {
    case none       // 无值(nil)
    case some(Wrapped) // 有值(Wrapped 类型)
}

例如,String? 实际上是 Optional<String> 的简写。


4. 可选类型的解包方式

(1) 强制解包(Force Unwrap)

使用 ! 强制解包,但如果值为 nil 会触发运行时错误。

let name: String? = "Alice"
print(name!) // 输出 "Alice"

let age: Int? = nil
print(age!) // 运行时崩溃!
(2) 可选绑定(Optional Binding)

通过 if let 或 guard let 安全解包:

if let unwrappedName = name {
    print("Name is (unwrappedName)")
} else {
    print("Name is nil")
}
(3) 空合并运算符(Nil-Coalescing Operator)

提供默认值:

let name = optionalName ?? "Unknown"
(4) 可选链式调用(Optional Chaining)

安全访问属性或方法:

let length = user?.name?.count // 若任一环节为 nil,返回 nil

5. 隐式解包可选类型(Implicitly Unwrapped Optional)

  • 用途:用于变量初始化后一定会被赋值,但声明时可能为 nil 的场景(如 IBOutlet)。
  • 风险:若访问时仍为 nil,会触发运行时错误。
@IBOutlet weak var label: UILabel! // 隐式解包类型

6. 可选类型的优势

  • 安全性:编译器强制处理 nil 情况,减少崩溃风险。
  • 清晰性:明确标记可能缺失的值,提升代码可读性。
  • 灵活性:与 Swift 的强类型系统结合,支持泛型和模式匹配。

7. 使用场景

  • 网络请求结果:可能成功(有数据)或失败(nil)。

  • 用户输入处理:如文本框内容可能为空。

  • 类型转换:尝试将 Any 转换为具体类型时可能失败。

    let value: Any = "123"
    let number = value as? Int // Int?
    

8. 注意事项

  • 避免滥用 ! :强制解包应仅在确定值非 nil 时使用。
  • 优先使用可选绑定if let 和 guard let 更安全。
  • 合理使用隐式解包:仅用于生命周期明确的变量(如 UI 控件)。

9. 示例代码

// 定义一个可能为 nil 的可选类型
var optionalNumber: Int? = 42

// 安全解包
if let number = optionalNumber {
    print("The number is (number)")
} else {
    print("The number is nil")
}

// 空合并运算符
let validNumber = optionalNumber ?? 0

// 可选链式调用
struct Person {
    var address: Address?
}

struct Address {
    var street: String?
}

let person: Person? = Person(address: Address(street: "Main St"))
let street = person?.address?.street // 类型为 String?

10. 总结

可选类型是 Swift 语言的核心特性之一,通过编译时的严格检查,显著提升了代码的健壮性。合理使用可选类型及其解包方式,能有效避免空指针异常,同时使代码逻辑更加清晰可靠。

三、Swift 中什么 是 泛型?

泛型是 Swift 语言中的核心特性之一,允许开发者编写灵活、可重用且类型安全的代码。通过泛型,可以定义适用于多种数据类型的函数、类、结构体或枚举,而无需重复编写相同逻辑的代码。


1. 泛型的基本概念

泛型通过类型参数化实现,类型参数在定义时作为占位符(如 <T>),在使用时由具体类型替换。例如:

// 泛型函数:交换任意类型的两个值
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

var x = 5, y = 10
swapTwoValues(&x, &y) // T 被推断为 Int

var str1 = "Hello", str2 = "World"
swapTwoValues(&str1, &str2) // T 被推断为 String

2. 泛型的核心作用

  • 代码复用:同一套逻辑可处理不同类型的数据。
  • 类型安全:编译器在编译时检查类型,避免运行时错误。
  • 性能优化:泛型在编译时生成具体类型的代码,无运行时开销。

3. 泛型的使用场景

(1) 泛型函数

适用于需要处理多种类型的算法(如排序、交换):

func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
    for (index, item) in array.enumerated() {
        if item == value { return index }
    }
    return nil
}

let names = ["Alice", "Bob"]
let index = findIndex(of: "Bob", in: names) // 返回 1
(2) 泛型类型

定义可存储任意类型数据的容器(如自定义集合):

struct Stack<Element> {
    private var elements = [Element]()
    mutating func push(_ element: Element) {
        elements.append(element)
    }
    mutating func pop() -> Element? {
        elements.popLast()
    }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)

var stringStack = Stack<String>()
stringStack.push("Swift")
(3) 泛型协议

通过 associatedtype 定义协议中的关联类型,允许遵循协议的类型自定义具体类型:

protocol Container {
    associatedtype Item
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}

struct IntList: Container {
    typealias Item = Int
    private var items = [Int]()
    var count: Int { items.count }
    mutating func append(_ item: Int) {
        items.append(item)
    }
    subscript(i: Int) -> Int { items[i] }
}

4. 泛型约束

限制泛型类型必须满足特定条件(如遵循协议、继承类或实现特定方法):

// 约束 T 必须遵循 Comparable 协议
func maxValue<T: Comparable>(_ a: T, _ b: T) -> T {
    return a > b ? a : b
}

// 约束 Key 必须遵循 Hashable,Value 可以是任意类型
struct Dictionary<Key: Hashable, Value> {
    private var storage = [Key: Value]()
    // ...
}

5. 关联类型(Associated Types)

在协议中使用关联类型,定义泛型需求:

protocol NetworkService {
    associatedtype Response: Decodable
    func fetchData(completion: @escaping (Result<Response, Error>) -> Void)
}

struct UserService: NetworkService {
    typealias Response = User // 指定具体类型
    func fetchData(completion: @escaping (Result<User, Error>) -> Void) {
        // 网络请求逻辑
    }
}

6. 类型擦除(Type Erasure)

处理需要隐藏具体泛型类型的场景(如返回泛型协议的实例):

struct AnyPrinter<T>: Printer {
    private let _print: (T) -> Void
    init<U: Printer>(_ printer: U) where U.T == T {
        _print = printer.print
    }
    func print(_ value: T) {
        _print(value)
    }
}

7. 泛型与标准库

Swift 标准库广泛使用泛型,例如:

  • 集合类型Array<Element>Dictionary<Key, Value>
  • 可选类型Optional<Wrapped>
  • 错误处理Result<Success, Failure>

8. 泛型的优势

  • 减少重复代码:无需为不同类型编写相同逻辑。
  • 增强类型安全:编译器在编译时检查类型错误。
  • 提升性能:无运行时类型转换开销。

9. 总结

场景示例核心作用
函数逻辑复用swapTwoValues<T>处理多种类型数据
自定义容器Stack<Element>存储任意类型元素
协议抽象Container 协议 + 关联类型定义灵活的类型需求
类型约束T: Equatable确保类型满足特定条件

通过合理使用泛型,开发者可以显著提升代码的灵活性和健壮性,同时减少冗余代码。


四、 Swift 中的 strongweakunowned 详解

1、基本概念

在 Swift 中,strongweakunowned 是用于管理对象引用计数的关键字,与 自动引用计数(ARC) 机制密切相关。它们的核心作用是 控制对象生命周期,避免内存泄漏或野指针问题。

关键字行为引用计数是否可为 nil安全性
strong默认修饰符,持有对象时增加引用计数,对象不会被释放。增加否(非可选类型)安全
weak不增加引用计数,对象释放后自动置为 nil不增加是(可选类型)安全(自动置空)
unowned不增加引用计数,对象释放后仍保留悬垂指针,访问时可能导致崩溃。不增加否(非可选类型)不安全(需谨慎)

2、核心区别

(1). strong
  • 特点:默认修饰符,持有对象时会增加引用计数,对象生命周期由引用计数控制。
  • 适用场景:大多数情况下使用,表示明确的“拥有关系”。
  • 示例
    class Person {
        var pet: Dog? // 默认是 strong
    }
    
(2). weak
  • 特点
    • 不增加引用计数,对象释放后自动置为 nil
    • 必须声明为可选类型(var + ?)。
    • 它在对象释放后弱引用也随即消失。继续访问该对象,程序会得到 nil,不会出现崩溃。
  • 适用场景:打破循环引用,尤其用于 非父子关系 的相互引用(如 delegate)。
  • 示例
    class ViewController: UIViewController {
        weak var delegate: DataDelegate? // 弱引用避免循环
    }
    
(3). unowned
  • 特点
    • 不增加引用计数,但假设对象在生命周期内始终有效,不会为 nil
    • 非可选类型,访问已释放的对象会导致崩溃。
  • 适用场景:两个对象生命周期 严格同步,且被引用对象不会先于引用者释放。
  • 示例
    class CreditCard {
        unowned let owner: Customer // Customer 一定比 CreditCard 生命周期长
        init(owner: Customer) {
            self.owner = owner
        }
    }
    

何时使用 unowned

1. 适用场景
  • 对象生命周期严格绑定
    被引用对象(如 owner)的生命周期 长于或等于 引用者(如 CreditCard)。
  • 避免循环引用且无法使用 weak
    当需要非可选类型且确保对象不会提前释放时。
2. 使用示例
class Customer {
    var card: CreditCard?
}

class CreditCard {
    unowned let owner: Customer
    init(owner: Customer) {
        self.owner = owner
    }
}

// 使用
let customer = Customer()
customer.card = CreditCard(owner: customer) // 无循环引用
3. 注意事项
  • 绝对不要滥用:必须确保被引用对象不会被提前释放。
  • 优先选择 weak:若无法确保生命周期同步,优先使用 weak
  • unowned 与弱引用本质上一样。不同的是,unowned 无主引用 实例销毁后仍然存储着实例的内存地址(类似于OC中的unsafe_unretained), 试图在实例销毁后访问无主引用,会产生运行时错误(野指针)
  • weak unowned 只能用在 类实例上面
  • weakunowned 都能解决 循环引用,unowned 要比 weak 性能 稍高
    • 在生命周期中可能会 变成 nil 的使用 weak
    • 初始化赋值以后再也不会变成 nil 使用 unowned

对比 weakunowned

特性weakunowned
是否可选必须声明为可选类型(?非可选类型
安全性自动置 nil,访问安全可能触发野指针崩溃
生命周期假设允许被引用对象提前释放假设被引用对象始终存在
内存管理无需手动置 nil需确保引用对象生命周期

常见问题

1. 循环引用是如何产生的?

当两个对象通过 strong 相互引用时,引用计数无法归零,导致内存泄漏:

class A {
    var b: B?
}

class B {
    var a: A?
}

let a = A()
let b = B()
a.b = b // A 强引用 B
b.a = a // B 强引用 A → 循环引用!
2. 如何打破循环引用?
  • 将其中一个引用改为 weakunowned
    class B {
        weak var a: A? // 弱引用打破循环
    }
    
3. 闭包中的循环引用

闭包捕获外部变量时默认是 strong,需使用捕获列表:

class NetworkManager {
    var completion: (() -> Void)?
    
    func fetchData() {
        // 使用 [weak self] 避免循环引用
        someAsyncTask { [weak self] in
            self?.handleData()
        }
    }
}

总结

场景推荐修饰符理由
默认对象引用strong明确所有权关系
打破父子对象外的循环引用weak安全自动置空
严格同步生命周期的对象引用unowned避免可选类型解包,需确保对象存活

通过合理使用 strongweakunowned,可以避免内存泄漏和野指针问题,写出更健壮的 Swift 代码。


五、Swift 中 staticclass 关键字对比

核心区别

特性staticclass
适用类型classstructenumclass
修饰存储属性✅ 允许❌ 禁止
修饰计算属性/方法✅ 允许✅ 允许
是否支持子类重写❌ 不可重写(隐含 final✅ 可重写(需用 override
协议中的使用协议中统一用 static,所有类型实现协议中不可用,仅类实现时可选 class

使用场景

  1. static

    • 通用场景:适用于所有类型(classstructenum)。
    • 存储属性:定义类级别的常量或变量。
    • 禁止重写:明确不希望子类重写的类型方法或属性。
    • 协议定义:协议中声明类型方法或属性时,必须用 static
  2. class

    • 类专用:仅用于 class 类型。
    • 允许重写:希望子类重写父类的类型方法或计算属性。
    • 计算属性:定义可被子类重写的类计算属性。

示例代码

// 协议定义
protocol MyProtocol {
    static func protocolMethod()  // 协议中必须用 static
}

// 类实现
class Parent: MyProtocol {
    static var storageProperty = "Parent"  // 类存储属性
    class var computedProperty: String { "Parent" }  // 可重写的类计算属性
    class func classMethod() { }          // 可重写的类方法
    static func protocolMethod() { }      // 实现协议方法(隐含 final)
}

class Child: Parent {
    override class var computedProperty: String { "Child" }
    override class func classMethod() { }  // ✅ 允许重写
    // override static func protocolMethod() { }  // ❌ 禁止重写(static隐含final)
}

// 结构体实现
struct MyStruct: MyProtocol {
    static func protocolMethod() { }      // 结构体必须用 static
}

注意事项

  • 存储属性class 不能修饰存储属性(仅 static 可以)。
  • 协议兼容性:协议中声明类型方法/属性时,使用 static;类实现时可用 classstaticstatic 会隐含 final)。
  • 性能优化static 方法/属性在编译期绑定,效率略高于 class

六、Swift 访问控制总结

Swift 提供了 5 种访问控制级别,用于限制代码中实体(类、属性、方法等)的可见性,确保代码的封装性和安全性。以下是各访问级别的核心区别及使用场景:


1. 访问级别从高到低

关键字作用范围允许继承/重写适用场景
open跨模块可见,且允许其他模块继承或重写框架或库的公开 API(如 UIKit)
public跨模块可见,但禁止其他模块继承或重写(类和成员默认不可继承)暴露无需继承的工具类、方法
internal仅当前模块内可见(默认级别)-模块内部实现细节
fileprivate仅当前文件内可见-文件内共享的辅助函数或类型
private仅当前作用域(类型或扩展)内可见-类型内部的私有实现细节

2. 核心规则

  • 默认访问级别:未显式指定时,默认为 internal
  • 成员访问限制:成员的访问级别不能高于其所属类型(如 public classprivate 属性合法,但 private classpublic 属性非法)。
  • 协议一致性:遵循协议的类型必须满足协议要求的访问级别(如协议声明为 public,则遵循类型也需 public)。
  • 扩展中的访问控制:扩展默认继承原始类型的访问级别,但可显式指定(如 private extension)。

3. 使用示例

框架开发(跨模块)
// Framework 模块
open class NetworkManager {        // 允许其他模块继承
    public static let shared = NetworkManager()
    internal func log() { }        // 仅模块内部可见
    private var token: String?     // 仅当前类可见
}

public struct APIError: Error {    // 其他模块可用,不可继承
    public let code: Int
}
模块内部
// App 模块
internal struct User {             // 默认 internal,仅模块内可见
    var name: String
    fileprivate var id: Int        // 同一文件内可见
}

private extension String {         // 扩展内所有成员默认 private
    func trimmed() -> String {
        self.trimmingCharacters(in: .whitespaces)
    }
}

4. 特殊场景

  • 单元测试:通过 @testable import ModuleName 可访问模块的 internal 实体。
  • 子类重写:子类方法的访问级别不能低于父类方法(如父类方法为 open,子类可重写为 public,但不能是 internal)。
  • 泛型约束:泛型类型的访问级别需与其类型参数一致或更高。

5. 选择指南

场景推荐级别示例
开发第三方框架或库open/public公开工具类、核心功能方法
模块内部工具类internal数据模型、网络请求封装
文件内共享的辅助函数fileprivate同一文件内的 JSON 解析工具
类型内部私有实现private缓存属性、敏感数据处理方法

通过合理使用访问控制,可以提升代码的可维护性,减少耦合,同时保护关键实现细节不被外部误用。

七、Swift 写时复制(Copy-on-Write)核心总结

1. 核心机制

  • 延迟复制:值类型(如 ArrayString)在赋值时不立即复制内存,而是共享同一份数据。
  • 写入触发:仅当修改副本时,才真正创建新内存,确保原数据不受影响。

2. 实现原理

  • 内部引用:值类型内部通过引用类型(如类)存储实际数据。
  • 引用计数检查:写入前调用 isKnownUniquelyReferenced,检测数据引用是否唯一:
    • 唯一引用:直接修改原数据。
    • 多引用:创建新副本后再修改,避免共享数据意外变更。

3. 优势

  • 性能优化:减少不必要的数据复制,提升内存效率。
  • 值类型安全:保留值语义(独立修改),同时兼顾性能。

4. 适用场景

  • 高频赋值的大数据:如集合操作、字符串处理。
  • 标准库类型ArrayDictionarySetString 默认支持 COW。

5. 示例

var a = [1, 2, 3]  // 内部引用计数为 1
var b = a           // 引用计数 +1(共享数据)
b.append(4)         // 检测到多引用,复制新内存再修改(a 仍为 [1,2,3])

总结

COW 通过共享数据 + 写入时复制,平衡了值类型的安全性与性能,是 Swift 高效处理大数据的核心机制。

八、Swift 将集合类型设计为值类型的原因

1. 值类型的核心优势

  • 线程安全:值类型在传递时通过复制确保数据独立性,避免多线程同时修改同一内存。
  • 无副作用:方法调用不会意外修改原始数据,代码行为更易推理。
  • 内存高效:结合 写时复制(Copy-on-Write, COW),减少不必要的内存分配。

2. 具体设计动机

(1) 安全性优先
  • 避免隐式共享:引用类型多个变量指向同一内存,修改一处会影响所有引用(如 NSMutableArray)。
  • 值类型赋值即复制:每个变量持有独立数据,修改副本不影响原数据。
    var a = [1, 2, 3]
    var b = a
    b.append(4)
    print(a) // [1, 2, 3](a 未被修改)
    
(2) 性能优化
  • 栈内存分配:值类型默认在栈上分配,操作高效(指针移动即可)。
  • 写时复制(COW):延迟实际内存复制,仅在修改时创建新副本。
    var largeArray = [Int](repeating: 0, count: 1000000)
    var copy = largeArray // 不立即复制内存(共享存储)
    copy[0] = 1          // 修改时触发复制(独立存储)
    
(3) 不可变性的真正保证
  • let 语义严格:值类型声明为 let 后,内容完全不可变。
  • 引用类型的缺陷:即使使用 let 修饰引用类型(如 NSArray),内部数据仍可能被修改。
    let nsArray: NSArray = NSMutableArray()
    let swiftArray = [1, 2, 3]
    // nsArray 可被其他引用修改(不安全),swiftArray 完全不可变
    

3. 与引用类型的对比

特性值类型(Array/String/Dictionary)引用类型(NSArray/NSString)
内存分配栈 + COW 优化堆内存堆内存
线程安全天然安全(独立内存)需手动同步(如锁、GCD)
赋值行为深拷贝(COW 延迟实际复制)浅拷贝(共享内存)
不可变性let 完全不可变let 仅保证引用不变,内容可能被其他引用修改

4. 写时复制(COW)的实现原理

  1. 内部引用类型存储:值类型内部使用类(如 _ContiguousArrayStorage)存储实际数据。
  2. 引用计数检测:通过 isKnownUniquelyReferenced 检查数据是否被多引用。
  3. 按需复制:修改数据时,若引用非唯一,则创建新副本再修改。
    struct MyArray<T> {
        private var storage: Storage<T> // 内部引用类型
        mutating func append(_ element: T) {
            if !isKnownUniquelyReferenced(&storage) {
                storage = storage.copy()
            }
            storage.append(element)
        }
    }
    

5. 总结

  • 安全第一:值类型 + COW 解决了引用类型在多线程和共享数据中的安全隐患。
  • 性能平衡:栈内存和 COW 机制将复制开销降至最低。
  • 开发友好:严格的不可变性和可预测行为,减少代码副作用。

通过将 ArrayStringDictionary 设计为值类型,Swift 在内存效率、线程安全和代码可靠性之间取得了最佳平衡,同时借助 COW 技术避免了传统值类型的性能缺陷。