Swift问答

目录

1. class 和 struct 的区别
2. 不通过继承,代码复用(共享)的方式有哪些
3. Set 独有的方法有哪些?
4. 实现一个 min 函数,返回两个元素较小的元素
5. map、filter、reduce 的作用
6. map 与 flatmap 的区别
7. 什么是 copy on write
8. 如何获取当前代码的函数名和行号
9. 如何声明一个只能被类 conform 的 protocol
10. guard 使用场景
11. defer 使用场景
12. String 与 NSString 的关系与区别
13. 怎么获取一个 String 的长度
14. 如何截取 String 的某段字符串
15. throws 和 rethrows 的用法与作用
16. try? 和 try!是什么意思
17. associatedtype 的作用
18. 什么时候使用 final
19. public 和 open 的区别
20. 声明一个只有一个参数没有返回值闭包的别名
21. Self 的使用场景
22. dynamic 的作用
23. 什么时候使用 @objc
24. Optional(可选型) 是用什么实现的
25. 如何自定义下标获取
26. ?? 的作用
27. lazy 的作用
28. 一个类型表示选项,可以同时表示有几个选项选中(类似 UIViewAnimationOptions ),用什么类型表示
29. inout 的作用
30. Error 如果要兼容 NSError 需要做什么操作
31. 下面的代码都用了哪些语法糖
[1, 2, 3].map{ $0 * 2 }
32. 什么是高阶函数
33. 如何解决引用循环
34. 下面的代码会不会崩溃,说出原因
var mutableArray = [1,2,3]
for _ in mutableArray {
  mutableArray.removeLast()
}
35. 给集合中元素是字符串的类型增加一个扩展方法,应该怎么声明
36. 定义静态方法时关键字 static 和 class 有什么区别
37.Swift中解除循环引用的三种方法
38.Swift与Objective-C混编
39. Swift 派发机制

1.class 和 struct 的区别

  • 相同点: 我们可以使用完全使用相同的语法规则来为 class 和 struct 定义属性、方法、下标操作、构造器,也可以通过extension 和 protocol 来提供某种功能。
  • 不同点:
    • 1)与 struct 相比,class 还有如下功能:
      继承允许一个类继承另一个类的特性
      类型转换允许在运行时检查和解释一个类实例的类型
      析构器允许一个类实例释放任何其所被分配的资源
      引用计数允许对一个类的多次引用
      1. Value Types & Reference Types:
        struct 是值类型 (Value Types) 即:通过被复制的方式在代码中传递,不使用引用计数
        class 是引用类型(Reference Types) 即:引用类型在被赋予到一个变量、常量或者被传递到一个函数时,其值不会被拷贝。因此,引用的是已存在的实例本身而不是其拷贝

2. 不通过继承,代码复用(共享)的方式有哪些

在Swift中,除了通过继承,还可以通过 扩展、协议 来实现代码复用。
扩展 --- Extensions

扩展 就是为一个已有的类、结构体、枚举类型或者协议类型添加新功能。这包括在没有权限获取原始源代码的情况下扩展类型的能力(即 逆向建模 )。扩展和 Objective-C 中的分类类似。

Swift 中的扩展可以:

添加计算型属性和计算型类型属性
定义实例方法和类型方法
提供新的构造器
定义下标
定义和使用新的嵌套类型
使一个已有类型符合某个协议

协议 --- Protocols

协议 规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现
另外,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现

3. Set 独有的方法有哪些

intersect(_:)// 根据两个集合中都包含的值创建的一个新的集合
exclusiveOr(_:) // 根据只在一个集合中但不在两个集合中的值创建一个新的集合
union(_:) // 根据两个集合的值创建一个新的集合
subtract(_:) //根据不在该集合中的值创建一个新的集合
isSubsetOf(_:) //判断一个集合中的值是否也被包含在另外一个集合中
isSupersetOf(_:) //判断一个集合中包含的值是否含有另一个集合中所有的值
isStrictSubsetOf(:) isStrictSupersetOf(:) //判断一个集合是否是另外一个集合的子集合或者父集合并且和特定集合不相等
isDisjointWith(_:) //判断两个集合是否不含有相同的值

4.实现一个 min 函数,返回两个元素较小的元素

func min<T: Comparable>(_ a: T, _ b: T) -> T {
    return a < b ? a: b
}
//这里一定要遵守 Comparable 协议,因为并不是所有的类型都具有“可比性”

5.map、filter、reduce 的作用

  • map 是Array类的一个方法,我们可以使用它来对数组的每个元素进行转换
let intArray = [1, 3, 5]
let stringArr = intArray.map {
          return "\($0)"
      }
// ["1", "3", "5"]
  • filter 用于选择数组元素中满足某种条件的元素
let filterArr = intArray.filter {
  return $0 > 1
}
//[3, 5]
  • reduce 把数组元素组合计算为一个值
let result = intArray.reduce(0) {
  return $0 + $1
}
//9

6.map 与 flatmap 的区别

  • map 可以对一个集合类型的所有元素做一个映射操作
    和map 不同,flatmap 有两个定义,分别是:
    func flatMap(transform: (Self.Generator.Element) throws -> T?) -> [T]
    func flatMap(transform: (Self.Generator.Element) -> S) -> [S.Generator.Element]
    let intArray = [1, 2, 3, 4]
    result = intArray.flatMap { $0 + 2 }
    // [3,4,5,6]

  • 对数组进行flatmap操作,和map是没有区别的

  1. 第一种情况返回值类型是 T?, 实际应用中,可以用来过滤元素为nil的情况,(内部使用了 if-let 来对nil值进行了过滤) 例如:
let optionalArray: [String?] = ["AA", nil, "BB", "CC"];
var optionalResult = optionalArray.flatMap{ $0 }
// ["AA", "BB", "CC"]
操作前是[String?], 操作后会变成[String]
  1. 第二种情况可以进行“降维”操作
let numbersCompound = [[1,2,3],[4,5,6]];
var res = numbersCompound.map { $0.map{ $0 + 2 } }
// [[3, 4, 5], [6, 7, 8]]
var flatRes = numbersCompound.flatMap { $0.map{ $0 + 2 } }
// [3, 4, 5, 6, 7, 8]

0.map{0 + 2 } 会得到[3, 4, 5], [6, 7, 8], 然后遍历这两个数组,将遍历的元素拼接到一个新的数组内,最终并返回就得到了[3, 4, 5, 6, 7, 8]

7.什么是 copy on write

copy on write, 写时复制,简称COW,它通过浅拷贝(shallow copy)只复制引用而避免复制值;当的确需要进行写入操作时,首先进行值拷贝,在对拷贝后的值执行写入操作,这样减少了无谓的复制耗时。

应用场景 :
写时复制最擅长的是并发读取场景,即多个线程/进程可以通过对一份相同快照,去处理实效性要求不是很高但是仍然要做的业务(比如实现
FS\DB备份、日志、分析)
适用于对象空间占用大,修改次数少,而且对数据实效性要求不高的场景

8.如何获取当前代码的函数名和行号

获取函数名: #function
获取行号:#line
获取文件名: #file
获取列:#column

9.如何声明一个只能被类 conform 的 protocol

协议的继承列表中,通过添加 class 关键字来限制协议只能被类类型遵循,而结构体或枚举不能遵循该协议。class 关键字必须第一个出现在协议的继承列表中,在其他继承的协议之前:

protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
    // 这里是类类型专属协议的定义部分
}

10.guard 使用场景

  • 使用 guard 来表达 “提前退出”的意图,有以下 使用场景 :
    在验证入口条件时
    在成功路径上提前退出
    在可选值解包时(拍扁 if let..else 金字塔)
    return 和 throw 中
    日志、崩溃和断言中
  • 而下面则是尽量 避免使用 的场景:
    不要用 guard :替代琐碎的 if..else 语句
    不要用 guard :作为 if 的相反情况
    不要:在 guard 的 else 语句中放入复杂代码

11.defer 使用场景

defer 语句用于在退出当前作用域之前执行代码.例如:
手动管理资源时,比如 关闭文件描述符,或者即使抛出了错误也需要执行一些操作时,就可以使用 defer 语句。
如果多个 defer 语句出现在同一作用域内,那么它们执行的顺序与出现的顺序相反

func f() {
    defer { print("First") }
    defer { print("Second") }
    defer { print("Third") }
}
f()
// 打印 “Third”
// 打印 “Second”
// 打印 “First”

12. String 与 NSString 的关系与区别

Swift 的String类型与 Foundation NSString类进行了无缝桥接。他们最大的区别就是:String是值类型,而NSString是引用类型。
其他方面的差异就体现在各自api 上的差异。

13.怎么获取一个 String 的长度

let length1 = "string".characters.count
let length2 = "string".data(using: .utf8).count
let length3 = ("string" as NSString).length

14. 如何截取 String 的某段字符串

每一个String值都有一个关联的索引(index)类型,String.Index,它对应着字符串中的每一个Character的位置

15.throws 和 rethrows 的用法与作用

  • throw异常,这表示这个函数可能会抛出异常,无论作为参数的闭包是否抛出异常
  • rethrow异常,这表示这个函数本身不会抛出异常,但如果作为参数的闭包抛出了异常,那么它会把异常继续抛上去
public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]

16. try? 和 try!是什么意思

  • try? 是用来修饰一个可能会抛出错误的函数。会将错误转换为可选值,当调用try?+函数或方法语句时候,如果函数或方法抛出错误,程序不会发崩溃,而返回一个nil,如果没有抛出错误则返回可选值
  • try! 会忽略错误传递链,并声明“do or die”。如果被调用函数或方法没有抛出异常,那么一切安好;但是如果抛出异常,二话不说,给你崩

17. associatedtype 的作用

Swift中的协议(protocol)采用的是“Associated Types”的方式来实现泛型功能的

18. 什么时候使用 final

  • 通过使用final提升程序性能,其实就算把所有不需要继承的方法、类都加上final关键字,效果也是微乎其微。
  • final 的正确使用场景-----权限控制, 具体情况如下:
    1. 类或者方法的功能确实已经完备了
      通常是一些辅助性质的工具类或者方法,比如MD5加密类这种,算法都十分固定,我们基本不会再继承和重写
    2. 避免子类继承和修改造成危险
    3. 为了让父类中某些代码一定会执行

19. public 和 open 的区别

  • open
    open 修饰的 class 在 Module 内部和外部都可以被访问和继承
    open 修饰的 func 在 Module 内部和外部都可以被访问和重载(override)
  • public
    public 修饰的 class 在 Module 内部可以访问和继承,在外部只能访问
    public 修饰的 func 在 Module 内部可以被访问和重载(override),在外部只能访问

20. 声明一个只有一个参数没有返回值闭包的别名

typealias IntBlock = (Int) -> Void

21. Self 的使用场景

  • 协议中声明
    protocol Hello {
    func hello() -> Self
    }
  • 协议扩展
    protocol MyProtocol { }
    extension MyProtocol where Self: UIView { }

22. dynamic 的作用

dynamic 可以用来修饰变量或函数,告诉编译器使用动态分发而不是静态分发。
使用动态分发,可以更好的与OC中runtime的一些特性(如CoreData,KVC/KVO)进行交互
标记为dynamic的变量/函数会隐式的加上@objc关键字,它会使用OC的runtime机制

23. 什么时候使用 @objc

在协议中使用 optional 关键字作为前缀来定义可选要求。可选要求用在你需要和 Objective-C 打交道的代码中。协议和可选要求都必须带上@objc属性。标记 @objc 特性的协议只能被继承自 Objective-C 类的类或者 @objc 类遵循,其他类以及结构体和枚举均不能遵循这种协议

24. Optional(可选型) 是用什么实现的

Optional 是个枚举。有两个枚举成员,Some(T) 和 None
通关泛型来兼容所有类型

25. 如何自定义下标获取

使用subscript语法

struct TimesTable {
    let multiplier: Int
    subscript(index: Int) -> Int {
        return multiplier * index
    }
}
let threeTimesTable = TimesTable(multiplier: 3)
threeTimesTable[6]  //18

26. ?? 的作用

?? 是空合运算符。
比如a ?? b ,将对可选类型a进行为空判断,如果a包含一个值,就进行解封,否则就返回一个默认值b。
表达式 a 必须是 Optional 类型。默认值 b 的类型必须要和 a 存储值的类型保持一致

27. lazy 的作用

使用lazy关键字修饰struct 或class 的成员变量,达到懒加载的效果。一般有以下使用场景:
属性开始时,还不确定是什么活着还不确定是否被用到
属性需要复杂的计算,消耗大量的CPU
属性只需要初始化一次

28. 一个类型表示选项,可以同时表示有几个选项选中(类似 UIViewAnimationOptions ),用什么类型表示

使用选项集合:OptionSet

29. inout 的作用

可以让值类型以引用方式传递,比如有时需要通过一个函数改变函数外面变量的值,例如:

var value = 50
print(value)  // 此时value值为50

func increment(inout value: Int, length: Int = 10) {
    value += length
}
increment(&value)
print(value)  // 此时value值为60,成功改变了函数外部变量value的值

30. Error 如果要兼容 NSError 需要做什么操作

想让我们的自定义Error可以转成NSError,实现CustomNSError就可以完整的as成NSError

/// Describes an error type that specifically provides a domain, code,
/// and user-info dictionary.
public protocol CustomNSError : Error {

    /// The domain of the error.
    public static var errorDomain: String { get }

    /// The error code within the given domain.
    public var errorCode: Int { get }

    /// The user-info dictionary.
    public var errorUserInfo: [String : Any] { get }
}

31. 下面的代码都用了哪些语法糖

[1, 2, 3].map{ 0 * 2 } 尾随闭包(Trailing Closures), 如果函数的最后一个参数是闭包,则可以省略 () 如果该闭包只有一行,则可以省略 return 类型推断,返回值被推断为Int0 代表集合的元素

32. 什么是高阶函数

接受一个或多个函数作为参数
把一个函数当作返回值
例如Swift中的map flatMap filter reduce

33. 如何解决循环引用

可以使用 weak 和 unowned
在引用对象的生命周期内,如果它可能为nil,那么就用weak引用。反之,当你知道引用对象在初始化后永远都不会为nil就用unowned

34. 下面的代码会不会崩溃,说出原因

var mutableArray = [1,2,3]
for _ in mutableArray {
mutableArray.removeLast()
}
不会崩溃。迭代器?

35. 给集合中元素是字符串的类型增加一个扩展方法,应该怎么声明

extension Sequence where Iterator.Element == Int {
    //your code
}
protocol SomeProtocol {}
extension Collection where Iterator.Element: SomeProtocol {
    //your code
}

36. 定义静态方法时关键字 static 和 class 有什么区别

static 和 class都是用来指定类方法
class关键字指定的类方法 可以被 override
static关键字指定的类方法 不能被 override

37.Swift中解除循环引用的三种方法

Swift在闭包的使用过程中有可能会引起循环引用, 这和OC中的循环引用是类似的.
现在假设我有两个控制器, 点击第一个控制器的跳转会跳转到第二个控制器, 第二个控制器pop回来的时候会deinit;

  • 方法一: OC的方法, 在OC中,可以是有__weak typeof(self)weakSelf = self;
    在Swift中, 也有类似的方法:
    override func viewDidLoad() {
        super.viewDidLoad()

        /* 方法一, OC的方法 */
        weak var weakSelf = self

        demo { (string) in
            if let wSelf = weakSelf {
                print("\(string)----\(wSelf.view)")
            }
        }
        secondDemo()
    }

Swift中就是weak var weakSelf = self这句代码

  • 方法二: [weak self]修饰闭包
    override func viewDidLoad() {
        super.viewDidLoad()

        /* 方法二: [weak self]修饰闭包 */

        demo {[weak self] (string) in
            if let weakSelf = self {
                print("\(string)----\(weakSelf.view)")
            }

        }
        secondDemo()
    }
  • 方法三: [unowned self]修饰闭包
 super.viewDidLoad()

        /* 方法三: [unowned self]修饰闭包 */

        demo {[unowned self] (string) in

            print("\(string)----\(self.view)")
        }
        secondDemo()
    }

注意: 方法二和方法三的区别在于, 当方法二中的控制器被销毁时, self指针会指向nil, 而当方法三中的控制器被销毁时, self指针是不会指向nil的,这时候就会形成野指针, 因此, 第三种方法解除循环引用是不推荐的, 有可能会引起一些问题;

38.Swift与Objective-C混编

Swift调用Objective-C

Swift调用Objective-C文件比较简单。当在Swift工程中新建Objective-C文件或者在Objective-C工程中新建Swift文件时,Xcode会自动提示你是否创建bridging header桥接头文件,点击创建后Xcode会自动为你创建一个桥接头文件。
当然你也可以在Building Settings中自己设置桥接头文件(一般情况下我们会用系统默认生成的)
创建好Bridging Header文件后,在Bridging Header文件中即可import需要提供给Swift的Objective-C头文件,Swift即可调用对应的Objective-C文件。
同时Xcode可以自动生成Objective-C对应的Swift接口。

Objective-C源代码对应的头文件

// 宏定义
#define DefaultHeight 100.f

// 协议
NS_ASSUME_NONNULL_BEGIN
@protocol OCViewControllerDelegate <NSObject>
- (void)detailButtonDidPressed:(id)sender;
@end

@interface OCViewController : UIViewController

// 属性
@property (nonatomic, weak) id<OCViewControllerDelegate> delegate;
@property (nonatomic, strong) NSString *testString;
@property (nonatomic, strong) NSArray  *testArray;
@property (nonatomic, strong, nullable) NSArray<NSString *> *testArray2;
@property (nonatomic, strong) NSNumber *testNumber;

// 方法
- (void)testMethod1;
- (BOOL)testMethod2WithParam:(NSString *)aParam;
- (NSString *)testMethod3WithParam:(NSArray *)aArray;
- (nullable NSString *)testMethod4WithParam:(nullable NSArray*)aArray;
@end
NS_ASSUME_NONNULL_END

转换后对应的Swift interface文件如下

import UIKit

// 宏定义
public var DefaultHeight: Float { get }

// 协议
public protocol OCViewControllerDelegate : NSObjectProtocol {
    public func detailButtonDidPressed(sender: AnyObject)
}

public class OCViewController : UIViewController {

    // 属性
    weak public var delegate: OCViewControllerDelegate?
    public var testString: String
    public var testArray: [AnyObject]
    public var testArray2: [String]?
    public var testNumber: NSNumber

    // 方法
    public func testMethod1()
    public func testMethod2WithParam(aParam: String) -> Bool
    public func testMethod3WithParam(aArray: [AnyObject]) -> String
    public func testMethod4WithParam(aArray: [AnyObject]?) -> String?
}

Objective-C调用Swift

Xcode会自动为Project生成头文件以便在Objective-C中调用。
在Objective-C类中调用Swift,只需要#import "productModuleName-Swift.h"即可调用,Xcode提供的头文件以Swift代码的模块名加上-Swift.h为命名。
在这个头文件中,将包含Swift提供给Objective-C的所有接口、Appdelegate及自动生成的一些宏定义;

Swift与C交互

有时候我们在Swift中可能需要与C交互(如Core Graphics),Swift对此提供了有效的支持。

  • 原始类型
    Swift提供了将Swift类型直接映射为C原始类型的“类C风格”类型。这些类型以C开头后跟C原始类型名。例如,bool -> CBool,unsigned long long -> CUnsignedLongLong。
  • 指针类型
    指针类型需要一些描述信息。例如,const void -> CConstVoidPointer, void -> CMutableVoidPointer或者COpaquePointer(两者区别在于用于参数还是函数返回值)。
    指向某一类型的指针
  • 类型化的指针
    可以用泛型语法CMutableVoidPointer<Type>,例如,Int* -> CMutableVoidPointer<Type>

Swift与C++交互

Objective-C能与C/C++很好的交互,目前Swift对C++的交互不是很好的支持(原因苹果认为C++是个很复杂的语言,与C++的交互性需要考虑很多东西,是件很长远的事情,至少在3.0及3.0版本之前Swift不支持),所以如果有些库需要与C++混编可以考虑用Objective-C作为桥接。

39. Swift 派发机制

函数派发就是程序判断使用哪种途径去调用一个函数的机制. 每次函数被调用时都会被触发, 但你又不会太留意的一个东西. 了解派发机制对于写出高性能的代码来说很有必要, 而且也能够解释很多 Swift 里"奇怪"的行为.

编译型语言有三种基础的函数派发方式: 直接派发(Direct Dispatch), 函数表派发(Table Dispatch) 和 消息机制派发(Message Dispatch), 下面我会仔细讲解这几种方式. 大多数语言都会支持一到两种, Java 默认使用函数表派发, 但你可以通过 final 修饰符修改成直接派发. C++ 默认使用直接派发, 但可以通过加上 virtual 修饰符来改成函数表派发. 而 Objective-C 则总是使用消息机制派发, 但允许开发者使用 C 直接派发来获取性能的提高. 这样的方式非常好, 但也给很多开发者带来了困扰,

程序派发的目的是为了告诉 CPU 需要被调用的函数在哪里

  • 直接派发 (Direct Dispatch)
    直接派发是最快的, 不止是因为需要调用的指令集会更少, 并且编译器还能够有很大的优化空间, 例如函数内联等, 但这不在这篇博客的讨论范围. 直接派发也有人称为静态调用.
    然而, 对于编程来说直接调用也是最大的局限, 而且因为缺乏动态性所以没办法支持继承.

  • 函数表派发 (Table Dispatch )
    函数表派发是编译型语言实现动态行为最常见的实现方式. 函数表使用了一个数组来存储类声明的每一个函数的指针. 大部分语言把这个称为 "virtual table"(虚函数表), Swift 里称为 "witness table". 每一个类都会维护一个函数表, 里面记录着类所有的函数, 如果父类函数被 override 的话, 表里面只会保存被 override 之后的函数. 一个子类新添加的函数, 都会被插入到这个数组的最后. 运行时会根据这一个表去决定实际要被调用的函数.
    举个例子, 看看下面两个类:

class ParentClass {
    func method1() {}
    func method2() {}
}
class ChildClass: ParentClass {
    override func method2() {}
    func method3() {}
}

在这个情况下, 编译器会创建两个函数表, 一个是 ParentClass 的, 另一个是 ChildClass的:
这张表展示了 ParentClass 和 ChildClass 虚数表里 method1, method2, method3 在内存里的布局.

let obj = ChildClass()
obj.method2()

当一个函数被调用时, 会经历下面的几个过程:
1.读取对象 0xB00 的函数表.
2.读取函数指针的索引. 在这里, method2 的索引是1(偏移量), 也就是 0xB00 + 1.
3.跳到 0x222 (函数指针指向 0x222)
查表是一种简单, 易实现, 而且性能可预知的方式. 然而, 这种派发方式比起直接派发还是慢一点. 从字节码角度来看, 多了两次读和一次跳转, 由此带来了性能的损耗. 另一个慢的原因在于编译器可能会由于函数内执行的任务导致无法优化. (如果函数带有副作用的话)
这种基于数组的实现, 缺陷在于函数表无法拓展. 子类会在虚数函数表的最后插入新的函数, 没有位置可以让 extension 安全地插入函数.

  • 消息机制派发 (Message Dispatch )
    消息机制是调用函数最动态的方式. 也是 Cocoa 的基石, 这样的机制催生了 KVO, UIAppearence 和 CoreData 等功能. 这种运作方式的关键在于开发者可以在运行时改变函数的行为. 不止可以通过 swizzling 来改变, 甚至可以用 isa-swizzling 修改对象的继承关系, 可以在面向对象的基础上实现自定义派发.
    当一个消息被派发, 运行时会顺着类的继承关系向上查找应该被调用的函数. 如果你觉得这样做效率很低, 它确实很低! 然而, 只要缓存建立了起来, 这个查找过程就会通过缓存来把性能提高到和函数表派发一样快. 但这只是消息机制的原理;

Swift 的派发机制

这里有四个选择具体派发方式的因素存在:
声明的位置
引用类型
特定的行为
显式地优化(Visibility Optimizations)
在解释这些因素之前, 我有必要说清楚, Swift 没有在文档里具体写明什么时候会使用函数表什么时候使用消息机制. 唯一的承诺是使用 dynamic 修饰的时候会通过 Objective-C 的运行时进行消息机制派发.

1.声明的位置 (Location Matters)
在 Swift 里, 一个函数有两个可以声明的位置: 类型声明的作用域, 和 extension. 根据声明类型的不同, 也会有不同的派发方式.

class MyClass {
    func mainMethod() {}
}
extension MyClass {
    func extensionMethod() {}
}

上面的例子里, mainMethod 会使用函数表派发, 而 extensionMethod 则会使用直接派发. 当我第一次发现这件事情的时候觉得很意外, 直觉上这两个函数的声明方式并没有那么大的差异.
总结起来有这么几点:

  • 值类型总是会使用直接派发, 简单易懂
  • 而协议和类的 extension 都会使用直接派发
  • NSObject 的 extension 会使用消息机制进行派发
  • NSObject 声明作用域里的函数都会使用函数表进行派发.
    协议里声明的, 并且带有默认实现的函数会使用函数表进行派发

2.引用类型 (Reference Type Matters)
引用的类型决定了派发的方式. 这很显而易见, 但也是决定性的差异. 一个比较常见的疑惑, 发生在一个协议拓展和类型拓展同时实现了同一个函数的时候.

protocol MyProtocol {
}
struct MyStruct: MyProtocol {
}
extension MyStruct {
    func extensionMethod() {
        print("结构体")
    }
}
extension MyProtocol {
    func extensionMethod() {
        print("协议")
    }
}

let myStruct = MyStruct()
let proto: MyProtocol = myStruct

myStruct.extensionMethod() // -> “结构体”
proto.extensionMethod() // -> “协议”

刚接触 Swift 的人可能会认为 proto.extensionMethod() 调用的是结构体里的实现. 但是, 引用的类型决定了派发的方式, 协议拓展里的函数会使用直接调用. 如果把 extensionMethod 的声明移动到协议的声明位置的话, 则会使用函数表派发, 最终就会调用结构体里的实现. 并且要记得, 如果两种声明方式都使用了直接派发的话, 基于直接派发的运作方式, 我们不可能实现预想的 override 行为. 这对于很多从 Objective-C 过渡过来的开发者是反直觉的.

3.指定派发方式 (Specifying Dispatch Behavior)
Swift 有一些修饰符可以指定派发方式.

  • final
    final 允许类里面的函数使用直接派发. 这个修饰符会让函数失去动态性. 任何函数都可以使用这个修饰符, 就算是 extension 里本来就是直接派发的函数. 这也会让 Objective-C 的运行时获取不到这个函数, 不会生成相应的 selector.
  • dynamic
    dynamic 可以让类里面的函数使用消息机制派发. 使用 dynamic, 必须导入 Foundation 框架, 里面包括了 NSObject 和 Objective-C 的运行时. dynamic 可以让声明在 extension 里面的函数能够被 override. dynamic 可以用在所有 NSObject 的子类和 Swift 的原声类.
  • @objc & @nonobjc
    @objc 和 @nonobjc 显式地声明了一个函数是否能被 Objective-C 的运行时捕获到. 使用 @objc 的典型例子就是给 selector 一个命名空间 @objc(abc_methodName), 让这个函数可以被 Objective-C 的运行时调用. @nonobjc 会改变派发的方式, 可以用来禁止消息机制派发这个函数, 不让这个函数注册到 Objective-C 的运行时里. 我不确定这跟 final 有什么区别, 因为从使用场景来说也几乎一样. 我个人来说更喜欢 final, 因为意图更加明显.
    译者注: 我个人感觉, 这这主要是为了跟 Objective-C 兼容用的, final 等原生关键词, 是让 Swift 写服务端之类的代码的时候可以有原生的关键词可以使用.
  • final @objc
    可以在标记为 final 的同时, 也使用 @objc 来让函数可以使用消息机制派发. 这么做的结果就是, 调用函数的时候会使用直接派发, 但也会在 Objective-C 的运行时里注册响应的 selector. 函数可以响应 perform(selector:) 以及别的 Objective-C 特性, 但在直接调用时又可以有直接派发的性能.
  • @inline
    Swift 也支持 @inline, 告诉编译器可以使用直接派发. 有趣的是, dynamic @inline(__always) func dynamicOrDirect() {} 也可以通过编译! 但这也只是告诉了编译器而已, 实际上这个函数还是会使用消息机制派发. 这样的写法看起来像是一个未定义的行为, 应该避免这么做.

推荐阅读更多精彩内容