Swift 中的指針使用
Apple 期望在 Swift 中指針能夠盡量減少登場幾率,因此在 Swift 中指針被映射為了一個泛型類型,并且還比較抽象。這在一定程度上造成了在 Swift 中指針使用的困難,特別是對那些并不熟悉指針,也沒有多少指針操作經(jīng)驗的開發(fā)者 (包括我自己也是) 來說,在 Swift 中使用指針確實是一個挑戰(zhàn)。在這篇文章里,我希望能從最基本的使用開始,總結(jié)一下在 Swift 中使用指針的一些常見方式和場景。這篇文章假定你至少知道指針是什么,如果對指針本身的概念不太清楚的話,可以先看看這篇五分鐘 C 指針教程 (或者它的中文版本),應(yīng)該會很有幫助。
初步
在 Swift 中,指針都使用一個特殊的類型來表示,那就是 UnsafePointer<T>。遵循了 Cocoa 的一貫不可變原則,UnsafePointer<T> 也是不可變的。當(dāng)然對應(yīng)地,它還有一個可變變體,UnsafeMutablePointer<T>。絕大部分時間里,C 中的指針都會被以這兩種類型引入到 Swift 中:C 中 const 修飾的指針對應(yīng) UnsafePointer (最常見的應(yīng)該就是 C 字符串的 const char * 了),而其他可變的指針則對應(yīng) UnsafeMutablePointer。除此之外,Swift 中存在表示一組連續(xù)數(shù)據(jù)指針的 UnsafeBufferPointer<T>,表示非完整結(jié)構(gòu)的不透明指針 COpaquePointer 等等。另外你可能已經(jīng)注意到了,能夠確定指向內(nèi)容的指針類型都是泛型的 struct,我們可以通過這個泛型來對指針指向的類型進行約束以提供一定安全性。
對于一個 UnsafePointer<T> 類型,我們可以通過 memory 屬性對其進行取值,如果這個指針是可變的 UnsafeMutablePointer<T> 類型,我們還可以通過 memory 對它進行賦值。比如我們想要寫一個利用指針直接操作內(nèi)存的計數(shù)器的話,可以這么做:
- func incrementor(ptr: UnsafeMutablePointer<Int>) {
- ptr.memory += 1
- }
- var a = 10
- incrementor(&a)
- a // 11
這里和 C 的指針使用類似,我們通過在變量名前面加上 & 符號就可以將指向這個變量的指針傳遞到接受指針作為參數(shù)的方法中去。在上面的 incrementor 中我們通過直接操作 memory 屬性改變了指針指向的內(nèi)容。
與這種做法類似的是使用 Swift 的 inout 關(guān)鍵字。我們在將變量傳入 inout 參數(shù)的函數(shù)時,同樣也使用 & 符號表示地址。不過區(qū)別是在函數(shù)體內(nèi)部我們不需要處理指針類型,而是可以對參數(shù)直接進行操作。
- func incrementor1(inout num: Int) {
- num += 1
- }
- var b = 10
- incrementor1(&b)
- b // 11
雖然 & 在參數(shù)傳遞時表示的意義和 C 中一樣,是某個“變量的地址”,但是在 Swift 中我們沒有辦法直接通過這個符號獲取一個 UnsafePointer 的實例。需要注意這一點和 C 有所不同:
- // 無法編譯
- let a = 100
- let b = &a
指針初始化和內(nèi)存管理
在 Swift 中不能直接取到現(xiàn)有對象的地址,我們還是可以創(chuàng)建新的 UnsafeMutablePointer 對象。與 Swift 中其他對象的自動內(nèi)存管理不同,對于指針的管理,是需要我們手動進行內(nèi)存的申請和釋放的。一個 UnsafeMutablePointer 的內(nèi)存有三種可能狀態(tài):
- 內(nèi)存沒有被分配,這意味著這是一個 null 指針,或者是之前已經(jīng)釋放過
- 內(nèi)存進行了分配,但是值還沒有被初始化
- 內(nèi)存進行了分配,并且值已經(jīng)被初始化
其中只有第三種狀態(tài)下的指針是可以保證正常使用的。UnsafeMutablePointer 的初始化方法 (init) 完成的都是從其他類型轉(zhuǎn)換到 UnsafeMutablePointer 的工作。我們?nèi)绻胍陆ㄒ粋€指針,需要做的是使用 alloc: 這個類方法。該方法接受一個 num: Int 作為參數(shù),將向系統(tǒng)申請 num 個數(shù)的對應(yīng)泛型類型的內(nèi)存。下面的代碼申請了一個 Int 大小的內(nèi)存,并返回指向這塊內(nèi)存的指針:
- var intPtr = UnsafeMutablePointer<Int>.alloc(1)
- // "UnsafeMutablePointer(0x7FD3A8E00060)"
接下來應(yīng)該做的是對這個指針的內(nèi)容進行初始化,我們可以使用 initialize: 方法來完成初始化:
- intPtr.initialize(10)
- // intPtr.memory 為 10
在完成初始化后,我們就可以通過 memory 來操作指針指向的內(nèi)存值了。
在使用之后,我們***盡快釋放指針指向的內(nèi)容和指針本身。與 initialize: 配對使用的 destroy 用來銷毀指針指向的對象,而與 alloc: 對應(yīng)的 dealloc: 用來釋放之前申請的內(nèi)存。它們都應(yīng)該被配對使用:
- intPtr.destroy()
- intPtr.dealloc(1)
- intPtr = nil
注意其實在這里對于 Int 這樣的在 C 中映射為 int 的 “平凡值” 來說,destroy 并不是必要的,因為這些值被分配在常量段上。但是對于像類的對象或者結(jié)構(gòu)體實例來說,如果不保證初始化和摧毀配對的話,是會出現(xiàn)內(nèi)存泄露的。所以沒有特殊考慮的話,不論內(nèi)存中到底是什么,保證 initialize: 和 destroy 配對會是一個好習(xí)慣。
指向數(shù)組的指針
在 Swift 中將一個數(shù)組作為參數(shù)傳遞到 C API 時,Swift 已經(jīng)幫助我們完成了轉(zhuǎn)換,這在 Apple 的官方博客中有個很好的例子:
- import Accelerate
- let a: [Float] = [1, 2, 3, 4]
- let b: [Float] = [0.5, 0.25, 0.125, 0.0625]
- var result: [Float] = [0, 0, 0, 0]
- vDSP_vadd(a, 1, b, 1, &result, 1, 4)
- // result now contains [1.5, 2.25, 3.125, 4.0625]
對于一般的接受 const 數(shù)組的 C API,其要求的類型為 UnsafePointer,而非 const 的數(shù)組則對應(yīng) UnsafeMutablePointer。使用時,對于 const 的參數(shù),我們直接將 Swift 數(shù)組傳入 (上例中的 a 和 b);而對于可變的數(shù)組,在前面加上 & 后傳入即可 (上例中的 result)。
對于傳參,Swift 進行了簡化,使用起來非常方便。但是如果我們想要使用指針來像之前用 memory 的方式直接操作數(shù)組的話,就需要借助一個特殊的類型:UnsafeMutableBufferPointer。Buffer Pointer 是一段連續(xù)的內(nèi)存的指針,通常用來表達像是數(shù)組或者字典這樣的集合類型。
- var array = [1, 2, 3, 4, 5]
- var arrayPtr = UnsafeMutableBufferPointer<Int>(start: &array, count: array.count)
- // baseAddress 是***個元素的指針
- var basePtr = arrayPtr.baseAddress as UnsafeMutablePointer<Int>
- basePtr.memory // 1
- basePtr.memory = 10
- basePtr.memory // 10
- //下一個元素
- var nextPtr = basePtr.successor()
- nextPtr.memory // 2
指針操作和轉(zhuǎn)換
withUnsafePointer
上面我們說過,在 Swift 中不能像 C 里那樣使用 & 符號直接獲取地址來進行操作。如果我們想對某個變量進行指針操作,我們可以借助 withUnsafePointer 這個輔助方法。這個方法接受兩個參數(shù),***個是 inout 的任意類型,第二個是一個閉包。Swift 會將***個輸入轉(zhuǎn)換為指針,然后將這個轉(zhuǎn)換后的 Unsafe 的指針作為參數(shù),去調(diào)用閉包。使用起來大概是這個樣子:
- var test = 10
- test = withUnsafeMutablePointer(&test, { (ptr: UnsafeMutablePointer<Int>) -> Int in
- ptr.memory += 1
- return ptr.memory
- })
- test // 11
這里其實我們做了和文章一開始的 incrementor 相同的事情,區(qū)別在于不需要通過方法的調(diào)用來將值轉(zhuǎn)換為指針。這么做的好處對于那些只會執(zhí)行一次的指針操作來說是顯而易見的,可以將“我們就是想對這個指針做點事兒”這個意圖表達得更加清晰明確。
unsafeBitCast
unsafeBitCast 是非常危險的操作,它會將一個指針指向的內(nèi)存強制按位轉(zhuǎn)換為目標的類型。因為這種轉(zhuǎn)換是在 Swift 的類型管理之外進行的,因此編譯器無法確保得到的類型是否確實正確,你必須明確地知道你在做什么。比如:
- let arr = NSArray(object: "meow")
- let str = unsafeBitCast(CFArrayGetValueAtIndex(arr, 0), CFString.self)
- str // “meow”
因為 NSArray 是可以存放任意 NSObject 對象的,當(dāng)我們在使用 CFArrayGetValueAtIndex 從中取值的時候,得到的結(jié)果將是一個 UnsafePointer<Void>。由于我們很明白其中存放的是 String 對象,因此可以直接將其強制轉(zhuǎn)換為 CFString。
關(guān)于 unsafeBitCast 一種更常見的使用場景是不同類型的指針之間進行轉(zhuǎn)換。因為指針本身所占用的的大小是一定的,所以指針的類型進行轉(zhuǎn)換是不會出什么致命問題的。這在與一些 C API 協(xié)作時會很常見。比如有很多 C API 要求的輸入是 void *,對應(yīng)到 Swift 中為 UnsafePointer<Void>。我們可以通過下面這樣的方式將任意指針轉(zhuǎn)換為 UnsafePointer。
- var count = 100
- var voidPtr = withUnsafePointer(&count, { (a: UnsafePointer<Int>) -> UnsafePointer<Void> in
- return unsafeBitCast(a, UnsafePointer<Void>.self)
- })
- // voidPtr 是 UnsafePointer<Void>。相當(dāng)于 C 中的 void *
- // 轉(zhuǎn)換回 UnsafePointer<Int>
- var intPtr = unsafeBitCast(voidPtr, UnsafePointer<Int>.self)
- intPtr.memory //100
總結(jié)
Swift 從設(shè)計上來說就是以安全作為重要原則的,雖然可能有些啰嗦,但是還是要重申在 Swift 中直接使用和操作指針應(yīng)該作為***的手段,它們始終是無法確保安全的。從傳統(tǒng)的 C 代碼和與之無縫配合的 Objective-C 代碼遷移到 Swift 并不是一件小工程,我們的代碼庫肯定會時不時出現(xiàn)一些和 C 協(xié)作的地方。我們當(dāng)然可以選擇使用 Swift 重寫部分陳舊代碼,但是對于像是安全或者性能至關(guān)重要的部分,我們可能除了繼續(xù)使用 C API 以外別無選擇。如果我們想要繼續(xù)使用那些 API 的話,了解一些基本的 Swift 指針操作和使用的知識會很有幫助。
對于新的代碼,盡量避免使用 Unsafe 開頭的類型,意味著可以避免很多不必要的麻煩。Swift 給開發(fā)者帶來的***好處是可以讓我們用更加先進的編程思想,進行更快和更專注的開發(fā)。只有在尊重這種思想的前提下,我們才能更好地享受這門新語言帶來的種種優(yōu)勢。顯然,這種思想是不包括到處使用 UnsafePointer 的 :)