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,输出变化