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() {} 也可以通過編譯! 但這也只是告訴了編譯器而已, 實際上這個函數還是會使用消息機制派發. 這樣的寫法看起來像是一個未定義的行為, 應該避免這么做.
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容