Swift经典面试题汇总

目录

[1.Swift 有哪些特点?它和 Objective-C 有什么区别?](#1.Swift 有哪些特点?它和 Objective-C 有什么区别?)

[2.Swift 中 let 和 var 有什么区别?什么时候使用它们?](#2.Swift 中 let 和 var 有什么区别?什么时候使用它们?)

[3.Swift 中的可选类型 Optional 是什么?为什么需要它?](#3.Swift 中的可选类型 Optional 是什么?为什么需要它?)

[4.Swift 中如何安全地解包可选类型?有哪些方式?](#4.Swift 中如何安全地解包可选类型?有哪些方式?)

[5.Swift 中的 guard 语句有什么作用?和 if let 有什么区别?](#5.Swift 中的 guard 语句有什么作用?和 if let 有什么区别?)

回答重点

[6.Swift 中的闭包是什么?它和函数有什么关系?](#6.Swift 中的闭包是什么?它和函数有什么关系?)

回答重点

[7.Swift 中什么是尾随闭包?在什么场景下使用?](#7.Swift 中什么是尾随闭包?在什么场景下使用?)

回答重点

[8.Swift 中的逃逸闭包和非逃逸闭包有什么区别?](#8.Swift 中的逃逸闭包和非逃逸闭包有什么区别?)

回答重点

[9.Swift 的类型推断是如何工作的?它有什么优势?](#9.Swift 的类型推断是如何工作的?它有什么优势?)

回答重点

[10.Swift 中的空合并运算符 ?? 是什么?如何使用?](#10.Swift 中的空合并运算符 ?? 是什么?如何使用?)

回答重点

[11.Swift 中的 class 和 struct 有什么区别?如何选择使用?](#11.Swift 中的 class 和 struct 有什么区别?如何选择使用?)

回答重点

[12.Swift 中什么是值类型和引用类型?有哪些值类型和引用类型?](#12.Swift 中什么是值类型和引用类型?有哪些值类型和引用类型?)

回答重点

[13.Swift 中的协议 Protocol 是什么?它有什么作用?](#13.Swift 中的协议 Protocol 是什么?它有什么作用?)

回答重点

[14.Swift 中什么是协议导向编程 POP?它和面向对象编程有什么区别?](#14.Swift 中什么是协议导向编程 POP?它和面向对象编程有什么区别?)

回答重点

[15.Swift 中的扩展 Extension 是什么?它有哪些使用场景?](#15.Swift 中的扩展 Extension 是什么?它有哪些使用场景?)

[16.Swift 中的枚举有什么特别之处?什么是关联值?](#16.Swift 中的枚举有什么特别之处?什么是关联值?)

回答重点

[17.Swift 中的构造器有哪些类型?convenience init 和 designated init 有什么区别?](#17.Swift 中的构造器有哪些类型?convenience init 和 designated init 有什么区别?)

回答重点

[18.Swift 中如何实现多态?override 关键字的作用是什么?](#18.Swift 中如何实现多态?override 关键字的作用是什么?)

回答重点

[19.Swift 的 ARC 自动引用计数是如何工作的?](#19.Swift 的 ARC 自动引用计数是如何工作的?)

回答重点

[20.Swift 中什么是循环引用?如何避免循环引用?](#20.Swift 中什么是循环引用?如何避免循环引用?)

回答重点

[21.Swift 中 weak 和 unowned 有什么区别?如何选择使用?](#21.Swift 中 weak 和 unowned 有什么区别?如何选择使用?)

回答重点

[22.Swift 中的泛型是什么?为什么要使用泛型?](#22.Swift 中的泛型是什么?为什么要使用泛型?)

回答重点

[23.Swift 中如何给泛型添加约束?associatedtype 是什么?](#23.Swift 中如何给泛型添加约束?associatedtype 是什么?)

回答重点

[24.Swift 中如何处理错误?throw、throws 和 try 有什么区别?](#24.Swift 中如何处理错误?throw、throws 和 try 有什么区别?)

回答重点

[25.Swift 5.5 引入的 async/await 是什么?如何使用?](#25.Swift 5.5 引入的 async/await 是什么?如何使用?)

回答重点

[26.Swift 中的 Actor 是什么?它解决了什么问题?](#26.Swift 中的 Actor 是什么?它解决了什么问题?)

回答重点

[27.Swift 中的属性包装器 Property Wrapper 是什么?如何自定义?](#27.Swift 中的属性包装器 Property Wrapper 是什么?如何自定义?)

回答重点

[28.Swift 中的访问控制有哪些级别?它们有什么区别?](#28.Swift 中的访问控制有哪些级别?它们有什么区别?)

回答重点

[29.Swift 的高阶函数有哪些?map、filter、reduce 分别做什么?](#29.Swift 的高阶函数有哪些?map、filter、reduce 分别做什么?)

回答重点

[30.Swift 和 Objective-C 如何混编?需要注意什么问题?](#30.Swift 和 Objective-C 如何混编?需要注意什么问题?)

回答重点


1.Swift 有哪些特点?它和 Objective-C 有什么区别?

它的核心特点可以概括为三个词:安全、快速、表现力强。

安全性方面,Swift 引入了可选类型(Optional)来处理空值,这在编译期就能避免空指针异常。类型安全也是 Swift 的重要特性,编译器会严格检查类型,减少运行时错误。内存管理使用 ARC 自动引用计数,开发者不需要手动管理内存。

性能方面,Swift 的执行效率接近 C 语言,比 Objective-C 更快。这得益于 Swift 的编译优化和现代语言特性设计。

表现力方面,Swift 的语法简洁优雅,代码可读性强。支持函数式编程、协议导向编程等多种编程范式,让开发者能用更少的代码实现更多功能。

和 Objective-C 相比,最大的区别在于语法和安全性。Objective-C 是 C 语言的超集,语法比较啰嗦,而 Swift 语法更现代、更简洁。Objective-C 中可以给 nil 对象发送消息而不会崩溃,但这也容易隐藏问题;Swift 的可选类型强制开发者处理空值情况,虽然写起来稍微麻烦一点,但能避免很多潜在的 bug。

扩展知识

Swift 的核心优势

Swift 作为苹果主推的编程语言,在实际开发中展现出明显优势。首先是开发效率的提升,Swift 的类型推断能自动推导出变量类型,很多时候不需要显式声明。比如写 let name = "Swift",编译器自动知道这是字符串类型。

其次是错误处理机制更完善。Swift 使用 do-catch-throw 模式处理错误,相比 Objective-C 的 NSError 指针方式更直观。而且编译器会强制要求处理可能抛出的错误,不会让异常被忽略。

再就是泛型支持更强大。Swift 的泛型不仅可以用在函数上,还能用在类、结构体、枚举上,配合协议约束能写出高度抽象和复用的代码。

从 Objective-C 到 Swift 的迁移

虽然 Swift 优势明显,但很多老项目仍在使用 Objective-C。好在 Swift 和 Objective-C 可以无缝混编,在同一个项目中可以同时使用两种语言。Swift 可以调用 Objective-C 的代码,反过来也可以,这让渐进式迁移成为可能。

不过需要注意的是,Swift 的某些特性在 Objective-C 中无法使用,比如泛型、元组、枚举的关联值等。如果要写给 Objective-C 调用的 Swift 代码,需要加上 @objc 标记,并且只能使用 Objective-C 兼容的特性。

目前苹果官方强烈推荐新项目使用 Swift 开发,Swift 也在持续演进,每年的 WWDC 都会发布新版本,带来更多实用特性。从长远来看,Swift 是 iOS 和 macOS 开发的未来方向。

2.Swift 中 let 和 var 有什么区别?什么时候使用它们?

let 和 var 都是 Swift 中声明变量的关键字,它们的核心区别在于是否可以修改。

let 声明的是常量,一旦赋值后就不能再改变。这里的"不能改变"指的是不能重新赋值,但如果是引用类型(比如类的实例),可以修改对象内部的属性,只是不能让这个常量指向另一个对象。

var 声明的是变量,可以随时修改它的值。

在实际开发中,Swift 官方推荐优先使用 let,只有在确实需要改变值的时候才用 var。这是一种防御性编程的思想,因为不可变的数据更安全,不会被意外修改,代码也更容易理解和维护。而且编译器对常量有更好的优化,性能上也会更好一点。

比如在处理用户 ID、配置参数这类确定后不会改变的数据时,就应该用 let。而对于计数器、临时结果这类需要不断更新的数据,才用 var。

扩展知识

值类型和引用类型的区别

let 和 var 的行为在值类型和引用类型上有些微妙的差异。对于值类型(struct、enum),let 声明后整个值都不可变,包括内部的属性都不能修改。

Swift 复制代码
struct Point {
    var x: Int
    var y: Int
}

let point = Point(x: 10, y: 20)
// point.x = 15  // 错误!不能修改

但对于引用类型(class),let 只是保证这个引用不变,对象内部的属性还是可以修改的:

Swift 复制代码
class Person {
    var name: String
    init(name: String) { self.name = name }
}

let person = Person(name: "张三")
person.name = "李四"  // 可以修改属性
// person = Person(name: "王五")  // 错误!不能改变引用

编译器优化

Swift 编译器对 let 常量有特殊优化。因为编译器知道这个值不会改变,所以可以做一些激进的优化,比如内联、常量传播等。在某些场景下,使用 let 能带来可观的性能提升。

而且在多线程环境下,不可变数据天然是线程安全的,不需要加锁保护,这也是 let 的一个重要优势。

实践建议

养成习惯先写 let,如果编译器报错说需要修改,再改成 var。Xcode 也会给出警告,提示哪些 var 可以改成 let。这种编程习惯能让代码更健壮,bug 更少。

在团队协作中,看到 let 声明的变量,其他开发者可以立即知道这个值不会改变,不用担心在其他地方被修改,代码的可预测性更强。

3.Swift 中的可选类型 Optional 是什么?为什么需要它?

回答重点

可选类型是 Swift 最重要的特性之一,用来表示一个值可能存在,也可能不存在(nil)。

在类型后面加个问号 ? 就定义了一个可选类型,比如 String? 表示可选的字符串。可选类型的变量要么包含一个具体的值,要么是 nil。这和普通类型不同,普通类型必须有值,不能为 nil。

为什么需要可选类型?最核心的原因是为了安全。在很多编程语言中,空指针异常是最常见的崩溃原因之一。Java、C++ 中的 NullPointerException 让无数程序员头疼,因为你不知道一个对象是否为 null,直到运行时才会暴露问题。

Swift 通过可选类型把这个问题前移到编译期。编译器强制要求开发者明确处理值可能为空的情况,不处理就无法编译通过。虽然写起来稍微麻烦一点,但能避免大量的运行时崩溃。

从本质上讲,可选类型其实是一个枚举,有两种情况:有值(.some(value))或没有值(.none,也就是 nil)。这种设计让"值是否存在"变成了类型系统的一部分,编译器能做更多的检查和优化。

扩展知识

可选类型的底层实现

可选类型实际上是用泛型枚举实现的,简化后的定义大概是这样:

Swift 复制代码
enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

当我们写 String? 时,实际上是 Optional<String> 的语法糖。nil 就是 .none,有值的情况就是 .some(值)

理解这一点很重要,因为可选类型不是什么神秘的语言特性,就是一个普通的枚举。这也解释了为什么可选类型可以用 switch 来处理:

Swift 复制代码
let age: Int? = 25

switch age {
case .none:
    print("没有年龄信息")
case .some(let value):
    print("年龄是 \(value)")
}

可选类型的使用场景

在实际开发中,可选类型用得非常频繁。字典取值返回的就是可选类型,因为 key 可能不存在;字符串转数字也返回可选类型,因为转换可能失败;从网络获取的数据往往也是可选的,因为请求可能失败。

Swift 复制代码
let dict = ["name": "Swift"]
let name = dict["name"]  // String? 类型

let numberStr = "123"
let number = Int(numberStr)  // Int? 类型

和其他语言的对比

相比 Objective-C 中的 nil(只能用于对象类型),Swift 的可选类型更灵活,任何类型都可以是可选的,包括基本类型 Int、Double 等。

很多现代语言也在借鉴可选类型的设计,比如 Kotlin 的可空类型(Nullable Types)、Rust 的 Option 枚举,都是类似的思想。这说明可选类型确实是解决空值问题的优秀方案。

4.Swift 中如何安全地解包可选类型?有哪些方式?

回答重点

Swift 提供了多种解包可选类型的方式,可以分为安全解包和强制解包两大类。

安全解包的方式有四种。第一种是可选绑定(Optional Binding),使用 if let 或 guard let,这是最常用的方式。if let 会在有值的时候执行代码块,guard let 则在没有值的时候提前返回。

第二种是可选链(Optional Chaining),用问号 ?. 来调用可选类型的属性或方法,如果是 nil 就直接返回 nil,不会崩溃。

第三种是空合并运算符 ??,可以给可选类型提供一个默认值,如果是 nil 就用默认值。

第四种是使用 map 或 flatMap 等函数式方法,对可选类型进行转换。

强制解包是用感叹号 !,但这种方式很危险,如果值是 nil 就会崩溃,所以只有在百分百确定有值的情况下才能用。实际开发中应该尽量避免强制解包,优先使用安全的解包方式。

扩展知识

各种解包方式的具体用法

可选绑定是最推荐的方式,代码清晰且安全:

Swift 复制代码
let name: String? = "Swift"

if let unwrappedName = name {
    print("名字是 \(unwrappedName)")
} else {
    print("没有名字")
}

guard let 特别适合在函数开头做参数检查:

Swift 复制代码
func greet(name: String?) {
    guard let name = name else {
        print("名字不能为空")
        return
    }
    print("你好,\(name)")
}

可选链在处理多层嵌套的可选类型时特别方便:

Swift 复制代码
class Person {
    var address: Address?
}

class Address {
    var street: String
}

let person: Person? = Person()
let street = person?.address?.street  // 任何一层是 nil 都返回 nil

空合并运算符让代码更简洁:

Swift 复制代码
let username: String? = nil
let displayName = username ?? "游客"  // 如果 username 是 nil 就用"游客"

隐式解包可选类型

还有一种特殊的可选类型叫隐式解包可选类型(Implicitly Unwrapped Optional),用感叹号 ! 声明,比如 String!

这种类型在使用时会自动解包,不需要手动解包。它主要用在确定会有值,但初始化时暂时是 nil 的场景,比如 Interface Builder 连接的 IBOutlet。

Swift 复制代码
@IBOutlet weak var label: UILabel!  // 界面加载后一定有值

但要注意,如果在它还是 nil 的时候访问,同样会崩溃,所以使用时要格外小心。

最佳实践

在实际项目中,建议按以下优先级选择解包方式:

  1. 优先使用可选绑定(if let、guard let),最安全
  2. 链式调用用可选链(?.),代码更简洁
  3. 需要默认值时用空合并运算符(??)
  4. 尽量避免强制解包(!),除非确实有把握

看到代码里有很多感叹号,往往是个危险信号,说明没有好好处理可选类型。优秀的 Swift 代码应该尽可能少用强制解包。

5.Swift 中的 guard 语句有什么作用?和 if let 有什么区别?

回答重点

guard 语句是 Swift 中用于提前退出的控制流语句,它的核心作用是让代码的正常流程更清晰,把异常情况提前处理掉。

guard 的语法是 guard 条件 else { 退出代码 },当条件不满足时会执行 else 块中的代码,必须通过 return、break、continue 或 throw 等方式退出当前作用域。最常见的用法是 guard let 来解包可选类型。

和 if let 的主要区别在于代码结构和作用域。if let 是嵌套结构,解包后的变量只能在 if 块内使用,容易形成"金字塔式"的嵌套代码。而 guard let 解包后的变量可以在后续代码中使用,代码呈扁平化结构,可读性更好。

另一个重要区别是语义上的。guard 表达的是"守卫"的含义,用来检查前置条件,不满足就提前退出。这种写法让函数的主要逻辑更清晰,因为所有的边界检查都在开头完成了,后面就是正常的业务逻辑,不需要深层嵌套。

实际开发中,函数开头做参数校验时用 guard,中间处理某个特定情况时用 if,这样代码意图更明确。

6.Swift 中的闭包是什么?它和函数有什么关系?

回答重点

闭包是一段可以在代码中传递和使用的功能代码块,可以简单理解为没有名字的函数,也叫匿名函数。

闭包和函数本质上是一回事,函数其实就是有名字的闭包。在 Swift 中,函数和闭包都是引用类型,都可以作为参数传递,也可以作为返回值。闭包能做的事情函数都能做,反过来也一样。

闭包最大的特点是能够捕获和存储其所在上下文中的常量和变量,即使定义这些常量和变量的原作用域已经不存在了,闭包仍然可以使用和修改这些值。这就是"闭包"这个名字的由来------它能"封闭"并捕获外部的变量。

在实际开发中,闭包用得非常多。比如数组的 map、filter、sort 等方法都接收闭包作为参数,异步网络请求的回调也是用闭包实现的。闭包让代码更简洁,特别是那些只用一次的小功能,没必要专门定义一个函数,直接写个闭包就行。

7.Swift 中什么是尾随闭包?在什么场景下使用?

回答重点

尾随闭包是 Swift 的一种语法糖,当闭包是函数的最后一个参数时,可以把闭包写在函数调用的括号外面,让代码更简洁易读。

如果闭包是唯一的参数,甚至可以把函数调用的括号都省略掉。这种写法在闭包体比较长的时候特别有用,能让代码结构更清晰。

比如数组的 sorted 方法,普通写法是 array.sorted(by: { 闭包 }),用尾随闭包可以写成 array.sorted { 闭包 }。看起来像是在函数后面直接跟了个代码块,读起来更自然。

尾随闭包最适合的场景是那些把闭包作为主要操作的函数,比如集合的 map、filter、forEach,还有异步操作的回调函数。当闭包代码比较长时,尾随闭包能避免代码嵌套太深,提高可读性。

不过如果闭包体很短,比如就一行代码,用不用尾随闭包区别不大,根据个人喜好选择就好。

8.Swift 中的逃逸闭包和非逃逸闭包有什么区别?

回答重点

逃逸闭包和非逃逸闭包的区别在于闭包的生命周期和执行时机。

非逃逸闭包是指在函数返回前就会执行完的闭包,它不会"逃出"函数作用域。Swift 中的闭包参数默认都是非逃逸的,这是更安全的选择,因为编译器能做更多优化,也不容易产生内存问题。

逃逸闭包则是在函数返回后才执行的闭包,需要用 @escaping 关键字标记。最典型的场景就是异步操作的回调,比如网络请求,函数早就返回了,但闭包要等网络响应回来才执行,这时闭包就"逃逸"了。

两者的区别还体现在对 self 的使用上。在非逃逸闭包中可以直接用 self,因为不会产生循环引用。但在逃逸闭包中必须显式写 self,提醒开发者注意可能的循环引用问题。

从性能角度看,非逃逸闭包因为生命周期短,编译器可以把它优化为栈分配,不需要堆内存,效率更高。逃逸闭包必须在堆上分配,因为要保证在函数返回后还能访问。

9.Swift 的类型推断是如何工作的?它有什么优势?

回答重点

类型推断是 Swift 编译器的一项重要特性,它能根据赋值的内容自动推导出变量的类型,不需要开发者显式声明。

工作原理很直观,编译器会分析赋值表达式右边的值,确定它的类型,然后把这个类型赋给左边的变量。比如 let name = "Swift",编译器看到右边是字符串字面量,就知道 name 是 String 类型。

类型推断并不意味着 Swift 是弱类型语言,相反 Swift 是强类型语言,每个变量都有确定的类型。只是编译器帮我们自动推导了,在编译期就确定好了类型,运行时不会改变。

类型推断的优势很明显。首先是代码更简洁,不用写很多类型声明,减少了代码量。其次是可读性更好,特别是当类型名很长的时候,省略类型能让代码重点更突出。最后是重构更容易,如果改变了某个函数的返回类型,不需要修改所有使用它的地方的类型声明。

不过在某些情况下,显式声明类型能提高代码可读性,特别是类型不够明显的时候。所以类型推断和显式声明要根据实际情况灵活选择。

10.Swift 中的空合并运算符 ?? 是什么?如何使用?

回答重点

空合并运算符 ?? 是用来给可选类型提供默认值的快捷方式,它的作用是:如果可选类型有值就使用这个值,如果是 nil 就使用默认值。

语法很简单:可选值 ?? 默认值。比如 let name = username ?? "游客",如果 username 有值就用 username,如果是 nil 就用"游客"。

这个运算符本质上是三元运算符的简化版。a ?? b 等价于 a != nil ? a! : b,但 ?? 的写法更简洁安全,不需要强制解包。

空合并运算符的好处是让代码更清晰。相比用 if let 或者三元运算符,?? 表达的意图更明确------就是提供一个后备值。特别是在链式调用中,?? 能让代码保持简洁流畅。

实际开发中用得非常频繁,比如处理用户输入、网络请求返回的可选数据、从字典取值等场景,都可以用 ?? 来优雅地处理空值情况。

11.Swift 中的 class 和 struct 有什么区别?如何选择使用?

回答重点

class 和 struct 最核心的区别是类型语义不同:class 是引用类型,struct 是值类型。

值类型和引用类型的区别体现在赋值和传递时的行为。struct 在赋值或作为参数传递时会复制整个值,创建一个独立的副本。而 class 传递的是引用,多个变量可以指向同一个对象,修改一个变量会影响到其他变量。

具体功能上,class 支持继承、多态、析构器,可以用 deinit 做清理工作。struct 不支持继承,但可以遵循协议。class 的实例存储在堆上,需要 ARC 管理内存,struct 通常存储在栈上,性能更好。

选择使用哪个要看具体场景。如果数据比较简单,逻辑独立,不需要继承,优先用 struct。比如坐标点、尺寸、简单的数据模型。如果需要共享可变状态,需要继承,或者对象生命周期比较复杂,就用 class。

Swift 官方推荐优先使用 struct,因为值类型更安全,不容易产生意外的数据共享问题。Swift 标准库中的基础类型像 Int、String、Array、Dictionary 都是 struct 实现的,这也体现了值类型在 Swift 中的重要地位。

12.Swift 中什么是值类型和引用类型?有哪些值类型和引用类型?

回答重点

值类型和引用类型是 Swift 中两种基本的数据类型分类,区别在于赋值和传递时的行为。

值类型在赋值或传参时会复制整个值,创建一个完全独立的副本。修改副本不会影响原来的值。就像复印文件,复印件和原件互不影响。

引用类型在赋值或传参时只复制引用(可以理解为地址),多个变量可以指向同一块内存。修改任何一个变量,其他变量看到的都是修改后的值。就像给同一个文件起多个快捷方式,改了文件,所有快捷方式打开的都是改后的内容。

Swift 中的值类型包括:struct、enum、tuple,以及所有基础类型比如 Int、Double、String、Bool、Array、Dictionary、Set 等。这些类型在标准库中都是用 struct 实现的。

引用类型主要就是 class,包括我们自定义的类,以及 UIKit、Foundation 中的大部分类比如 NSObject、UIView、URLSession 等。闭包也是引用类型。

值类型和引用类型的选择影响着程序的性能、安全性和设计模式,理解它们的差异是掌握 Swift 的关键。

13.Swift 中的协议 Protocol 是什么?它有什么作用?

回答重点

协议是 Swift 中定义接口和规范的方式,可以理解为一份"合同",规定了遵循它的类型必须实现哪些方法和属性。

协议只定义规范,不提供实现。任何类型(class、struct、enum)都可以遵循协议,只要实现了协议要求的内容就行。这让不同的类型可以有统一的接口,提高了代码的灵活性和复用性。

协议的作用主要有三个方面。首先是定义接口,让不同的类型可以以相同的方式使用。比如定义一个 Drawable 协议,不管是圆形、矩形还是三角形,只要遵循了这个协议,都能用相同的方式调用 draw 方法。

其次是实现多态,一个协议类型的变量可以存储任何遵循该协议的类型实例,在运行时动态调用具体的实现。这比继承更灵活,因为一个类型可以同时遵循多个协议,而只能继承一个父类。

第三是实现依赖注入和解耦。通过协议定义依赖,而不是依赖具体的类型,让代码更容易测试和维护。比如网络层定义一个 NetworkService 协议,业务层只依赖协议,不依赖具体实现,这样可以轻松替换实现或者 mock 测试。

Swift 推崇协议导向编程(POP),协议是 Swift 设计的核心之一,理解协议是写好 Swift 代码的关键。

14.Swift 中什么是协议导向编程 POP?它和面向对象编程有什么区别?

回答重点

协议导向编程(Protocol-Oriented Programming,POP)是 Swift 推崇的编程范式,核心思想是面向协议设计,而不是面向类继承。

POP 的核心理念是通过协议定义能力和行为,通过协议扩展提供默认实现,通过协议组合实现功能复用。相比传统的面向对象编程,POP 更灵活、更模块化。

和面向对象编程(OOP)的主要区别在于复用机制。OOP 通过继承实现代码复用,一个类只能继承一个父类,容易形成复杂的继承链,而且父类的改变会影响所有子类。POP 通过协议组合实现复用,一个类型可以遵循多个协议,每个协议关注一个特定能力,更加解耦和灵活。

另一个重要区别是类型支持。OOP 主要针对引用类型(class),而 POP 对值类型(struct、enum)和引用类型一视同仁。Swift 的基础类型像 Int、String、Array 都是值类型,用 POP 可以很好地扩展它们。

POP 还避免了一些 OOP 的常见问题,比如脆弱基类问题、菱形继承问题。通过协议扩展提供默认实现,既保证了灵活性,又避免了代码重复。

实际开发中,POP 和 OOP 不是对立的,而是互补的。Swift 同时支持两种范式,根据场景选择最合适的方式才是关键。

15.Swift 中的扩展 Extension 是什么?它有哪些使用场景?

扩展(Extension)是 Swift 中为已有的类型添加新功能的方式,可以在不修改原始代码的情况下给类型增加方法、计算属性、构造器等。

扩展最大的特点是可以扩展任何类型,包括系统类型。你可以给 Int、String、Array 这些标准库类型添加新方法,让它们更符合项目需求。也可以给自己的类型添加功能,而不需要修改原来的定义。

扩展能添加的内容包括:计算属性(不能添加存储属性)、实例方法和类型方法、新的构造器、下标、嵌套类型,以及让类型遵循协议。虽然限制比较多,但已经足够强大。

使用场景很广泛。最常见的是组织代码,比如把一个类的不同功能分散到多个扩展中,让代码结构更清晰。还可以用来添加协议遵循,特别是标准库协议像 Equatable、Codable。另外就是扩展系统类型,添加项目特定的便捷方法。

扩展和继承不同,扩展不能重写已有的方法,只能添加新功能。而且扩展在编译期静态分派,没有运行时开销,性能很好。

16.Swift 中的枚举有什么特别之处?什么是关联值?

回答重点

Swift 的枚举比其他语言强大得多,不仅仅是定义一组常量,它是一等公民类型,功能非常丰富。

Swift 枚举可以有关联值、原始值、方法、计算属性,甚至可以遵循协议。这让枚举不只是简单的状态标识,而是可以承载数据和逻辑的完整类型。

关联值是 Swift 枚举的特色功能,可以让每个枚举成员关联不同类型的值。比如定义一个二维码类型,可以是字符串码也可以是数字码,用关联值就能把不同类型的数据和枚举成员绑定在一起。

关联值和原始值不同。原始值是所有成员共享同一种类型,每个成员都有一个预设的值。关联值是每次创建枚举实例时动态指定的,不同成员可以关联不同类型的值。

Swift 的枚举还可以有方法,这让枚举能封装相关的逻辑。比如网络请求结果的枚举,可以有一个方法来处理不同的情况,代码更内聚。

可选类型 Optional 其实就是用枚举实现的,有 none 和 some(value) 两种情况,some 就是用关联值存储实际的值。理解了枚举的强大之处,就能理解 Swift 很多设计的巧妙之处。

17.Swift 中的构造器有哪些类型?convenience init 和 designated init 有什么区别?

回答重点

Swift 的构造器分为两种:指定构造器(Designated Initializer)和便利构造器(Convenience Initializer)。

指定构造器是类的主要构造器,负责初始化所有属性,必须调用父类的指定构造器完成继承链的初始化。每个类至少要有一个指定构造器,它是初始化的"主力军"。

便利构造器是辅助性的构造器,用 convenience 关键字标记,为类提供额外的初始化方式。便利构造器必须调用同一个类中的其他构造器(最终会调用到指定构造器),不能直接调用父类的构造器。

两者的区别可以总结为:指定构造器向上代理(调用父类构造器),便利构造器横向代理(调用同类构造器)。这种设计保证了初始化的安全性,确保所有属性都被正确初始化。

指定构造器通常比较少,提供最基本最通用的初始化方式。便利构造器可以有多个,提供各种快捷初始化方式,内部调用指定构造器并提供默认参数。

结构体没有这个区分,所有构造器都是指定构造器。这是类特有的概念,因为类有继承,需要确保继承链上所有类的属性都正确初始化。

18.Swift 中如何实现多态?override 关键字的作用是什么?

回答重点

Swift 中实现多态主要有两种方式:类继承和协议。

类继承的多态是传统的面向对象多态。子类继承父类,可以重写父类的方法,当用父类类型引用指向子类实例时,调用重写的方法会执行子类的实现。这就是多态------同一个接口,不同的实现。

协议的多态更灵活。不同的类型只要遵循相同的协议,就可以用协议类型统一处理,调用协议方法时会执行各自的实现。这种多态不依赖继承关系,更加解耦。

override 关键字用于标记子类重写父类的方法、属性或下标。它的作用有两个:一是明确告诉编译器这是重写而不是新定义,二是让编译器检查父类是否真的有这个成员,避免拼写错误。

重写时必须写 override,否则编译器会报错。如果写了 override 但父类没有对应成员,编译器也会报错。这种强制性的标记避免了意外重写或拼写错误,提高了代码安全性。

final 关键字和 override 相对,用于阻止重写。给方法、属性或整个类加上 final,子类就不能重写了。这在设计框架时很有用,可以保护关键方法不被改变。

19.Swift 的 ARC 自动引用计数是如何工作的?

回答重点

ARC(Automatic Reference Counting,自动引用计数)是 Swift 的自动内存管理机制,用于管理类实例的内存。

ARC 的工作原理很直接:每个类实例都有一个引用计数,表示有多少个引用指向它。当创建一个新引用指向实例时,引用计数加 1;当引用被销毁或指向其他对象时,引用计数减 1。当引用计数降为 0 时,说明没有任何引用指向这个实例了,ARC 会自动释放这块内存。

这个过程是在编译期自动插入的,开发者不需要手动调用 retain 和 release(这是 Objective-C 时代的事情了)。编译器会分析代码,在合适的位置插入增减引用计数的指令。

ARC 只管理类实例的内存,因为类是引用类型,存储在堆上。结构体和枚举是值类型,存储在栈上,作用域结束自动释放,不需要引用计数。

虽然 ARC 很智能,但也不是完美的。最大的问题是循环引用,两个对象互相强引用,导致引用计数永远不为 0,内存泄漏。这时候需要用 weak 或 unowned 打破循环。

20.Swift 中什么是循环引用?如何避免循环引用?

回答重点

循环引用是指两个或多个对象相互强引用,导致引用计数永远无法降为 0,造成内存泄漏。

最典型的情况是对象 A 强引用对象 B,对象 B 又强引用对象 A。这时候即使外部没有引用指向它们,两个对象的引用计数都至少是 1,ARC 无法释放它们,内存就泄漏了。

避免循环引用主要有三种方式:使用 weak 弱引用、使用 unowned 无主引用,或者打破引用关系。

weak 引用不会增加引用计数,而且当对象被释放时会自动设为 nil,所以是可选类型。适合引用可能在生命周期中变为 nil 的情况,最典型的就是 delegate。

unowned 引用也不增加引用计数,但不是可选类型,对象释放后不会自动置空。适合引用的生命周期始终不短于被引用对象的情况,比如父对象引用子对象时,子对象引用父对象可以用 unowned。

第三种方式是手动打破循环,在合适的时机把某个引用设为 nil。比如在 deinit 或者某个清理方法中。

21.Swift 中 weak 和 unowned 有什么区别?如何选择使用?

回答重点

weak 和 unowned 都是用来打破循环引用的弱引用方式,但它们有重要的区别。

weak 引用是可选类型,当被引用的对象释放时会自动设为 nil。这意味着使用 weak 引用时需要解包,而且要处理可能为 nil 的情况。weak 更安全,因为即使对象被释放了,访问 weak 引用也不会崩溃,只是得到 nil。

unowned 引用不是可选类型,像普通引用一样可以直接使用,但对象释放后不会自动置空。如果访问已经释放的 unowned 引用会导致崩溃。unowned 的优势是使用更方便,不需要解包,代码更简洁。

选择哪个的原则很简单:如果引用可能在生命周期中变为 nil,用 weak;如果引用在生命周期中肯定不为 nil,而且不会比被引用对象活得更久,用 unowned。

典型场景是 delegate 用 weak,因为 delegate 可能被设为 nil。而子对象引用父对象用 unowned,因为子对象不会比父对象活得更久,而且父对象肯定存在。

22.Swift 中的泛型是什么?为什么要使用泛型?

回答重点

泛型是一种编写灵活、可复用代码的方式,可以让函数、类、结构体、枚举适用于任何类型,而不是固定的某一个类型。

泛型的核心思想是用类型参数替代具体类型,在使用时再指定具体类型。这就像函数参数一样,函数定义时用形参,调用时传实参。泛型是"类型的参数"。

为什么要用泛型?最大的好处是避免重复代码。如果没有泛型,要实现一个交换两个 Int 的函数、交换两个 String 的函数、交换两个其他类型的函数,代码几乎一样但要写很多遍。有了泛型,一个函数就能处理所有类型。

泛型还提供类型安全。相比用 Any 类型,泛型在编译期就确定了类型,不需要运行时类型转换,更安全也更高效。编译器能做更多检查,避免类型错误。

Swift 标准库大量使用泛型。Array、Dictionary、Set、Optional 都是泛型类型,这让它们既灵活又类型安全。理解泛型是掌握 Swift 的关键之一。

23.Swift 中如何给泛型添加约束?associatedtype 是什么?

回答重点

泛型约束用来限制类型参数必须满足某些条件,比如遵循某个协议或者继承某个类。

添加约束的方式有两种。第一种是在类型参数后面用冒号指定,比如 <T: Equatable> 表示 T 必须遵循 Equatable 协议。如果有多个约束,用 & 连接,比如 <T: Equatable & Codable>

第二种是使用 where 子句,写在函数签名或类型定义后面,可以表达更复杂的约束。where 子句更灵活,可以约束关联类型、指定类型相等等。

associatedtype 是协议中的泛型机制,用来定义关联类型。因为协议本身不能直接使用泛型参数,所以用 associatedtype 来表示"某个类型,具体是什么由遵循协议的类型决定"。

关联类型让协议更灵活。比如定义一个容器协议,不同的容器存储不同类型的元素,用关联类型就能表达这种"元素类型由具体容器决定"的关系。

遵循带关联类型的协议时,可以用 typealias 显式指定关联类型,也可以让编译器根据实现自动推断。大多数情况下自动推断就够了。

24.Swift 中如何处理错误?throw、throws 和 try 有什么区别?

回答重点

Swift 使用 do-catch-throw 模式处理错误,这是一种结构化的错误处理机制。

throw 是抛出错误的关键字,用在函数内部,当遇到错误情况时用 throw 抛出一个遵循 Error 协议的错误值。

throws 用在函数声明中,标记这个函数可能抛出错误。调用 throws 函数时必须处理可能的错误,要么用 try 捕获,要么把错误继续向上抛。

try 是调用 throws 函数时使用的关键字,有三种变体:普通 try 配合 do-catch 使用,try? 把错误转为可选值,try! 强制认为不会出错(会抛错就崩溃)。

这三个关键字组成了完整的错误处理链条:函数内部用 throw 抛出错误,函数签名用 throws 声明可能抛错,调用时用 try 处理错误。编译器会强制检查,确保所有可能的错误都被处理了。

相比 Objective-C 的 NSError 指针或者返回 nil,Swift 的错误处理更清晰明确,编译器能做更多检查,不容易漏掉错误处理。

25.Swift 5.5 引入的 async/await 是什么?如何使用?

回答重点

async/await 是 Swift 5.5 引入的结构化并发特性,用于简化异步编程。

async 关键字标记一个函数是异步的,表示这个函数可能会挂起,等待某些操作完成后再继续。异步函数可以在等待期间释放线程,让线程去做其他事情,提高效率。

await 关键字用于调用异步函数,标记这是一个可能挂起的点。在 await 处,程序可能会暂停,等待异步操作完成。await 只能在异步上下文中使用,也就是异步函数内部或者 Task 里面。

相比传统的回调和 DispatchQueue,async/await 让异步代码看起来像同步代码,逻辑更清晰,避免了回调地狱。错误处理也更简单,可以直接用 try-catch,不需要在回调中传递错误。

async/await 和 GCD 不是替代关系,而是更高层的抽象。底层依然是多线程和任务调度,但开发者不需要关心这些细节,只需要标记哪些地方是异步的,编译器和运行时会处理好一切。

26.Swift 中的 Actor 是什么?它解决了什么问题?

回答重点

Actor 是 Swift 5.5 引入的新类型,用于保证并发安全,防止数据竞争。

Actor 类似于 class,可以有属性和方法,但有一个关键特性:对 actor 的访问是互斥的,同一时刻只有一个任务能访问 actor 的可变状态。这通过自动的同步机制实现,开发者不需要手动加锁。

Actor 解决的核心问题是数据竞争。在多线程环境下,如果多个线程同时读写同一块数据,可能导致数据不一致或崩溃。传统做法是用锁保护,但容易出错。Actor 把同步机制内置到类型系统中,编译器强制检查,更安全。

访问 actor 的属性和方法需要用 await,因为可能需要等待其他任务完成。这让并发访问变得显式,开发者能清楚知道哪里可能发生挂起。

Actor 特别适合管理共享的可变状态,比如缓存、计数器、状态管理器等。相比自己用锁保护的 class,actor 更简洁、更安全,编译器能帮你检查很多潜在问题。

27.Swift 中的属性包装器 Property Wrapper 是什么?如何自定义?

回答重点

属性包装器(Property Wrapper)是 Swift 5.1 引入的特性,用于在属性上添加通用的逻辑,比如验证、转换、存储等,避免重复代码。

属性包装器本质上是一个结构体、类或枚举,用 @propertyWrapper 标记,必须有一个 wrappedValue 属性。使用时在属性前加 @包装器名称,属性的读写会自动通过包装器的 wrappedValue 进行。

最典型的应用是 SwiftUI 中的 @State@Binding@Published 等,它们都是属性包装器。@State 让属性变化时自动刷新 UI,@Published 让属性变化时发出通知,这些逻辑都封装在包装器中。

自定义属性包装器很简单,定义一个类型,加上 @propertyWrapper 标记,实现 wrappedValue 属性即可。还可以提供 projectedValue 来暴露额外的功能,通过 $ 前缀访问。

属性包装器让代码更简洁、更声明式,把通用的属性逻辑抽取出来复用,这是 Swift 强大表达力的体现。

28.Swift 中的访问控制有哪些级别?它们有什么区别?

回答重点

Swift 有五种访问控制级别,从限制最严到最宽依次是:private、fileprivate、internal、public、open。

private 是最严格的,只能在定义的作用域内访问,比如类内部、结构体内部、扩展内部。同一个文件的其他类型也访问不了。

fileprivate 放宽到整个文件,同一个源文件中的所有代码都能访问,但其他文件不行。适合文件内的多个类型需要共享某些实现细节的场景。

internal 是默认级别,在整个模块内部都能访问。模块通常是一个 target,比如你的 App、一个 Framework。同一模块内随便用,但模块外不行。

public 可以被其他模块访问,但不能被继承或重写。适合提供给外部使用但不想被修改的 API。

open 是最开放的,不仅可以被其他模块访问,还能被继承和重写。适合设计给外部扩展的框架类。

选择访问级别的原则是:尽量限制,只暴露必要的接口。这样可以隐藏实现细节,降低耦合,让代码更容易维护和重构。

29.Swift 的高阶函数有哪些?map、filter、reduce 分别做什么?

回答重点

高阶函数是接收函数作为参数或返回函数的函数,Swift 的集合类型提供了很多实用的高阶函数。

map 用于转换,对集合中的每个元素应用一个转换函数,返回转换后的新集合。比如把整数数组的每个元素乘以 2,或者把对象数组转换为字符串数组,都可以用 map。

filter 用于筛选,根据条件过滤集合中的元素,只保留满足条件的元素。比如从数组中筛选出大于 10 的数字,或者筛选出符合某个条件的对象。

reduce 用于归约,把集合中的所有元素组合成一个值。比如求和、求积、拼接字符串等。reduce 接收一个初始值和一个组合函数,依次处理每个元素,最终返回一个结果。

除了这三个,还有 compactMap(过滤 nil)、flatMap(展平嵌套)、forEach(遍历)、sorted(排序)、contains(包含判断)等常用高阶函数。

使用高阶函数能让代码更简洁、更声明式,避免手写循环,提高可读性。而且这些函数都是链式调用,可以组合使用,实现复杂的数据处理逻辑。

30.Swift 和 Objective-C 如何混编?需要注意什么问题?

回答重点

Swift 和 Objective-C 可以在同一个项目中无缝混编,这是苹果为了平滑过渡设计的重要特性。

从 Swift 调用 Objective-C 代码,需要创建一个桥接头文件(Bridging Header),在里面 import 需要使用的 Objective-C 头文件。Xcode 会在第一次添加不同语言文件时自动提示创建,文件名通常是 项目名-Bridging-Header.h

从 Objective-C 调用 Swift 代码,需要 import 自动生成的 Swift 头文件,格式是 项目名-Swift.h。这个文件是编译器自动生成的,包含了所有暴露给 Objective-C 的 Swift 接口。Swift 的类需要继承 NSObject,方法需要加 @objc 才能被 Objective-C 看到。

混编时需要注意几个问题。首先是类型转换,Swift 的很多类型在 Objective-C 中没有对应,比如元组、泛型、可选类型的内部机制。其次是命名规范,Objective-C 没有命名空间,Swift 的命名空间概念在 Objective-C 中会变成类名前缀。再就是协议,Swift 的协议有些特性 Objective-C 不支持。

总体来说,常见的 API 都能很好地互操作,但 Swift 的高级特性不能暴露给 Objective-C。混编适合渐进式迁移老项目,或者使用 Objective-C 写的第三方库。

相关推荐
得一录1 小时前
TradingAgents金融股票分析的最小实现
开发语言·数据库·人工智能·python
迷途酱1 小时前
Swift 真的被搞得乱七八糟了吗?写了几年之后说点实话
ios·swift
yuanpan1 小时前
Python 与 Conda 编程实战指南:从环境配置到项目运行完整入门
开发语言·python·conda
水木流年追梦1 小时前
大模型入门-应用篇1-prompt技术
开发语言·python·算法·prompt
莫生灬灬1 小时前
ElementUI封装 共91个组件 支持易语言/火山/C#/Python
开发语言·c++·python·ui·elementui·c#
Brilliantwxx1 小时前
【C++】stack_queue与deque模版(模拟实现+认识+对比)
开发语言·c++·笔记·算法·list
ch.ju1 小时前
Java Programming Chapter 3——Subscript of the array
java·开发语言
雨落在了我的手上1 小时前
初识java(三):运算符
java·开发语言
爱喝水的鱼丶1 小时前
SAP-ABAP:ABAP Development Tools(ADT)安装配置学习分享教程(四篇连载)第四篇:ADT连接故障排查与环境迁移教程
运维·开发语言·数据库·学习·sap·abap