Swift面试题及答案(三)高级进阶

Swift高阶面试题及答案,覆盖内存管理、并发、协议、泛型等核心高级特性:

1. 解释Swift中的ARC(自动引用计数)及其工作原理,如何避免循环引用?

答案:

ARC通过跟踪对象的引用计数(retain count)自动管理内存:对象被引用时计数+1,引用失效时-1,计数为0时释放内存。

循环引用(两个对象互相强引用)会导致内存泄漏,解决方式:

• weak:修饰可选类型引用,当对象释放时自动置为nil(适用于可能为nil的场景)。

• unowned:修饰非可选类型引用,假设引用对象始终有效(适用于确定不会提前释放的场景,如闭包捕获自身但生命周期绑定的情况)。

2. 闭包的捕获列表(Capture List)有什么作用?如何使用?

答案:

捕获列表用于指定闭包对外部变量的捕获方式(强、弱、无主),主要解决闭包与外部对象的循环引用。

语法:在闭包参数列表前用[]声明,例如:

let closure = { [weak self, unowned someObject] in

    guard let self = self else { return }

    self.doSomething(with: someObject)

}

• [weak self]:弱引用捕获self,避免闭包强引用self导致循环。

3. 泛型约束(Generic Constraints)的作用是什么?举例说明带约束的泛型函数。

答案:

泛型约束限制泛型参数必须遵循特定协议或继承特定类型,确保代码安全并支持更具体的操作。

示例:定义一个比较两个元素是否相等的泛型函数,约束元素必须遵循Equatable:

func isEqual<T: Equatable>(a: T, b: T) -> Bool {

    return a == b

}

4. 协议中的associatedtype和Self有什么区别?

答案:

• associatedtype:在协议中声明一个未指定的关联类型,由遵循协议的类型具体定义(类似泛型的“占位符”)。

示例:protocol Container { associatedtype Item; var items: [Item] { get } }

• Self:指代遵循协议的具体类型本身(动态类型),用于限制方法返回值或参数类型与自身一致。

示例:protocol Copyable { func copy() -> Self }

5. GCD中sync和async的区别?使用sync可能导致什么问题?

答案:

• async:异步执行任务,立即返回,不阻塞当前线程(任务在指定队列中并发/串行执行)。

• sync:同步执行任务,阻塞当前线程直到任务完成(必须等待任务结束才继续)。

风险:若在同一串行队列中调用sync(如主线程调用sync到主队列),会导致死锁(当前线程等待自身队列的任务完成)。

6. Swift 5.5引入的async/await相比GCD有什么优势?

答案:

• 代码线性化:避免“回调地狱”,逻辑更清晰(类似同步代码的写法)。

• 结构化并发:通过Task管理任务生命周期,支持取消和优先级。

• 错误处理:直接与try/catch结合,无需嵌套闭包处理错误。

7. Actor的作用是什么?如何保证线程安全?

答案:

Actor是Swift 5.5引入的并发安全类型,用于解决多线程数据竞争。

• 内部状态仅能通过自身方法访问,且所有方法调用会被自动序列化(同一时间只有一个任务执行)。

• 跨Actor访问需用await(异步等待),确保线程安全。

示例:

actor Counter {

    private var value = 0

    func increment() { value += 1 }

}

8. 解释Swift中的值类型(Value Type)和引用类型(Reference Type)的区别,各有哪些代表类型?

答案:

• 值类型:赋值或传递时复制完整数据(独立副本),修改不影响原数据。

代表:struct、enum、tuple、String、Array(本质是结构体,通过Copy-on-Write优化)。

• 引用类型:赋值或传递时共享同一份实例(仅复制引用地址),修改会影响所有引用者。

代表:class、closure(捕获引用类型时)。

9. Copy-on-Write(写时复制)机制在Swift中如何工作?举例说明。

答案:

值类型(如Array、Dictionary)的优化机制:当多个变量共享同一份数据时,仅存储引用;首次修改其中一个变量时,才真正复制数据(避免不必要的复制开销)。

示例:

var a = [1, 2, 3]

var b = a  // 此时a和b共享同一份数据(引用)

b.append(4)  // 首次修改b,触发复制,a仍为[1,2,3]

10. map、flatMap、compactMap的区别?各举一个场景。

答案:

• map:将集合元素通过闭包转换为新元素,返回同长度集合(如[1,2].map { $0 * 2 } → [2,4])。

• flatMap:

处理嵌套集合:展平二维集合(如[[1,2], [3]].flatMap { $0 } → [1,2,3])。

处理可选值:过滤nil并解包(Swift 4.1后被compactMap替代)。

• compactMap:过滤集合中的nil并解包可选值(如[1, nil, 3].compactMap { $0 } → [1,3])。

11. @propertyWrapper(属性包装器)的作用是什么?如何自定义一个?

答案:

封装属性的读写逻辑(如验证、存储、计算),复用属性访问逻辑。

自定义步骤:

1. 定义遵循PropertyWrapper协议的类型。

2. 实现wrappedValue属性(包装的实际值)。

示例(UserDefaults存储):

@propertyWrapper

struct UserDefault<T> {

    let key: String

    var wrappedValue: T? {

        get { UserDefaults.standard.object(forKey: key) as? T }

        set { UserDefaults.standard.set(newValue, forKey: key) }

    }

}

// 使用:@UserDefault(key: "username") var username: String?

12. Swift的错误处理机制是什么?try?、try!、try有什么区别?

答案:

通过Error协议定义错误类型,用throw抛出,try调用,do-catch捕获。

• try:必须在do-catch中使用,强制处理可能的错误。

• try?:将错误转为可选值(成功返回值,失败返回nil,无需catch)。

• try!:强制认为无错误(失败时崩溃,慎用)。

13. Result类型的作用是什么?如何使用?

答案:

Result是一个枚举,用于封装异步操作的“成功”或“失败”结果,避免嵌套回调中的错误处理混乱。

定义:enum Result<Success, Failure: Error> { case success(Success), case failure(Failure) }

示例:

func fetchData() -> Result<String, Error> {

    return .success("data") // 或 .failure(NetworkError.timeout)

}

// 使用:

switch fetchData() {

case .success(let data): print(data)

case .failure(let error): print(error)

}

14. Any和AnyObject的区别是什么?各自的使用场景?

答案:

• Any:可表示任何类型(值类型、引用类型、函数类型等),如Int、String、class实例、闭包。

• AnyObject:仅表示类的实例(引用类型),等价于Objective-C的id(用于与OC互操作)。

场景:Any用于完全不确定类型的场景;AnyObject用于限定为类实例的场景(如[AnyObject]存储类实例数组)。

15. 协议扩展(Protocol Extension)和类扩展(Class Extension)的区别?协议扩展中能否添加存储属性?

答案:

• 协议扩展:为所有遵循协议的类型添加默认方法实现(无需每个类型单独实现),但不能添加存储属性(只能添加计算属性)。

• 类扩展:为特定类添加方法、计算属性等(不影响子类),可访问类的原有成员。

协议扩展的核心是“为协议提供默认实现”,而类扩展是“为单个类增强功能”。

16. 解释Swift中的条件一致性(Conditional Conformance)?

答案:

允许类型在满足特定条件时才遵循某个协议(即“部分遵循”)。

示例:让Array在元素遵循Equatable时才遵循Equatable:

extension Array: Equatable where Element: Equatable {

    static func == (lhs: [Element], rhs: [Element]) -> Bool {

        guard lhs.count == rhs.count else { return false }

        for (l, r) in zip(lhs, rhs) where l != r { return false }

        return true

    }

}

17. SwiftUI中的@State、@Binding、@ObservedObject的区别?

答案:

• @State:管理视图内部私有状态(值类型),仅在当前视图内生效,标记为private。

• @Binding:建立与父视图@State的双向绑定(引用父视图的状态),用于子视图修改父视图状态。

• @ObservedObject:观察外部的ObservableObject实例(引用类型),当对象的@Published属性变化时,视图刷新。

18. Combine框架中的Publisher和Subscriber是什么关系?如何自定义Publisher?

答案:

• Publisher:发送事件流(值、完成、失败),是数据的“生产者”。

• Subscriber:接收并处理Publisher发送的事件,是数据的“消费者”。

自定义Publisher需实现Publisher协议(指定Output和Failure类型),并提供receive(subscriber:)方法绑定订阅者。

19. 枚举的关联值(Associated Values)和原始值(Raw Values)有什么区别?如何使用递归枚举?

答案:

• 关联值:为枚举case附加自定义数据(可不同类型),用于携带动态信息(如enum Result { case success(Int), case failure(String) })。

• 原始值:枚举case的预定义常量值(所有case必须同类型),通过rawValue访问(如enum Direction: String { case up = "UP" })。

递归枚举:枚举case包含自身类型,需用indirect修饰:

indirect enum Tree {

    case leaf(Int)

    case node(Tree, Tree) // 包含自身类型

}

20. 结构体(Struct)和类(Class)的初始化器有什么区别?

答案:

• 结构体:自动生成成员初始化器(包含所有存储属性),无需显式定义;若自定义初始化器,需包含所有存储属性。

• 类:无默认成员初始化器,必须显式定义初始化器(或继承父类初始化器);继承时需确保所有存储属性被初始化(super.init()需在子类属性初始化后调用)。

21. 动态方法派发(Dynamic Dispatch)和静态方法派发(Static Dispatch)的区别?

答案:

• 动态派发:运行时确定调用的方法(基于对象的实际类型),支持多态(类的继承中重写方法),通过虚函数表实现,性能略低。

• 静态派发:编译时确定调用的方法(基于变量声明类型),无运行时开销,适用于结构体、枚举、协议扩展方法(非required或override的方法)。

22. 什么是类型擦除(Type Erasure)?在Swift中如何实现?

答案:

类型擦除用于隐藏具体类型,仅暴露协议接口(解决泛型类型在多态场景下的类型不匹配问题)。

实现方式:定义一个“包装器”类型,封装具体泛型实例,转发协议方法调用。

示例(AnyCollection的简化实现):

protocol MyCollection { associatedtype Element; func next() -> Element? }

struct AnyMyCollection<Element>: MyCollection {

    private let _next: () -> Element?

    init<C: MyCollection>(_ collection: C) where C.Element == Element {

        _next = collection.next

    }

    func next() -> Element? { _next() }

}

23. DispatchGroup的作用是什么?如何使用它等待多个任务完成?

答案:

DispatchGroup用于管理多个并发任务,监听所有任务完成的事件。

使用步骤:

1. 任务开始前调用enter(),结束后调用leave()(enter和leave必须成对)。

2. 通过notify(queue:)在所有任务完成后执行回调,或wait()阻塞当前线程等待。

示例:

let group = DispatchGroup()

let queue = DispatchQueue.global()

for i in 0..<3 {

    group.enter()

    queue.async {

        defer { group.leave() }

        print("Task \(i)")

    }

}

group.notify(queue: .main) { print("All done") }

24. 延迟存储属性(lazy var)有什么特点?使用时需注意什么?

答案:

• 特点:首次访问时才初始化(节省资源),初始化闭包在首次访问时执行。

• 注意:

必须声明为var(不可为let)。

初始化闭包中不能访问self的其他延迟属性(可能未初始化)。

线程不安全(多线程同时访问可能导致多次初始化)。

25. 协议中的required关键字有什么作用?

答案:

required修饰类的初始化器时,要求所有子类必须实现该初始化器(即使子类未定义自己的初始化器),确保子类遵循协议或父类的初始化约束。

示例:

class Parent {

    required init() {}

}

class Child: Parent {

    required init() { super.init() } // 必须实现

}

26. Combine中的Operator(如map、filter)有什么作用?

答案:

Operator是Combine中用于处理数据流的中间件,接收上游Publisher的事件,转换后发送到下游Subscriber。

作用:

• 转换数据(map)、过滤数据(filter)、合并流(merge)等。

• 每个Operator返回新的Publisher,形成“管道式”数据流处理。

27. 为什么说SwiftUI的View是值类型且轻量?

答案:

• View协议的实现者(如Text、VStack)本质是struct(值类型),仅存储视图的配置信息(而非渲染状态)。

• 渲染逻辑由SwiftUI框架内部管理,View实例只是“描述”界面,不包含实际渲染数据,因此轻量。

• 当状态变化时,SwiftUI会重新计算body生成新的View实例,通过差异算法高效更新UI。

28. 泛型的协变(Covariance)和逆变(Contravariance)在Swift中如何体现?

答案:

• 协变:若B是A的子类型,则Generic<B>是Generic<A>的子类型(如[Dog]可视为[Animal]的子类型,仅在只读场景安全)。

• 逆变:若B是A的子类型,则Generic<A>是Generic<B>的子类型(如回调类型(Animal) -> Void可作为(Dog) -> Void使用)。

Swift中通过协议关联类型的out(协变)和in(逆变)关键字声明:

protocol Producer<Output> { associatedtype Output out } // 协变

protocol Consumer<Input> { associatedtype Input in }  // 逆变

29. 闭包和函数的区别是什么?闭包的三种形式是什么?

答案:

• 区别:闭包是自包含的函数代码块,可捕获上下文变量;函数是命名的闭包(无捕获时等价于闭包)。

• 三种形式:

1. 全局函数(无捕获);

2. 嵌套函数(捕获外部函数变量);

3. 匿名闭包(简洁语法,{ (params) -> ReturnType in ... })。

30. 属性观察器(willSet和didSet)的作用?它们在什么情况下不会被调用?

答案:

• willSet:在属性值被设置前调用(参数为新值)。

• didSet:在属性值被设置后调用(参数为旧值)。

不触发的情况:

• 初始化时设置属性(init中赋值);

• 直接修改属性的底层存储(如通过withUnsafePointer(to:));

• 结构体在mutating方法中修改自身属性(部分场景下优化可能跳过)。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容