swift面试题及答案(二)中级进阶

Swift中级进阶面试题及答案,涵盖核心特性、内存管理、协议、泛型等关键知识点,适合中级进阶开发者考察:

1.  Swift中值类型和引用类型的核心区别是什么?各自的代表类型有哪些?

答案:

• 核心区别:值类型赋值时会复制数据(“值传递”),引用类型赋值时仅传递指针(“引用传递”,共享同一份数据)。

• 代表类型:

值类型:Struct、Enum、Tuple、基本数据类型(Int、String等)。

引用类型:Class、Function、Closure。

2.  可选类型(Optional)的本质是什么?如何安全解包?

答案:

• 本质:Optional是Swift的泛型枚举,定义为enum Optional<T> { case none; case some(T) },nil即Optional.none。

• 安全解包方式:

可选绑定:if let/guard let

可选链:object?.property?.method()(若链中任一环节为nil,整体返回nil)

nil合并运算符:let value = optionalValue ?? defaultValue

3.  协议扩展(Protocol Extension)的作用是什么?与类扩展有何不同?

答案:

• 作用:为协议添加默认实现(方法、计算属性等),无需让遵守协议的类型手动实现,提升代码复用性。

• 与类扩展的区别:

协议扩展可给所有遵守协议的类型添加功能;类扩展仅作用于单个类。

协议扩展中无法添加存储属性;类扩展可以添加计算属性,但同样不能添加存储属性。

4.  泛型约束(Generic Constraints)如何使用?举例说明where子句的用法。

答案:

• 泛型约束用于限制泛型参数的类型范围,确保其满足特定条件(如遵守协议、继承自某类)。

• where子句示例:

// 约束T必须遵守Equatable协议

func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {

    for (index, item) in array.enumerated() {

        if item == value { return index }

    }

    return nil

}

// 多约束场景(T遵守Equatable,U继承自UIView)

func someFunction<T, U>(t: T, u: U) where T: Equatable, U: UIView { ... }

5.  闭包的“捕获值”机制是什么?捕获列表(Capture List)的作用?

答案:

• 捕获值:闭包会捕获其定义环境中的变量/常量,延长其生命周期(即使原作用域销毁,闭包仍可访问)。

• 捕获列表:在闭包参数前用[]定义,用于修改捕获方式(解决强引用循环),例如:

class Person {

    var name: String

    init(name: String) { self.name = name }

    lazy var closure: () -> Void = { [weak self] in  // 弱引用捕获self,避免循环

        print(self?.name ?? "已释放")

    }

}

6.  ARC如何工作?强引用循环(Strong Reference Cycles)如何产生和解决?

答案:

• ARC(自动引用计数):通过跟踪对象的强引用数量,当计数为0时自动释放内存。

• 强引用循环产生场景:两个对象互相强引用(如类实例与闭包互相引用)。

• 解决方式:

对类实例:用weak(可选类型,对象释放后变为nil)或unowned(非可选,对象必须始终存在)。

对闭包:通过捕获列表[weak self]或[unowned self]弱化引用。

7.  枚举的“关联值”和“原始值”有何区别?

答案:

• 关联值:为枚举成员附加动态数据(编译期未知),同一成员可关联不同类型。

enum Result {

    case success(Int)  // 关联“成功码”

    case failure(String)  // 关联“错误信息”

}

• 原始值:枚举成员的固定值(编译期已知),所有成员必须是同一类型,通过rawValue访问。

enum Direction: String {

    case north = "N"

    case south = "S"

}

print(Direction.north.rawValue)  // 输出 "N"

8.  结构体(Struct)和类(Class)的核心区别?

答案:

• 继承:类支持继承,结构体不支持。

• 引用类型vs值类型:类是引用类型(共享数据),结构体是值类型(复制数据)。

• 析构器:类可定义deinit(对象销毁前调用),结构体不能。

• 身份标识:类有唯一身份(可用===比较是否为同一实例),结构体无(仅比较值是否相等)。

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

答案:

• 机制:通过Error协议定义错误类型,用throw抛出错误,do-catch捕获处理。

• 区别:

try:必须配合do-catch,明确处理错误。

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

try!:强制解包(认为不会出错),出错时崩溃。

10. 属性观察器(willSet、didSet)的作用?有哪些限制?

答案:

• 作用:监听属性值的变化,willSet在值改变前调用(参数为新值),didSet在值改变后调用(参数为旧值)。

• 限制:

不能用于lazy属性(初始化时机不确定)。

初始化器中修改属性不会触发观察器(仅外部修改时触发)。

计算属性无需观察器(可直接在set中处理)。

11. 延迟存储属性(lazy var)的特点?

答案:

• 特点:

首次访问时才初始化(延迟初始化),节省资源。

必须用var(不可变let要求编译期确定值)。

初始化闭包中可访问self(因初始化延迟到访问时,self已完全初始化)。

• 示例:

class DataManager {

    lazy var database: Database = {  // 首次访问时才创建数据库实例

        Database.connect()

    }()

}

12. 协议中的“关联类型”(associatedtype)如何使用?

答案:

• 作用:在协议中声明一个占位类型,由遵守协议的类型指定具体类型(类似泛型协议)。

• 示例:

protocol Container {

    associatedtype Item  // 关联类型

    mutating func append(_ item: Item)

    var count: Int { get }

}

// 遵守协议时指定Item为Int

struct IntContainer: Container {

    typealias Item = Int  // 可省略(编译器自动推断)

    private var items: [Int] = []

    mutating func append(_ item: Int) { items.append(item) }

    var count: Int { items.count }

}

13. 逃逸闭包(@escaping)的使用场景?为什么需要标记?

答案:

• 场景:闭包生命周期超过函数调用周期(如异步操作回调、存储在外部变量中)。

• 为什么标记:

非逃逸闭包(默认):函数返回后自动销毁,编译器可优化内存(如不捕获self)。

逃逸闭包:需显式标记@escaping,告知编译器其生命周期更长,需手动处理引用(如避免循环引用)。

• 示例:

var completionHandlers: [() -> Void] = []

func addCallback(_ callback: @escaping () -> Void) {  // 逃逸闭包(存储到外部数组)

    completionHandlers.append(callback)

}

14. 自动闭包(@autoclosure)的作用?

答案:

• 作用:自动将表达式包装为闭包,延迟表达式执行(直到闭包被调用),简化语法。

• 示例:

// 不使用自动闭包:需显式传入闭包

func check(condition: () -> Bool) {

    if condition() { print("Valid") }

}

check(condition: { 1 > 0 })  // 调用时需写闭包

// 使用自动闭包:直接传表达式

func checkAuto(@autoclosure condition: () -> Bool) {

    if condition() { print("Valid") }

}

checkAuto(condition: 1 > 0)  // 自动包装为闭包,更简洁

15. 扩展(Extension)的限制?

答案:

• 限制:

不能添加存储属性(只能添加计算属性)。

不能重写原有方法(可添加新方法,或通过协议扩展提供默认实现)。

不能添加指定初始化器(可添加便利初始化器,但需调用原有初始化器)。

16. 类型转换操作符is、as?、as!的区别?

答案:

• is:检查实例是否为某类型,返回Bool。

• as?:安全转换为目标类型,返回Optional(成功为值,失败为nil)。

• as!:强制转换(认为一定成功),失败时崩溃(谨慎使用)。

• 示例:

class Animal {}

class Dog: Animal {}

let pet: Animal = Dog()

print(pet is Dog)  // true(is检查)

if let dog = pet as? Dog { ... }  // 安全转换(as?)

let dog = pet as! Dog  // 强制转换(as!)

17. 下标(subscript)的作用?如何自定义?

答案:

• 作用:让自定义类型支持类似数组/字典的下标访问(instance[index])。

• 示例(为String扩展下标访问字符):

extension String {

    subscript(index: Int) -> Character? {

        guard index >= 0, let strIndex = self.index(startIndex, offsetBy: index, limitedBy: endIndex) else {

            return nil

        }

        return self[strIndex]

    }

}

let str = "Swift"

print(str[1])  // "w"

18. static和class关键字在定义方法时的区别?

答案:

• 共同点:都用于定义类型方法(无需实例即可调用)。

• 区别:

static:不可被子类重写( final 类型方法)。

class:可被子类重写(仅类支持,结构体/枚举只能用static)。

• 示例:

class Parent {

    static func staticMethod() {}  // 不可重写

    class func classMethod() {}    // 可重写

}

class Child: Parent {

    override class func classMethod() {}  // 允许重写

}

19. 自动闭包(@autoclosure)与逃逸闭包结合使用的场景?

答案:

• 场景:需要延迟执行表达式,且闭包需逃逸(如异步验证)。

• 示例(异步检查条件):

func checkLater(@autoclosure @escaping condition: () -> Bool, completion: @escaping (Bool) -> Void) {

    DispatchQueue.global().async {

        let result = condition()  // 延迟执行表达式

        completion(result)

    }

}

checkLater(1 > 0) { print($0) }  // 自动包装为闭包,且支持逃逸

20. Result类型的作用?

答案:

• 作用:统一处理异步操作的结果(成功/失败),避免回调嵌套或多个可选参数。

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

• 示例:

func fetchData(completion: @escaping (Result<String, Error>) -> Void) {

    if let data = "response".data(using: .utf8) {

        completion(.success(String(data: data, encoding: .utf8)!))

    } else {

        completion(.failure(NSError(domain: "FetchError", code: -1)))

    }

}

21. 委托模式(Delegate Pattern)在Swift中如何实现?

答案:

• 实现步骤:

1. 定义协议(声明委托方法)。

2. 委托方声明weak var delegate: 协议?(用weak避免循环引用)。

3. 代理方遵守协议并实现方法。

• 示例:

// 1. 定义协议

protocol ButtonDelegate: AnyObject {  // 限制为类类型,支持weak

    func buttonTapped()

}

// 2. 委托方(按钮)

class Button {

    weak var delegate: ButtonDelegate?  // weak避免循环引用

    func tap() { delegate?.buttonTapped() }

}

// 3. 代理方(视图控制器)

class ViewController: ButtonDelegate {

    let button = Button()

    init() { button.delegate = self }

    func buttonTapped() { print("按钮被点击") }

}

22. @objc的作用?何时需要使用?

答案:

• 作用:将Swift声明暴露给Objective-C(生成Objective-C兼容的接口)。

• 使用场景:

调用Objective-C代码时(如UIKit的@selector)。

实现NSObject的方法(如KVO、通知)。

标记需要被Objective-C反射的成员。

23. 闭包的“尾随闭包”(Trailing Closure)语法?

答案:

• 作用:当函数最后一个参数是闭包时,可将闭包写在函数括号外,简化语法。

• 示例:

// 普通写法

UIView.animate(withDuration: 0.3, animations: {

    self.view.alpha = 0

})

// 尾随闭包写法(省略参数名)

UIView.animate(withDuration: 0.3) {

    self.view.alpha = 0

}

24. 如何实现单例模式(Singleton)?需注意什么?

答案:

• 实现(线程安全版):

class NetworkManager {

    static let shared: NetworkManager = {

        let instance = NetworkManager()

        // 初始化配置

        return instance

    }()

    private init() {}  // 私有初始化器,禁止外部创建实例

}

• 注意:

私有初始化器(private init)防止外部实例化。

线程安全:Swift的static let初始化是原子操作,默认线程安全。

25. GCD中async和sync的区别?

答案:

• async:异步执行(不阻塞当前线程),提交任务后立即返回,任务在指定队列中后台执行。

• sync:同步执行(阻塞当前线程),等待任务执行完成后才返回。

• 注意:避免在主线程中用sync调用主线程队列(会导致死锁)。

26. 可失败初始化器(init?)的作用?

答案:

• 作用:初始化可能失败时(如参数无效),返回nil表示失败(普通初始化器必须成功)。

• 示例:

struct PositiveNumber {

    let value: Int

    init?(value: Int) {  // 可失败初始化器

        guard value > 0 else { return nil }  // 失败返回nil

        self.value = value

    }

}

let num = PositiveNumber(value: -5)  // nil(初始化失败)

27. defer语句的作用?

答案:

• 作用:确保代码块在当前作用域退出前执行(无论正常退出还是错误抛出),常用于资源释放(如关闭文件、解锁锁)。

• 示例:

func readFile() {

    let file = openFile()

    defer { closeFile(file) }  // 函数退出前一定会关闭文件

    // 读取文件(即使中途return或throw,defer仍执行)

    if errorOccurred { return }

}

28. 函数重载(Function Overloading)的条件?

答案:

• 条件:同一作用域内,函数名相同但参数列表不同(参数个数、类型、标签不同),与返回值无关。

• 示例:

func sum(a: Int, b: Int) -> Int { a + b }

func sum(a: Double, b: Double) -> Double { a + b }  // 重载(类型不同)

func sum(_ a: Int, _ b: Int) -> Int { a + b }  // 重载(标签不同)

29. 属性包装器(@propertyWrapper)的基本原理?

答案:

• 原理:将属性的读写逻辑封装到一个结构体/枚举/类中,简化重复逻辑(如验证、存储)。

• 示例(限制数值范围的包装器):

@propertyWrapper

struct Clamped<Value: Comparable> {

    private var value: Value

    let min: Value, max: Value

   

    init(wrappedValue: Value, min: Value, max: Value) {

        self.min = min

        self.max = max

        self.value = wrappedValue.clamped(to: min...max)  // 初始化时验证

    }

   

    var wrappedValue: Value {

        get { value }

        set { value = newValue.clamped(to: min...max) }  // 设置时验证

    }

}

// 使用

struct Product {

    @Clamped(min: 0, max: 100) var stock: Int = 50  // 库存限制在0-100

}

30. Swift中如何实现KVO?

答案:

• 步骤:

1. 类必须继承NSObject(KVO基于Objective-C运行时)。

2. 被观察属性用@objc dynamic标记(确保动态派发)。

3. 调用addObserver(_:forKeyPath:options:context:)注册观察者。

4. 实现observeValue(forKeyPath:of:change:context:)处理变化。

5. 移除观察者(避免崩溃)。

• 示例:

class User: NSObject {

    @objc dynamic var name: String  // 必须@objc dynamic

    init(name: String) { self.name = name }

}

class Observer: NSObject {

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

        print("name changed to: \(change?[.newKey] as! String)")

    }

}

let user = User(name: "Tom")

let observer = Observer()

user.addObserver(observer, forKeyPath: "name", options: .new, context: nil)

user.name = "Jerry"  // 触发KVO,输出变化

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

相关阅读更多精彩内容

友情链接更多精彩内容