标签

Swift

Swift是由苹果公司开发的一种开源的编程语言,用于iOS、iPadOS、watchOS、tvOS和macOS等平台的应用程序开发。Swift结合了Objective-C的灵活性和C的高性能,同时还引入了许多新的特性,如安全性、现代化的语法、内存管理等。Swift支持面向对象编程、泛型编程和函数式编程等多种编程范式,可以用于编写复杂和高性能的应用程序。Swift还具有易读易写的语法和丰富的标准库,可以大大提高开发效率和代码质量。由于Swift的易用性和高性能,它已经成为一种备受欢迎的编程语言,并且被许多企业和开发者使用。

Swift
查看更多相关内容
服务端5月27日 12:49
Swift 逃逸闭包和非逃逸闭包有什么区别?## 闭包的本质 闭包是自包含的函数代码块,能捕获和存储所在上下文中的常量和变量。Swift 里的闭包就是匿名函数,和 OC 的 Block、JS 的箭头函数本质相同。 ```swift // 最简闭包 let add: (Int, Int) -> Int = { a, b in a + b } add(1, 2) // 3 // 闭包捕获外部变量 var counter = 0 let increment = { counter += 1 // 捕获了 counter 的引用,不是值拷贝 } increment() print(counter) // 1 ``` 闭包是引用类型——赋值给新变量不会拷贝,而是共享同一个闭包实例。这一点和 class 一样,和 struct 不同。 ## 逃逸闭包 vs 非逃逸闭包 这是面试最爱问的区分点。核心区别就一个:**闭包的执行时机在函数返回之前还是之后**。 ### 非逃逸闭包(默认) 闭包在函数体内就被调用了,函数返回时闭包已经执行完毕,生命周期不会超出函数作用域。Swift 3 之后闭包参数默认就是非逃逸的,不用加任何标注。 ```swift func doWork(closure: () -> Void) { closure() // 函数返回前就执行了 // 函数结束,closure 被释放 } ``` ### 逃逸闭包(@escaping) 闭包被存储到函数外部(属性、数组、异步回调),在函数返回之后才被调用。必须显式标注 `@escaping`,否则编译报错。 ```swift var completions: [() -> Void] = [] func doAsyncWork(closure: @escaping () -> Void) { completions.append(closure) // 存到外部,函数返回后才执行 // 函数结束了,但 closure 还活着 } ``` 最常见的场景是异步网络请求回调——函数发起请求后立刻返回,回调在响应回来后才执行,这就是逃逸。 ## 面试必考的三个区别 | 维度 | 非逃逸 | 逃逸 (@escaping) | |------|--------|------------------| | 执行时机 | 函数返回前 | 函数返回后 | | self 引用 | 可以隐式引用 | 必须显式写 `self` | | 循环引用风险 | 无(函数结束就释放) | 有(闭包持有 self,self 持有闭包) | self 引用的区别是编译器强制的: ```swift class ViewModel { var data: String = "" func load() { // 非逃逸:隐式引用 self,不需要写 self doWork { data = "updated" } // 逃逸:必须显式写 self,提醒你注意循环引用 doAsyncWork { self.data = "updated" } } } ``` 逃逸闭包必须写 `self` 是 Swift 的安全设计——强制你意识到这里可能产生循环引用,该用 `[weak self]` 就得用。 ## 逃逸闭包的循环引用 ```swift class NetworkManager { var result: String? func fetch() { API.request { [weak self] response in // 必须用 weak self self?.result = response.data } } } ``` 不用 `[weak self]` 的话:NetworkManager 持有闭包(作为 API 回调),闭包捕获了 self(强引用 NetworkManager),谁也释放不了。 非逃逸闭包不存在这个问题,因为函数执行完闭包就释放了,捕获的引用也会跟着释放。 ## 性能差异 非逃逸闭包比逃逸闭包快一点点——编译器可以省去一些 retain/release 调用,闭包上下文可以分配在栈上而不是堆上。但这个差异在绝大多数场景下可以忽略,不用为了性能特意选非逃逸。 真正重要的是语义:能用非逃逸就用非逃逸,它给编译器和读者都传达了更明确的信息——这个闭包不会跑到函数外面去。 ## 捕获列表 闭包默认以引用方式捕获变量。如果需要值拷贝,用捕获列表: ```swift var value = 10 let closure = { [value] in // 拷贝当前值 print(value) // 10,不会随外部 value 变化 } value = 20 closure() // 仍然打印 10 ``` 捕获列表的语法:`[弱引用/强引用/值拷贝]`,可以混用: ```swift let closure = { [weak self, unowned delegate = self.delegate, copy = self.data] in self?.doSomething(copy) delegate?.notify() } ``` ## 追问 ### 可选闭包是逃逸的吗? 是的。`(() -> Void)?` 即使没标 `@escaping` 也是逃逸的,因为可选值本质是枚举,闭包被包了一层,生命周期超出了函数范围。 ```swift // 编译通过,可选闭包天然逃逸 func doWork(closure: (() -> Void)?) { DispatchQueue.main.async { closure?() } } ``` ### 什么时候必须用 @escaping? 三种典型场景: 1. 异步回调(网络请求、延迟执行) 2. 存储闭包到属性或集合中 3. 闭包作为可选参数 ### autoreleasepool 在闭包里需要用吗? 逃逸闭包如果捕获了大量临时对象,可以在闭包内部用 `autoreleasepool` 包裹关键代码段,及时释放不需要的对象,降低内存峰值。
服务端5月27日 11:52
Swift 可选类型怎么用?if let、guard let 和 ?? 有什么区别?可选类型表示"值可能为 nil"。`String?` 要么是 String 要么是 nil。本质是枚举 `Optional<Wrapped>`,有 .some(Wrapped) 和 .none 两个 case。Swift 不允许变量为 nil 除非声明为可选类型——编译器强制你处理值缺失的情况。 解包方式:if let(安全解包,作用域内可用)、guard let(安全解包,后续可用)、??(提供默认值)、!(强制解包,nil 崩溃)、?.(可选链,nil 时短路)。 ## 追问 ### if let 和 guard let 怎么选? if let 适合"有值就处理,没有就跳过"——解包后的变量只在 if 块内可用。guard let 适合"没有值就提前退出"——解包后的变量在 guard 之后整个作用域可用。函数参数验证用 guard let,条件分支用 if let。 ### ?? 运算符和 if let 有什么区别? `??` 适合"没有值就用默认值"——`name ?? "unknown"`,简洁一行。if let 适合"没有值要做复杂处理"——打日志、return、throw。?? 链式使用很方便:`a ?? b ?? c ?? "default"`,依次尝试非 nil 值。 ### 隐式解包可选类型 String! 什么时候用? 几乎不用。`String!` 声明后当普通 String 用,但底层仍是 Optional,nil 时崩溃。唯一合理场景:IBOutlet( storyboard 初始化时赋值,之后不会为 nil)和 Objective-C 互操作。新代码用 String? + 显式解包,不要用 String!。 ### 可选链 ?. 和可选绑定哪个好? 可选链适合"只需要访问一层,nil 就整个返回 nil"——`user?.address?.city` 返回 String?,简洁。可选绑定适合"需要拿到值做进一步操作"——`if let city = user?.address?.city`。可选链不改变可选性,结果始终是 Optional。 ### 多个可选值怎么一起解包? 逗号分隔:`guard let a = a, let b = b, let c = c else { return }`——所有值都非 nil 才继续。如果需要组合解包,用 `guard let (a, b) = optionalTuple` 或者逐个解包。Swift 没有 `let (a?, b?) = (optA, optB)` 这种语法。 ## 写段代码 ```swift // if let if let name = user?.name { print(name) // 只在 if 块内可用 } // guard let func process(user: User?) { guard let user = user else { return } print(user.name) // 后续都可用 } // ?? 默认值 let name = user?.name ?? "unknown" // 可选链 let city = user?.address?.city // String? // 多个一起解包 guard let name = name, let age = age, age >= 18 else { return } // map/flatMap 处理可选值 let length = name?.count // Int? let uppercased = name?.uppercased() // String? ```
服务端5月27日 11:52
Swift lazy 属性是什么?初始化时机和线程安全怎么处理?lazy 延迟初始化——属性第一次被访问时才计算初始值,之后缓存结果。声明方式:`lazy var importer = DataImporter()` 或 `lazy var config: Config = { loadConfig() }()`。必须是 var(let 在 init 时就必须有值)。 lazy 的典型场景:初始化成本高的对象(数据库连接、大图加载)、依赖其他属性后才能初始化的值、不是每次都会用到的属性(省内存)。 ## 追问 ### lazy 属性线程安全吗? 不安全。如果两个线程同时首次访问同一个 lazy 属性,它可能被初始化两次,或者一个线程拿到未完全初始化的值。解决方案:用 `lazy var` + 串行队列保护,或者改用 actor 隔离。如果线程安全是刚需,别用 lazy,用手动初始化 + 锁。 ### lazy 和计算属性有什么区别? 计算属性每次访问都重新计算,lazy 只计算一次然后缓存。如果计算结果不会变且计算成本高,用 lazy;如果结果依赖可能变化的值,用计算属性。lazy 占用存储空间(缓存结果),计算属性不占。 ### lazy 闭包里能引用 self 吗? 能。lazy 闭包在实例初始化完成后才执行,此时 self 已经可用,所以不需要 `[weak self]`。但这也意味着 lazy 闭包会强引用 self——如果 lazy 属性在闭包里引用了 self 的属性/方法,会形成循环引用(self 持有 lazy 属性,lazy 闭包持有 self)。用 `[weak self]` 可以打破,但要注意解包。 ### lazy 能和 didSet 一起用吗? 不能。lazy 属性不能有属性观察器——因为 lazy 的初始化时机不确定,观察器的触发时机也变得模糊。如果需要在 lazy 初始化后执行副作用,在 lazy 闭包里手动调用。 ## 写段代码 ```swift class DataManager { lazy var importer = DataImporter() // 第一次访问才创建 lazy var config: Config = { print("Loading config...") return loadConfig() }() var data: [String] = [] } let manager = DataManager() manager.data.append("item") // importer 还没创建 print(manager.importer) // 此刻才创建 DataImporter // lazy 闭包引用 self(注意循环引用) class ViewController { lazy var label: UILabel = { let lbl = UILabel() lbl.text = self.title // 强引用 self return lbl }() var title: String = "" } ```
服务端5月27日 11:52
Swift inout 参数是什么?有什么限制和使用场景?inout 让函数直接修改传入的变量。默认情况下函数参数是常量(let),不能修改。加 inout 后,函数可以修改参数值,修改会写回原变量。调用时在变量前加 `&`:`swap(&a, &b)`。 inout 不是真正的引用传递——它的工作方式是"拷入-修改-拷出":函数调用时拷贝值进参数,函数内部修改,返回时拷贝回原变量。效果等价于引用传递,但实现不同。 ## 追问 ### inout 和引用传递有什么区别? Swift 没有真正的引用传递(除了 class 本身就是引用类型)。inout 是拷入拷出语义——函数拿到的是拷贝,修改后写回。这意味着函数内部对 inout 参数的修改不会影响其他指向同一变量的引用。实际效果和引用传递很像,但不是一回事。 ### inout 有什么限制? 不能传常量(let)或字面量——必须是 var。不能传计算属性——因为计算属性没有存储空间让函数写回。不能传有属性观察器(willSet/didSet)的属性——因为拷出时会触发观察器,但函数内部的修改不是通过正常的赋值路径。同一个函数调用中,同一个变量不能作为多个 inout 参数传入。 ### 什么时候该用 inout? 极少用。Swift 风格更倾向返回新值而不是就地修改。合理场景:swap 函数、reduce 的 accumulator 模式、性能敏感场景避免大值拷贝。如果一个函数需要"返回多个值",优先用元组返回值,不要用多个 inout 参数。 ### inout 参数能传给另一个 inout 函数吗? 不能直接传——同一个变量不能同时作为两个 inout 参数。但可以在第一个 inout 函数返回后,把修改后的变量传给第二个函数。这个限制是为了防止两个函数同时修改同一个变量,导致结果不可预测。 ## 写段代码 ```swift func swap<T>(_ a: inout T, _ b: inout T) { let temp = a; a = b; b = temp } var x = 10, y = 20 swap(&x, &y) // x=20, y=10 // 累加器模式 func accumulate(_ value: Int, into total: inout Int) { total += value } var sum = 0 accumulate(5, into: &sum) // sum=5 accumulate(10, into: &sum) // sum=15 // 错误用法 // let a = 5; modify(&a) // 编译错误:a 是 let // modify(&view.frame.width) // 编译错误:frame 是计算属性 ```
服务端5月27日 11:52
Swift 初始化器有哪些?指定初始化器和便利初始化器有什么区别?Swift 有三种初始化器:指定初始化器(designated,负责初始化所有属性)、便利初始化器(convenience,调用其他初始化器的快捷方式)、可失败初始化器(init?,参数无效时返回 nil)。 指定初始化器是"主力"——必须初始化类引入的所有属性,然后调用父类的指定初始化器。便利初始化器是"辅助"——必须调用同类的另一个初始化器,最终链到指定初始化器。规则简单:便利调便利或指定,指定调父类指定。 ## 追问 ### 便利初始化器能调用父类的初始化器吗? 不能。便利初始化器必须调用同类的初始化器(`self.init`),不能直接调 `super.init`。这是 Swift 初始化安全链的保证——所有初始化路径最终都经过指定初始化器。如果子类需要父类的初始化器,编译器会自动继承(条件是子类没有自定义指定初始化器)。 ### 可失败初始化器 init? 和 init! 有什么区别? `init?` 返回 Optional,调用方得到 `Type?` 必须解包。`init!` 返回隐式解包 Optional,调用方直接用,nil 时崩溃。init! 基本只用于兼容 Objective-C 的初始化器——ObjC 的 init 返回 nil 表示失败,映射到 Swift 就是 init!。新代码用 init?。 ### 结构体的初始化器和类有什么区别? 结构体没有指定/便利之分——所有初始化器地位平等。编译器自动合成成员初始化器(前提是没有自定义 init)。类没有自动合成的成员初始化器(除非所有属性有默认值且没有父类)。结构体的 init 不需要调用 super.init(没有继承)。 ### 初始化器什么时候能访问 self? 类:指定初始化器在调用 super.init 之后(父类初始化完成),便利初始化器在调用 self.init 之后。结构体:所有存储属性初始化之后。在此之前访问 self 会编译报错——因为 self 还没完全构造。 ### required init 是什么? 标记 `required` 的初始化器,子类必须实现。典型场景:NSCoding 的 `init(coder:)`——如果子类不实现,反序列化会崩溃。required 保证了继承链上每个类都能响应这个初始化器。子类实现 required init 时不需要 override 关键字(因为不是重写,是满足协议要求)。 ## 写段代码 ```swift class Vehicle { var wheels: Int init(wheels: Int) { self.wheels = wheels } // 指定初始化器 } class Car: Vehicle { var brand: String init(brand: String, wheels: Int) { // 指定初始化器 self.brand = brand super.init(wheels: wheels) } convenience init(brand: String) { // 便利初始化器 self.init(brand: brand, wheels: 4) } } // 可失败初始化器 struct Temperature { let celsius: Double init?(celsius: Double) { guard celsius >= -273.15 else { return nil } self.celsius = celsius } } if let temp = Temperature(celsius: -300) { print(temp) // 不会执行,nil } ```
服务端5月27日 11:52
Swift guard 语句怎么用?和 if let 有什么区别?guard 在条件不满足时提前退出,减少嵌套。核心规则:guard 的 else 块必须终止当前作用域(return/break/continue/throw),条件满足时绑定的变量在后续代码中可用——这是和 if let 最大的区别。 guard 让代码从上到下读——"先验证条件,不满足就走人,满足就继续"。比 if-else 的金字塔嵌套清晰得多。 ## 追问 ### guard let 和 if let 有什么区别? if let 解包后变量只在 if 块内可用,guard let 解包后变量在 guard 之后的整个作用域可用。所以 guard 更适合"验证后继续用"的场景,if let 更适合"有值就处理,没有就不处理"的场景。函数参数验证几乎都用 guard——提前 return,主逻辑不需要嵌套在 if 里。 ### guard 可以配合多个条件吗? 可以,用逗号分隔:`guard let name = name, !name.isEmpty, name.count < 50 else { return }`。逗号是"且"的关系——所有条件都满足才继续。也支持 `where` 子句:`guard let age = age where age >= 18`(Swift 3 之后改用逗号语法)。 ### guard 在循环里怎么用? 循环里 guard 的 else 块用 continue(跳过当前迭代)或 break(退出循环)。常见模式:遍历数组时跳过不符合条件的元素。比在循环体里嵌套 if 更清晰——"不符合就跳过,符合才处理"。 ### guard 能用于可选链吗? 不能直接用。`guard let x = obj?.property` 编译不过——可选链返回的是 Optional,guard let 需要完整解包。正确做法:先解包 obj,再访问 property。或者用 if let + 可选链处理多层嵌套的可选值。 ### guard 有什么性能影响? 没有。guard 在编译后和 if-else 一样,只是语法糖。编译器不会因为 guard 生成额外代码。选择 guard 纯粹是为了可读性——代码意图更清晰,减少嵌套。 ## 写段代码 ```swift // guard let: 解包后后续可用 func process(user: User?) { guard let user = user else { return } print(user.name) // user 在这里可用 } // 多条件 guard func register(name: String?, age: Int?) { guard let name = name, !name.isEmpty else { return } guard let age = age, age >= 18 else { return } print("Registered: \(name), \(age)") } // 循环中用 continue let items: [Int?] = [1, nil, 3, nil, 5] for item in items { guard let value = item else { continue } print(value) // 只打印 1, 3, 5 } // guard + throw func divide(_ a: Int, by b: Int) throws -> Int { guard b != 0 else { throw DivisionError.zero } return a / b } ```
服务端5月27日 11:50
Swift 泛型怎么用?泛型约束和关联类型有什么区别?泛型让你写"适用于多种类型"的代码,编译器在使用时确定具体类型。函数用 `<T>` 声明占位类型,类型用 `<Element>` 声明泛型参数。泛型保证了类型安全——编译期检查,不会运行时类型错误。 泛型约束限制 T 必须满足什么条件:`<T: Equatable>` 要求 T 可比较,`<T: AnyObject>` 要求 T 是类类型。where 子句做更复杂的约束:`where T.Element: Comparable`。 关联类型是协议里的泛型——用 `associatedtype Item` 声明,遵循协议的类型确定具体类型。有关联类型的协议不能直接当类型用,需要 `some Protocol` 或 `any Protocol`。 ## 追问 ### 泛型约束和 where 子句有什么区别? 泛型约束写在 `<T: Protocol>` 里,是简单的"遵循某个协议"。where 子句写在函数签名后面,可以做更精确的约束:`where T.Element == U.Element`(两个泛型的元素类型相同)、`where T: Collection, T.Index == Int`。简单约束用 `:` 语法,复杂约束用 where。 ### some 和 any 有什么区别? `some Protocol` 是不透明类型——编译器知道具体类型但调用方不知道,性能好(无动态派发)。`any Protocol` 是存在类型——运行时可以是任何遵循协议的类型,有装箱开销。函数返回值优先用 some(Swift 5.7+),需要存储不同类型时用 any。 ### 泛型函数和函数重载有什么区别? 重载为每种类型写一个函数,泛型只写一个。重载可以在每个版本做不同实现,泛型所有类型共享同一个实现。如果不同类型需要不同逻辑,用重载或协议扩展。如果逻辑相同只是类型不同,用泛型。 ### 泛型的类型擦除是什么? 有关联类型的协议不能直接当类型用——`let items: Container` 编译不过,因为 Container 的 Item 类型不确定。类型擦除用包装器隐藏具体类型:AnyContainer<Item> 包装任何 Container,对外只暴露 Item 类型。标准库的 AnySequence、AnyPublisher 都是类型擦除。 ## 写段代码 ```swift // 泛型函数 func first<T>(of array: [T]) -> T? { array.first } // 泛型约束 func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? { array.firstIndex(of: value) } // where 子句 func allEqual<C: Collection>(in collection: C) -> Bool where C.Element: Equatable { guard let first = collection.first else { return true } return collection.allSatisfy { $0 == first } } // 关联类型 protocol Container { associatedtype Item var count: Int { get } mutating func append(_ item: Item) } struct Stack<Element>: Container { private var items: [Element] = [] var count: Int { items.count } mutating func append(_ item: Element) { items.append(item) } mutating func pop() -> Element? { items.popLast() } } ```
服务端5月27日 11:50
Swift 扩展 extension 能做什么?有什么限制?扩展(extension)给已有类型添加新功能,不需要访问源码。能加计算属性、方法、初始化器、嵌套类型、协议遵循。不能加存储属性、不能重写已有方法、不能添加指定初始化器。 扩展最大的价值:给系统类型加业务方法。比如给 String 加 `var isPhoneNumber: Bool`,给 Int 加 `var formatted: String`,给 UIColor 加 `convenience init(hex:)`——代码组织更清晰,不用写工具类。 ## 追问 ### 扩展能添加存储属性吗? 不能。存储属性需要修改类型的内存布局,扩展没有这个权限。需要额外存储空间时,用关联对象(Objective-C 运行时的 objc_setAssociatedObject)或者用包装类型。纯 Swift 类型没法用关联对象,只能换设计——用字典存储额外数据,或者改用子类/包装 struct。 ### 扩展和继承有什么区别? 扩展是"横向添加功能",不改变类型的继承关系;继承是"纵向派生子类",可以重写方法。扩展不能重写已有方法,继承可以。扩展适用于所有类型(struct/enum/protocol),继承只适用于 class。优先用扩展——更轻量,不引入继承链的复杂性。 ### 协议扩展的默认实现是怎么工作的? 协议扩展可以为协议方法提供默认实现,遵循协议的类型如果不自己实现就用默认的。但如果类型通过另一个协议扩展也提供了实现,调用时选哪个取决于变量的静态类型——这就是"协议扩展派发"的坑。解决:把方法写在协议声明里(不是扩展里),这样走动态派发,运行时决定。 ### 扩展里的私有成员对外可见吗? Swift 4 之前,同一文件的多个扩展可以访问彼此的 private 成员。Swift 4 之后,扩展和类型定义在同一文件时也能访问 private 成员,但不同文件的扩展不行。fileprivate 始终对同文件可见。 ## 写段代码 ```swift // 给系统类型加计算属性 extension String { var isPhoneNumber: Bool { let regex = "^1[3-9]\\d{9}$" return range(of: regex, options: .regularExpression) != nil } } // 便利初始化器 extension UIColor { convenience init(hex: String) { let hex = hex.trimmingCharacters(in: .alphanumerics.inverted) var int: UInt64 = 0 Scanner(string: hex).scanHexInt64(&int) let r, g, b: UInt64 switch hex.count { case 6: (r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF) default: (r, g, b) = (0, 0, 0) } self.init(red: Double(r)/255, green: Double(g)/255, blue: Double(b)/255, alpha: 1) } } // 协议默认实现 protocol Identifiable { var id: String { get } } extension Identifiable { var id: String { UUID().uuidString } } ```
服务端5月27日 11:50
Swift 枚举的关联值和原始值有什么区别?怎么用?Swift 的枚举比其他语言强得多——每个 case 可以携带关联值(不同类型的数据),也可以有原始值(同类型的预填充值)。关联值是"每个实例存不同的数据",原始值是"每个 case 对应一个固定值"。 关联值让枚举变成"带数据的标签":`case success(User)` 携带一个 User,`case failure(Error)` 携带一个 Error。原始值让枚举变成"有名字的常量":`case mercury = 1, venus, earth`,rawValue 自动递增。 用 switch 匹配关联值时,可以提取数据:`case .success(let user)`。如果所有关联值都用 let/let,简写为 `case let .success(user)`。 ## 追问 ### 关联值和原始值能同时用吗? 不能。一个枚举要么有关联值要么有原始值,不能两者兼有。原始值要求所有 case 共享同一类型(Int/String/Character/Float/Double),关联值每个 case 可以不同。需要两者时,用关联值 + 计算属性模拟 rawValue。 ### 原始值的自动赋值规则是什么? Int 类型从 0 开始递增,可以指定起始值:`case mercury = 1, venus, earth` 则分别是 1、2、3。String 类型默认是 case 名字本身。手动指定了某个 case 的 rawValue,后面的自动递增。注意 rawValue 必须唯一,重复会编译报错。 ### 枚举可以嵌套吗? 可以。枚举可以嵌套在 struct/class/enum 内部,用于表达层级关系。比如 `AST.Expression.Literal.number(42)`。嵌套枚举的 case 仍然可以用 `.caseName` 简写(类型推断时)。 ### 枚举和 struct 相比什么时候用枚举? 状态是互斥的用枚举(只能是 A、B、C 中的一种),状态是组合的用 struct(可以同时有 A 和 B)。网络请求结果用枚举(成功或失败,不会同时),用户配置用 struct(可以同时有多个设置)。枚举强制穷举检查,适合有限状态机。 ### indirect enum 是什么? 枚举的 case 引用自身时(递归枚举),需要加 `indirect` 关键字。比如链表:`indirect enum List { case empty; case node(Int, List) }`。不加 indirect 编译器报错——因为递归类型的内存大小不确定,indirect 告诉编译器用引用语义存储。 ## 写段代码 ```swift // 关联值 enum Result<Success, Failure: Error> { case success(Success) case failure(Failure) } // 原始值 enum Planet: Int { case mercury = 1, venus, earth, mars } Planet.earth.rawValue // 3 Planet(rawValue: 3) // Optional(Planet.earth) // 提取关联值 enum Barcode { case upc(Int, Int, Int, Int) case qrCode(String) } let code = Barcode.upc(8, 85909, 51226, 3) switch code { case let .upc(a, b, c, d): print("\(a)-\(b)-\(c)-\(d)") case let .qrCode(str): print(str) } // 递归枚举 indirect enum Tree { case leaf(Int) case node(Tree, Tree) } ```
服务端5月27日 11:50
Swift 并发编程怎么做?async/await 和 Actor 怎么用?Swift 5.5 引入了 async/await 模型,用线性的代码写异步逻辑,告别回调地狱。async 标记异步函数,await 挂起等待结果,编译器保证不会阻塞线程。Task 是异步任务的执行容器,Actor 是线程安全的引用类型。 async/await 的核心优势:异步代码看起来和同步代码一样,从上到下顺序执行,错误处理也用正常的 try-catch。编译器在 await 挂起点自动让出线程,不浪费资源。 `async let` 实现结构化并发——多个异步操作并行执行,`await` 时一起等结果。`TaskGroup` 更灵活,适合动态数量的并行任务。 Actor 是带隔离的 class——同一时刻只有一个任务能访问 actor 的可变状态,编译器在编译期检查,不需要手动加锁。actor 的属性和方法默认隔离,外部访问必须用 await。 ## 追问 ### Task 和 async let 有什么区别? `async let` 是结构化并发——子任务的生命周期绑定在当前函数作用域,函数退出时子任务自动取消。`Task` 是非结构化并发——任务独立于作用域存在,需要手动管理取消。简单并行用 async let,复杂场景(动态添加任务、手动取消)用 Task 或 TaskGroup。 ### Actor 和 class 有什么区别? Actor 和 class 都是引用类型,区别在并发安全:actor 的隔离属性和方法同一时刻只允许一个任务访问,class 没有这个保证。外部调用 actor 的方法必须 await(因为可能等锁),class 不需要。Actor 没有 deinit 问题(不像 class 需要担心循环引用),因为 actor 本身就是为并发设计的。 ### Sendable 是什么?什么时候需要? Sendable 标记"可以安全跨并发域传递"的类型。值类型(struct/enum)如果所有属性都是 Sendable,自动遵循。class 默认不是 Sendable——引用类型跨域传递可能产生数据竞争。函数参数跨 actor 边界时,编译器要求类型必须是 Sendable。如果你确定某个 class 是线程安全的,可以手动标记 `@unchecked Sendable`。 ### MainActor 是什么? MainActor 是标记"必须在主线程执行"的特殊 actor。UI 更新必须在主线程,用 `@MainActor` 标记的函数/类型自动在主线程调度。SwiftUI 的 View body 就是隐式 @MainActor 的。从后台任务切回主线程:`await MainActor.run { ... }`,或者调用 @MainActor 标记的方法。 ## 写段代码 ```swift // async/await func fetchUser(id: String) async throws -> User { let (data, _) = try await URLSession.shared.data(from: url) return try JSONDecoder().decode(User.self, from: data) } // async let 并行 func loadAll() async throws -> (User, [Post]) { async let user = fetchUser(id: "1") async let posts = fetchPosts() return try await (user, posts) } // Actor actor Counter { private var value = 0 func increment() -> Int { value += 1; return value } } let counter = Counter() Task { let v = await counter.increment() // 必须 await } // @MainActor @MainActor func updateUI() { label.text = "done" } Task { let data = await fetch() // 后台 await updateUI() // 切主线程 } ```
服务端5月27日 11:50
Swift 字符串怎么拼接、截取和替换?常用方法详解Swift 的 String 是值类型,基于 Unicode 标量构建。拼接用 `+` 和插值 `\()`,截取用 `prefix`/`suffix`/`dropFirst`/`dropLast`,替换用 `replacingOccurrences`,查找用 `contains`/`hasPrefix`/`hasSuffix`。 拼接最常用字符串插值:`"Hello, \(name)"`,比 + 拼接更灵活,支持表达式。截取要注意 Swift 的 String 不能用整数下标——因为 Unicode 字符长度不等,必须用 String.Index。`str.index(str.startIndex, offsetBy: 5)` 取偏移位置,`str[first..<last]` 取子串。 替换支持正则:`str.replacingOccurrences(of: "[0-9]", with: "*", options: .regularExpression)`。查找除了 contains/hasPrefix/hasSuffix,还可以用 `range(of:)` 获取位置,`ranges(of:)` 获取所有匹配位置。 ## 追问 ### 为什么 Swift 的 String 不能用整数下标? Swift 的 String 是 Unicode 正确的——一个"字符"可能由多个 Unicode 标量组成(如 emoji 👨‍👩‍👧‍👦 由 4 个标量组成)。用整数下标意味着 O(n) 遍历找到位置,所以 Swift 干脆不允许,强制用 String.Index。如果确实需要按下标访问,先转成 Array<Character> 再用整数索引。 ### Substring 和 String 有什么区别? String 切片得到的是 Substring,它和原 String 共享底层内存(写时复制),不会立即拷贝。Substring 适合临时使用,长期持有应该转成 String(`String(substring)`),否则原字符串的内存无法释放。函数返回值推荐用 String,内部临时操作用 Substring 省内存。 ### 如何高效拼接大量字符串? 用 `+` 或插值在循环里拼接,每次都创建新字符串,O(n²) 复杂度。高效做法是用 `joined()`:`array.joined(separator: ",")` 一次完成拼接,或者用 `String` 的 `write(to:)` 流式写入。少量拼接用插值就行,别过早优化。 ### Swift 字符串比较是按什么规则? `==` 比较的是 Unicode 标量是否相同,不是字节相同。"é" 可以是一个标量(U+00E9)也可以是两个(e + 组合重音 U+0301),它们视觉一样但 `==` 返回 false。如果需要规范比较,用 `str.precomposedStringWithCanonicalMapping` 先标准化再比较。 ## 写段代码 ```swift let str = "Hello, Swift!" // 拼接 let greeting = "Hello, \(name)" // 截取 str.prefix(5) // "Hello" str.suffix(6) // "Swift!" str.dropFirst(7) // "Swift!" // 替换(支持正则) str.replacingOccurrences(of: "Swift", with: "World") str.replacingOccurrences(of: "[aeiou]", with: "*", options: .regularExpression) // 查找 str.contains("Swift") // true str.hasPrefix("Hello") // true str.range(of: "Swift") // Range<String.Index>? // 索引访问 let idx = str.index(str.startIndex, offsetBy: 7) str[idx...] // "Swift!" ```
服务端5月27日 11:30
Swift Codable 怎么用?JSON 键名不一致和默认值怎么处理?Codable 是 Encodable + Decodable 的类型别名,让 Swift 类型可以自动编解码为 JSON、Plist 等格式。只要所有属性都是 Codable 类型,编译器自动合成编解码逻辑——不需要手写一行解析代码。 ```swift struct User: Codable { let id: Int let name: String let email: String? } // 自动支持 JSONEncoder/JSONDecoder ``` Codable 最大的价值是消灭了手动 JSON 解析的样板代码。之前用 Objective-C 的 NSJSONSerialization 返回 NSDictionary,再手动取值转型,又长又容易崩溃。Codable 让这个过程类型安全、编译器检查。 ## 追问 ### JSON 的键名和 Swift 属性名不一样怎么办? 用 CodingKeys 枚举映射: ```swift struct User: Codable { let name: String enum CodingKeys: String, CodingKey { case name = "full_name" } } ``` CodingKeys 必须覆盖所有需要编解码的属性——没列出的属性会被跳过(如果是可选型且有默认值)。这个枚举也可以用来"只编码部分字段",不需要的字段不写进 CodingKeys 即可。 ### 什么时候需要自定义 init(from:) 和 encode(to:)? 自动合成搞不定的场景:日期格式(JSON 里是时间戳或字符串,Swift 是 Date)、嵌套结构不一致、默认值逻辑、多个字段组合解码。比如 JSON 里日期是秒级时间戳: ```swift init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let timestamp = try container.decode(Double.self, forKey: .createdAt) self.createdAt = Date(timeIntervalSince1970: timestamp) } ``` 能用 CodingKeys 解决的就不要自定义 init(from:)——自定义越多,维护成本越高。 ### Codable 的默认值怎么处理? Swift 不支持在 Codable 属性上直接设默认值——如果 JSON 里缺了某个键,解码直接失败。解法一:用可选型 `let email: String?`,缺失时为 nil。解法二:自定义 init(from:),用 `decodeIfPresent` + 空合并运算符:`self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "unknown"`。Swift 5.9 有社区提案支持默认值,但目前还没有正式语法。 ### Codable 和 NSCoding 有什么区别? NSCoding 是 Objective-C 时代的协议,需要手动实现 encode/decode,类型不安全(Any 类型),只支持 class。Codable 是 Swift 原生的,编译器自动合成,类型安全,struct 和 class 都支持,且不依赖 Objective-C 运行时。新项目用 Codable,没有理由再用 NSCoding。 ### CustomStringConvertible 和 Codable 有什么关系? 没有直接关系。CustomStringConvertible 控制 `print()` 和字符串插值的输出格式,Codable 控制序列化/反序列化格式。一个常见错误:在 CustomStringConvertible 的 description 里输出 JSON——应该用 JSONEncoder 编码,description 应该输出人类可读的调试信息,不是 JSON 字符串。 ## 写段代码 ```swift struct User: Codable { let id: Int let name: String let email: String? // 键名映射 enum CodingKeys: String, CodingKey { case id, name case email = "email_address" } } // 编码 let user = User(id: 1, name: "Alice", email: "a@b.com") let data = try JSONEncoder().encode(user) // 解码 let decoded = try JSONDecoder().decode(User.self, from: data) // 自定义日期格式 let decoder = JSONDecoder() decoder.dateDecodingStrategy = .secondsSince1970 decoder.keyDecodingStrategy = .convertFromSnakeCase // snake_case → camelCase ```
服务端5月27日 11:30
Swift 高阶函数 map、filter、reduce 怎么用?有什么区别?高阶函数是"接受函数作为参数"或"返回函数"的函数。Swift 里最常用的是 map、filter、reduce,加上 compactMap、flatMap、sorted。它们让集合操作从命令式循环变成声明式一行代码。 - **map**:变换每个元素,返回新数组 - **filter**:按条件筛选,返回满足条件的元素 - **reduce**:把所有元素聚合成一个值 - **compactMap**:map + 过滤 nil - **flatMap**:map + 展平一层嵌套 ## 追问 ### map 和 compactMap 有什么区别? map 对每个元素做变换,结果包含 nil(如果闭包返回 Optional)。compactMap 会自动过滤掉 nil,只保留非空值。典型场景:字符串数组转 Int——`["1", "abc", "3"].compactMap { Int($0) }` 得到 `[1, 3]`,用 map 则得到 `[Optional(1), nil, Optional(3)]`。规则:闭包返回 Optional 时用 compactMap,否则用 map。 ### flatMap 和 compactMap 有什么区别? Swift 4.1 之后,flatMap 的职责简化了:只用来展平嵌套数组(`[[1,2],[3,4]].flatMap { $0 }` 得到 `[1,2,3,4]`)。过滤 nil 的功能全部交给 compactMap。之前 flatMap 两个功能都有,容易混淆,所以拆开了。现在记住:去 nil 用 compactMap,展平用 flatMap。 ### reduce 能做什么 map 和 filter 做不到的事? reduce 把集合聚合成任意类型的单个值——字符串拼接、字典构建、对象累积修改都能做。map 和 filter 只能返回数组。比如统计字符频率:`str.reduce(into: [:]) { $0[$1, default: 0] += 1 }`,这个 map 做不了。另一个例子:把数组转成字典 `items.reduce(into: [:]) { $0[$1.id] = $1 }`。 ### 链式调用 map + filter + reduce 性能怎么样? 每次链式调用都创建一个中间数组——map 创建一个,filter 再创建一个。数据量大时这有额外内存和 CPU 开销。如果性能敏感,用 reduce 合并操作,或者用 for 循环一次遍历完成。但大部分场景下,链式调用的可读性收益远大于性能损耗——除非 profile 发现有问题,否则别过早优化。 ### forEach 和 for-in 有什么区别? forEach 不支持 break/continue/return——闭包里的 return 只退出当前闭包,不影响外层。for-in 循环可以用 break 提前退出。所以 forEach 只适合"对每个元素执行操作且必须全部执行"的场景。需要条件退出时用 for-in。另外 forEach 不返回值,不能链式调用。 ## 写段代码 ```swift let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // map: 变换 let doubled = numbers.map { $0 * 2 } // filter: 筛选 let evens = numbers.filter { $0 % 2 == 0 } // reduce: 聚合 let sum = numbers.reduce(0, +) // compactMap: 去 nil let strings = ["1", "abc", "3"] let ints = strings.compactMap { Int($0) } // [1, 3] // flatMap: 展平 let nested = [[1, 2], [3, 4]] let flat = nested.flatMap { $0 } // [1, 2, 3, 4] // 链式:筛选 + 变换 + 聚合 let result = numbers .filter { $0 % 2 == 0 } .map { $0 * $0 } .reduce(0, +) // 4+16+36+64+100 = 220 // reduce(into:): 构建字典 let freq = "hello".reduce(into: [Character: Int]()) { $0[$1, default: 0] += 1 } ```
服务端5月27日 11:29
Swift 结构体和类有什么区别?什么时候用 struct?结构体(struct)和类(class)的核心区别:struct 是值类型,赋值拷贝;class 是引用类型,赋值传引用。Swift 推荐优先用 struct——更安全(不受其他代码影响)、更简单(没有循环引用)、更高效(栈分配 + 写时复制)。 class 能做而 struct 做不了的事:继承、类型转换(is/as)、析构器(deinit)、引用计数(ARC)、共享同一实例。反过来,struct 能做而 class 做不了的事:编译器自动合成 init(class 没有成员初始化器)、不需要手动管理内存、天然线程安全(值隔离)。 ## 追问 ### 什么时候必须用 class? 三种情况:需要继承(UIKit 的视图体系)、需要共享状态(单例、ViewModel 在多个视图间共享)、需要 deinit 清理资源(关闭文件、取消网络请求)。除了这三种,用 struct。 ### struct 的方法为什么默认不能修改属性? struct 是值类型,方法调用时 self 是 let(不可变)。要修改属性必须标记 `mutating`,这会让编译器在调用时确保变量是 var 而不是 let。class 不需要 mutating——引用类型的方法调用不影响 self 的可变性。mutating 的另一个效果:在 mutating 方法里可以给 self 赋新值,比如 `self = Self()`。 ### struct 可以实现协议吗?和 class 实现协议有什么区别? 都可以实现协议。区别在于:如果协议要求 mutating 方法,struct 实现时必须标 mutating,class 不需要——因为 class 的方法天然可以修改属性。另外,class 实现的协议可以用作 existencial(`let x: Protocol = MyClass()`),struct 实现的协议在 Swift 5.7 之前有更多限制(有关联类型时不能直接当类型用)。 ### struct 的 init 是怎么合成的? 如果没有自定义 init,编译器自动合成成员初始化器:`Point(x: 1, y: 2)`。一旦你写了自定义 init,成员初始化器就没了。class 没有自动合成的成员初始化器——必须手写 init,或者所有属性都有默认值且不需要传参。Swift 5.10 之后 struct 在有自定义 init 的同时可以保留成员初始化器,用 `init(x:y:)` 即可。 ### 为什么 Apple 在 SwiftUI 里大量用 struct? 两个原因:值语义让视图状态隔离——修改一个视图的状态不会意外影响其他视图;struct 没有引用计数开销,视图创建销毁频繁,用 struct 性能更好。SwiftUI 的 View 协议要求是 struct,body 属性每次调用都生成新的值,diff 算法比较新旧值决定是否更新。如果用 class,diff 成本和内存管理都会更复杂。 ## 写段代码 ```swift struct Point { var x: Int, y: Int } class Node { var value: Int = 0; var next: Node? } // 值类型:拷贝隔离 var p1 = Point(x: 1, y: 2) var p2 = p1 p2.x = 10 print(p1.x) // 1 // 引用类型:共享实例 var n1 = Node(); n1.value = 42 var n2 = n1; n2.value = 99 print(n1.value) // 99 // mutating 修改属性 struct Counter { var count = 0 mutating func increment() { count += 1 } } var c = Counter() c.increment() ```
服务端5月27日 11:29
Swift 值类型和引用类型有什么区别?什么时候用 struct?值类型(struct、enum、tuple)赋值时拷贝,每个变量拥有独立的数据副本;引用类型(class、closure)赋值时传递引用,多个变量指向同一个实例。这是 Swift 最根本的类型区分,直接影响内存管理、线程安全和代码行为。 值类型的"拷贝"不是每次赋值都真拷贝——Swift 有写时复制(Copy-on-Write)优化,只有修改时才真正创建副本,所以大数组的传参开销没有想象中大。 ## 追问 ### 值类型和引用类型在内存上有什么区别? 值类型通常分配在栈上,分配和释放由编译器管理,速度快。引用类型分配在堆上,由 ARC 管理生命周期,有引用计数的开销。栈上的值类型在函数返回时自动释放,不需要额外的内存管理;堆上的引用类型需要 ARC 追踪引用计数,计数归零时才释放。 ### 写时复制(Copy-on-Write)是什么? 值类型赋值时不立即拷贝数据,而是和原始值共享底层存储。只有当某一方尝试修改时,才真正创建独立副本。Array、Dictionary、String 等标准库类型都用了这个优化。自定义 struct 要支持 COW,需要手动检测引用计数并在修改时拷贝。 ### 什么时候必须用引用类型(class)? 三种情况:需要继承和多态、需要共享状态(多个地方修改同一个实例)、需要控制生命周期(deinit 做清理)。UI 控件的 ViewModel、网络请求的 Session、文件句柄——这些场景需要用 class。其他情况优先 struct。 ### 值类型一定线程安全吗? 值类型在单次赋值/传参时是安全的(因为拷贝隔离),但如果你把值类型放在 class 里作为属性,多线程同时修改就不安全了——值类型的拷贝只发生在赋值时,不是每次访问时。所以"值类型线程安全"的说法不严谨,准确说是"值类型的独立副本之间互不影响"。 ### struct 里嵌套 class 会怎样? struct 仍然是值类型,赋值时 struct 本身被拷贝,但内部的 class 引用不会深拷贝——多个 struct 副本共享同一个 class 实例。这会导致意外的数据共享:修改一个 struct 副本里的 class 属性,其他副本也会受影响。如果需要真正的深拷贝,必须手动实现 Clone 方法或者在 struct 里只放值类型。 ## 写段代码 ```swift struct Point { var x: Int, y: Int } // 值类型 class Node { var value: Int = 0 } // 引用类型 var p1 = Point(x: 1, y: 2) var p2 = p1 // 拷贝 p2.x = 10 print(p1.x) // 1 — p1 不受影响 var n1 = Node() n1.value = 42 var n2 = n1 // 传递引用 n2.value = 99 print(n1.value) // 99 — n1 受影响,指向同一实例 // struct 嵌套 class 的陷阱 struct Wrapper { var node = Node() } var w1 = Wrapper() var w2 = w1 // struct 拷贝,但 node 是引用 w2.node.value = 100 print(w1.node.value) // 100 — 共享同一个 Node 实例! ```
服务端5月27日 11:29
Swift Array、Set、Dictionary 有什么区别?怎么选?Swift 有三种集合类型:Array(有序、可重复)、Set(无序、不可重复)、Dictionary(键值对)。选哪个看需求:需要顺序和索引用 Array,需要去重和集合运算用 Set,需要键值查找用 Dictionary。 Array 是最常用的——按下标随机访问 O(1),尾部插入删除 O(1),中间插入删除 O(n)。Set 基于 HashMap,查找/插入/删除都是 O(1),但不能保序。Dictionary 也是 HashMap,键必须可哈希,下标访问返回 Optional(键可能不存在)。 ## 追问 ### Set 的集合运算有哪些?实际用在哪? 并集(union)、交集(intersection)、差集(subtracting)、对称差集(symmetricDifference)。实际场景:标签系统——"用户A的标签和用户B的标签有多少重叠"用交集,"A有B没有的"用差集,"两人不同时拥有的"用对称差集。Set 比 Array 做这些操作快得多——Array 的 contains 是 O(n),Set 是 O(1)。 ### Dictionary 的下标访问为什么返回 Optional? 因为键可能不存在。如果下标直接返回值类型,访问不存在的键就会崩溃。返回 Optional 让你安全处理两种情况:`if let value = dict["key"]` 安全解包,`dict["key"]!` 强制解包(确定存在时)。这比 NSDictionary 的返回 AnyObject? 更安全——Swift 的类型系统保证了值的类型。 ### 什么时候用 Array,什么时候用 Set? 需要顺序或允许重复用 Array。去重或集合运算用 Set。一个常见误区:去重时先 Array 再手动过滤——直接用 Set 初始化就行了:`Set(array).sorted()`。另外 Set 不保序,如果去重后还要保持原始顺序,得用 reduce 手动构建有序数组,或者用 Swift 5.7+ 的 OrderedCollections 库。 ### 集合类型作为函数参数时应该传值还是传引用? Array、Set、Dictionary 都是值类型(struct),传参时默认拷贝。Swift 有写时复制(Copy-on-Write)优化——只有实际修改时才真正拷贝,所以大数组传参不会浪费内存。但如果函数内部需要修改集合并影响调用方,需要用 inout 参数:`func append(_ item: Int, to array: inout [Int])`。 ### Dictionary 怎么安全地修改值? 用下标修改时,键不存在会自动插入 nil 值。用 `updateValue(_:forKey:)` 返回旧值(Optional),可以知道是更新还是新增。最常见的模式是"有就更新,没有就插入默认值":`dict[key, default: 0] += 1`——比先判断再操作简洁得多。 ## 写段代码 ```swift // Array 基本操作 var nums = [3, 1, 4, 1, 5] nums.sort() // [1, 1, 3, 4, 5] nums.append(9) // 尾部插入 O(1) // Set 去重和集合运算 let a: Set = [1, 2, 3, 4] let b: Set = [3, 4, 5, 6] a.intersection(b) // {3, 4} a.symmetricDifference(b) // {1, 2, 5, 6} // Dictionary 安全操作 var freq: [String: Int] = [:] freq["apple", default: 0] += 1 // 安全递增,不存在时默认0 freq.updateValue(5, forKey: "banana") // 返回旧值 // 去重但保持顺序 let items = [3, 1, 4, 1, 5, 3] var seen = Set<Int>() let unique = items.filter { seen.insert($0).inserted } ```
服务端5月27日 11:28
Swift 访问控制有哪些级别?open 和 public 有什么区别?Swift 有五个访问级别,从宽松到严格:open > public > internal > fileprivate > private。internal 是默认值——不写访问修饰符就是 internal。 核心区别在于两个维度:谁能访问(同模块/跨模块/同文件/同作用域),和能不能继承重写(只有 open 允许跨模块继承和重写)。 | 级别 | 同模块 | 跨模块 | 可继承重写 | |------|--------|--------|------------| | open | ✅ | ✅ | ✅(跨模块) | | public | ✅ | ✅ | ❌(跨模块不可重写) | | internal | ✅ | ❌ | ✅(同模块内) | | fileprivate | 同文件 | ❌ | 同文件内 | | private | 同作用域 | ❌ | 同作用域内 | open 和 public 的唯一区别:open 允许跨模块继承和重写,public 不允许。框架对外暴露的基类用 open,工具类/辅助类用 public。 ## 追问 ### open 和 public 有什么区别?什么时候用 open? open 允许其他模块继承类和重写方法,public 只允许访问不允许继承重写。用 open 的场景:你设计一个框架,希望使用者能继承你的基类来定制行为——UIKit 的 UIViewController 就是 open 的。用 public 的场景:功能完整的类,不希望被子类化——比如工具类、配置类。 ### fileprivate 和 private 有什么区别? private 限制在定义的作用域内——类里的 private 属性,类的方法能访问,但扩展(同文件)不能访问(Swift 4 之前)。fileprivate 限制在定义的文件内——同文件的所有类型和扩展都能访问。Swift 4 之后 private 在同文件的扩展里也能访问了,所以 fileprivate 的用武之地变少了。如果多个类型需要共享某个属性或方法,放在同文件里用 fileprivate。 ### 默认访问级别是什么?为什么不写修饰符就是 internal? Swift 的哲学是"模块即边界"——大部分代码只在模块内部使用,不需要暴露给外部。internal 正好对应这个边界:模块内可见,模块外不可见。不写修饰符就是 internal,减少了大量样板代码。只有明确需要跨模块的 API 才需要写 public 或 open。 ### 子类的访问级别可以比父类更严格吗? 可以。子类可以把父类的 open 方法重写为 public,但不能反过来——父类是 public 的方法,子类不能重写为 open。规则是"子类的访问级别不能比父类更宽松",但重写时可以收紧。属性也一样——父类 public var,子类可以重写为 internal var。 ### 访问控制和泛型有什么交互? 泛型类型的访问级别取决于类型本身和泛型参数中最严格的那个。`public class Container<T: PrivateProtocol>` 编译不过——public 类型不能依赖 private 协议。函数同理:`public func process(_ item: InternalType)` 也编译不过。规则是"实体的访问级别不能比它依赖的类型更高"。 ## 写段代码 ```swift // 框架对外 API open class BaseService { open func execute() { } // 允许跨模块重写 public func validate() { } // 允许跨模块调用,但不允许重写 } // 模块内部实现 class InternalHelper { // 默认 internal fileprivate func assist() { } // 同文件可访问 private var state: Int = 0 // 同作用域可访问 } // 子类收紧访问级别 class MyService: BaseService { public override func execute() { } // ✅ 收紧:open → public } ```
服务端5月27日 11:27
Swift 下标 subscript 怎么自定义?和多参数下标下标(subscript)让你用 `instance[index]` 的方式访问值,不需要调方法。Array、Dictionary 的 `[]` 访问就是下标实现的。自定义类型也可以定义下标,用 `subscript` 关键字。 下标本质是 getter + setter 的语法糖,和计算属性类似——可以只读也可以读写,可以有多个参数。区别是下标用 `[]` 调用,属性用 `.` 调用;下标可以接受参数,属性不能。 ## 追问 ### 下标可以接受多个参数吗? 可以,参数数量和类型没有限制。典型用法是二维数组用 `matrix[row, col]` 访问。也可以用不同类型的参数重载下标——比如字典同时支持 `dict["key"]` 和 `dict[0]`。参数还可以有默认值和可变参数。 ### 下标和方法的区别?什么时候该用下标? 下标是访问语义——"取一个值"或"设一个值"。方法是操作语义——"执行一个动作"。如果一个操作的核心含义是"按索引取值",用下标;如果是"做某件事",用方法。字典的 `dict["key"]` 是下标(取值),`dict.removeValue(forKey:)` 是方法(执行删除)。别滥用下标——复杂逻辑用方法更清晰。 ### 类型下标是什么? 用 `static subscript` 定义,通过类型名直接调用:`SomeType[index]`,不需要实例。枚举常用类型下标——比如 `Planet[4]` 返回第四颗行星。类型下标不能是实例下标的重载(参数签名相同时会冲突)。 ### 下标能抛出错误吗? 能。Swift 5.2 开始下标可以标记 `throws`,调用时需要 `try`。不过实际项目中很少用——下标调用期望是简单的取值操作,抛出错误会让调用方写一堆 try-catch,违背了下标的简洁语义。索引越界通常用返回 Optional(`dict[key]` 返回 Value?)或直接 crash(`array[index]`)来处理,而不是抛错误。 ## 写段代码 ```swift struct Matrix { let rows: Int, cols: Int var grid: [Double] init(rows: Int, cols: Int) { self.rows = rows; self.cols = cols self.grid = Array(repeating: 0.0, count: rows * cols) } subscript(row: Int, col: Int) -> Double { get { grid[row * cols + col] } set { grid[row * cols + col] = newValue } } } var m = Matrix(rows: 3, cols: 3) m[0, 0] = 1.0 m[1, 1] = 2.0 print(m[0, 0]) // 1.0 // 类型下标 enum Planet: Int { case mercury = 1, venus, earth, mars static subscript(n: Int) -> Planet? { Planet(rawValue: n) } } print(Planet[3]!) // earth ```
服务端5月27日 11:27
Swift 协议是什么?协议扩展和面向协议编程怎么理解?协议定义了一组属性和方法的蓝图,任何类型都可以遵循协议来实现这些要求。Swift 的协议比 Java 的接口更强——它支持默认实现(通过协议扩展)、协议组合、关联类型,是面向协议编程(POP)的核心。 协议本身不实现功能,只定义"需要什么"。遵循协议的类型必须提供具体实现。协议可以作为类型使用——函数参数、变量、集合元素都可以是协议类型,实现多态。 ## 追问 ### 协议扩展的默认实现有什么用? 协议扩展可以为协议方法提供默认实现,遵循协议的类型如果不自己实现,就自动用默认的。这解决了两个问题:一是不需要每个遵循类型都写一遍相同逻辑;二是可以给协议添加新方法而不破坏已有的遵循类型。标准库里大量用了这个特性——Collection 协议的 map、filter 都是协议扩展提供的默认实现。 ### 协议组合是什么?和继承有什么区别? 用 `ProtocolA & ProtocolB` 把多个协议组合成一个临时类型,不需要定义新的协议。和继承的区别:组合是"有这些能力",继承是"是一种"。一个类型可以同时遵循任意多个协议(组合),但只能继承一个父类(单继承)。实际开发中,优先用协议组合代替继承——更灵活,耦合更低。 ### 关联类型是什么?什么时候用? 协议里用 `associatedtype` 声明一个占位类型,由遵循协议的具体类型来确定。比如 Collection 协议的 Element 就是关联类型——Array 的 Element 是具体类型,Dictionary 的 Element 是键值对。有关联类型的协议不能直接当类型用(不能用 `let x: SomeProtocol`),需要用泛型约束 `some SomeProtocol` 或 `any SomeProtocol`。 ### any 和 some 关键字有什么区别? `any Protocol` 是存在类型(existential),运行时可以是任何遵循协议的类型,有动态派发开销。`some Protocol` 是不透明类型,编译期确定具体类型,性能更好。Swift 5.7 开始,协议类型的变量必须显式写 `any`,否则编译器警告。函数返回值用 `some` 可以隐藏具体类型同时保证性能。 ### 面向协议编程(POP)比 OOP 好在哪? POP 用协议+扩展代替继承,解决了 OOP 的几个痛点:单继承限制(协议可以组合多个)、脆弱基类问题(协议扩展不依赖继承链)、强耦合(协议只定义接口,不绑定实现)。Swift 标准库本身就是面向协议设计的——Array 遵循 Collection 协议获得几十个方法,而不是继承自某个基类。 ## 写段代码 ```swift protocol Drawable { func draw() } // 协议扩展提供默认实现 extension Drawable { func draw() { print("默认绘制") } } protocol Scalable { var scale: CGFloat { get set } mutating func resize(to scale: CGFloat) } // 协议组合 func render(_ item: some Drawable & Scalable) { item.draw() } // 关联类型 protocol Container { associatedtype Item var count: Int { get } mutating func append(_ item: Item) subscript(i: Int) -> Item { get } } struct Stack<Element>: Container { private var items: [Element] = [] var count: Int { items.count } mutating func append(_ item: Element) { items.append(item) } subscript(i: Int) -> Element { items[i] } } ```
服务端5月27日 11:26
Swift 属性包装器 @propertyWrapper 怎么用?有什么局限?属性包装器 @propertyWrapper 把属性的 get/set 逻辑抽出来封装成可复用的类型。比如你有一堆属性都需要做范围限制、线程安全、UserDefaults 存取——不写包装器的话,每个属性的 getter/setter 里都要写一遍相同的逻辑。 属性包装器的核心是 wrappedValue:它替代了原始属性的存取逻辑。外部访问属性时,实际访问的是包装器的 wrappedValue。包装器还可以通过 projectedValue(用 $ 前缀访问)提供额外功能,比如标记值是否被裁剪过。 ```swift @propertyWrapper struct Clamped<Value: Comparable> { var value: Value let range: ClosedRange<Value> var wrappedValue: Value { get { value } set { value = min(max(newValue, range.lowerBound), range.upperBound) } } var projectedValue: Bool { value != value // 是否被裁剪 } } struct Player { @Clamped(range: 0...100) var health: Int = 100 } ``` ## 追问 ### 属性包装器的 wrappedValue 和 projectedValue 有什么区别? wrappedValue 是属性的值本身,通过属性名直接访问。projectedValue 是包装器暴露的额外信息,通过 $ 前缀访问。比如 @UserDefault 的 wrappedValue 是存储的值,$someKey 可以返回一个 Publisher 用于响应式监听。projectedValue 不是必须的,大部分简单包装器只实现 wrappedValue。 ### 属性包装器能替代 willSet/didSet 吗? 能,而且更灵活。willSet/didSet 是写在属性定义里的,每个属性重复一遍;属性包装器把同样的逻辑封装成类型,多个属性复用。但属性包装器比 willSet/didSet 复杂得多——如果只是简单的值变化通知,willSet/didSet 更直接。需要复用逻辑时才用包装器。 ### SwiftUI 里常用的属性包装器有哪些?各自的作用? - @State:值类型本地状态,视图独占 - @Binding:父子的双向数据绑定 - @ObservedObject:外部传入的引用类型观察对象 - @StateObject:视图自己创建和持有的观察对象 - @EnvironmentObject:从环境注入的共享对象 - @Published:在 ObservableObject 里标记属性变化时自动通知 这些本质上都是属性包装器,但 SwiftUI 框架为它们注入了特殊的依赖追踪和视图刷新机制。 ### 属性包装器有什么局限? 不能包装 lazy 属性和带观察器的属性——它们对存储方式有特殊要求,和包装器冲突。包装器的 init 不能访问 enclosing instance(所在类型的实例),所以包装器内部没法直接调用所在类型的方法。另外,包装器增加了间接调用层级,极端性能场景下可能有微小的额外开销。 ## 写段代码 ```swift @propertyWrapper struct UserDefault<T> { let key: String let defaultValue: T var wrappedValue: T { get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) } } } struct Settings { @UserDefault(key: "darkMode", defaultValue: false) var darkMode: Bool @UserDefault(key: "fontSize", defaultValue: 14) var fontSize: Int } var settings = Settings() settings.darkMode = true // 自动写入 UserDefaults print(settings.darkMode) // 自动从 UserDefaults 读取 ```