Apple 指望在 Swift 中指針可以儘可能減小登場概率,所以在 Swift 中指針被映射爲了一個泛型類型,而且還比較抽象。這在必定程度上形成了在 Swift 中指針使用的困難,特別是對那些並不熟悉指針,也沒有多少指針操做經驗的開發者 (包括我本身也是) 來講,在 Swift 中使用指針確實是一個挑戰。在這篇文章裏,我但願能從最基本的使用開始,總結一下在 Swift 中使用指針的一些常見方式和場景。這篇文章假定你至少知道指針是什麼,若是對指針自己的概念不太清楚的話,能夠先看看這篇 五分鐘C指針教程(或者它的中文版本),應該會頗有幫助。
編程
初步數組
在 Swift 中,指針都使用一個特殊的類型來表示,那就是 UnsafePointer。遵循了 Cocoa 的一向不可變原則,UnsafePointer也是不可變的。固然對應地,它還有一個可變變體,UnsafeMutablePointer。 絕大部分時間裏,C 中的指針都會被以這兩種類型引入到 Swift 中:C 中 const 修飾的指針對應 UnsafePointer (最多見的應該就是 C 字符串的 const char * 了),而其餘可變的指針則對應 UnsafeMutablePointer。除此以外,Swift 中存在表示一組連續數據指針的 UnsafeBufferPointer,表示非完整結構的不透明指針 COpaquePointer 等等。另外你可能已經注意到了,可以肯定指向內容的指針類型都是泛型的 struct,咱們能夠經過這個泛型來對指針指向的類型進行約束以提供必定安全性。安全
對於一個 UnsafePointer類型,咱們能夠經過 memory 屬性對其進行取值,若是這個指針是可變的 UnsafeMutablePointer類型,咱們還能夠經過 memory 對它進行賦值。好比咱們想要寫一個利用指針直接操做內存的計數器的話,能夠這麼作:閉包
func incrementor(ptr: UnsafeMutablePointer) {
ptr.memory += 1
}
var a = 10
incrementor(&a)
a // 11
這裏和 C 的指針使用相似,咱們經過在變量名前面加上 & 符號就能夠將指向這個變量的指針傳遞到接受指針做爲參數的方法中去。在上面的 incrementor 中咱們經過直接操做 memory 屬性改變了指針指向的內容。ide
與這種作法相似的是使用 Swift 的 inout 關鍵字。咱們在將變量傳入 inout 參數的函數時,一樣也使用 & 符號表示地址。不過區別是在函數體內部咱們不須要處理指針類型,而是能夠對參數直接進行操做。函數
func incrementor1(inout num: Int) {
num += 1
}
var b = 10
incrementor1(&b)
b // 11
雖然 & 在參數傳遞時表示的意義和 C 中同樣,是某個「變量的地址」,可是在 Swift 中咱們沒有辦法直接經過這個符號獲取一個 UnsafePointer 的實例。須要注意這一點和 C 有所不一樣:性能
// 沒法編譯
let a = 100
let b = &a
指針初始化和內存管理ui
在 Swift 中不能直接取到現有對象的地址,咱們仍是能夠建立新的 UnsafeMutablePointer 對象。與 Swift 中其餘對象的自動內存管理不一樣,對於指針的管理,是須要咱們手動進行內存的申請和釋放的。一個 UnsafeMutablePointer 的內存有三種可能狀態:spa
內存沒有被分配,這意味着這是一個 null 指針,或者是以前已經釋放過設計
內存進行了分配,可是值尚未被初始化
內存進行了分配,而且值已經被初始化
其 中只有第三種狀態下的指針是能夠保證正常使用的。UnsafeMutablePointer 的初始化方法 (init) 完成的都是從其餘類型轉換到 UnsafeMutablePointer 的工做。咱們若是想要新建一個指針,須要作的是使用 alloc: 這個類方法。該方法接受一個 num: Int 做爲參數,將向系統申請 num 個數的對應泛型類型的內存。下面的代碼申請了一個 Int 大小的內存,並返回指向這塊內存的指針:
var intPtr = UnsafeMutablePointer.alloc(1)
// "UnsafeMutablePointer(0x7FD3A8E00060)"
接下來應該作的是對這個指針的內容進行初始化,咱們可使用 initialize: 方法來完成初始化:
intPtr.initialize(10)
// intPtr.memory 爲 10
在完成初始化後,咱們就能夠經過 memory 來操做指針指向的內存值了。
在使用以後,咱們最好儘快釋放指針指向的內容和指針自己。與 initialize: 配對使用的 destroy 用來銷燬指針指向的對象,而與 alloc: 對應的 dealloc: 用來釋放以前申請的內存。它們都應該被配對使用:
intPtr.destroy()
intPtr.dealloc(1)
intPtr = nil
注意:其實在這裏對於 Int 這樣的在 C 中映射爲 int 的 「平凡值」 來講,destroy 並非必要的,由於這些值被分配在常量段上。可是對於像類的對象或者結構體實例來講,若是不保證初始化和摧毀配對的話,是會出現內存泄露的。因此沒有特殊 考慮的話,不論內存中究竟是什麼,保證 initialize: 和 destroy 配對會是一個好習慣。
指向數組的指針
在 Swift 中將一個數組做爲參數傳遞到 C API 時,Swift 已經幫助咱們完成了轉換,這在 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 數組的 C API,其要求的類型爲 UnsafePointer,而非 const 的數組則對應 UnsafeMutablePointer。使用時,對於 const 的參數,咱們直接將 Swift 數組傳入 (上例中的 a 和 b);而對於可變的數組,在前面加上 & 後傳入便可 (上例中的 result)。
對於傳參,Swift 進行了簡化,使用起來很是方便。可是若是咱們想要使用指針來像以前用 memory 的方式直接操做數組的話,就須要藉助一個特殊的類型:UnsafeMutableBufferPointer。Buffer Pointer 是一段連續的內存的指針,一般用來表達像是數組或者字典這樣的集合類型。
var array = [1, 2, 3, 4, 5]
var arrayPtr = UnsafeMutableBufferPointer(start: &array, count: array.count)
// baseAddress 是第一個元素的指針
var basePtr = arrayPtr.baseAddress as UnsafeMutablePointer
basePtr.memory // 1
basePtr.memory = 10
basePtr.memory // 10
//下一個元素
var nextPtr = basePtr.successor()
nextPtr.memory // 2
指針操做和轉換
withUnsafePointer
上 面咱們說過,在 Swift 中不能像 C 裏那樣使用 & 符號直接獲取地址來進行操做。若是咱們想對某個變量進行指針操做,咱們能夠藉助 withUnsafePointer 這個輔助方法。這個方法接受兩個參數,第一個是 inout 的任意類型,第二個是一個閉包。Swift 會將第一個輸入轉換爲指針,而後將這個轉換後的 Unsafe 的指針做爲參數,去調用閉包。使用起來大概是這個樣子:
var test = 10
test = withUnsafeMutablePointer(&test, { (ptr: UnsafeMutablePointer) -> Int in
ptr.memory += 1
return ptr.memory
})
test // 11
這裏其實咱們作了和文章一開始的 incrementor 相同的事情,區別在於不須要經過方法的調用來將值轉換爲指針。這麼作的好處對於那些只會執行一次的指針操做來講是顯而易見的,能夠將「咱們就是想對這個指針作點事兒」這個意圖表達得更加清晰明確。
unsafeBitCast
unsafeBitCast 是很是危險的操做,它會將一個指針指向的內存強制按位轉換爲目標的類型。由於這種轉換是在 Swift 的類型管理以外進行的,所以編譯器沒法確保獲得的類型是否確實正確,你必須明確地知道你在作什麼。好比:
let arr = NSArray(object: "meow")
let str = unsafeBitCast(CFArrayGetValueAtIndex(arr, 0), CFString.self)
str // 「meow」
由於 NSArray 是能夠存聽任意 NSObject 對象的,當咱們在使用 CFArrayGetValueAtIndex 從中取值的時候,獲得的結果將是一個 UnsafePointer。因爲咱們很明白其中存放的是 String 對象,所以能夠直接將其強制轉換爲 CFString。
關 於 unsafeBitCast 一種更常見的使用場景是不一樣類型的指針之間進行轉換。由於指針自己所佔用的的大小是必定的,因此指針的類型進行轉換是不會出什麼致命問題的。這在與一些 C API 協做時會很常見。好比有不少 C API 要求的輸入是 void *,對應到 Swift 中爲 UnsafePointer。咱們能夠經過下面這樣的方式將任意指針轉換爲 UnsafePointer。
var count = 100
var voidPtr = withUnsafePointer(&count, { (a: UnsafePointer) -> UnsafePointerin
return unsafeBitCast(a, UnsafePointer.self)
})
// voidPtr 是 UnsafePointer。至關於 C 中的 void *
// 轉換回 UnsafePointer
var intPtr = unsafeBitCast(voidPtr, UnsafePointer.self)
intPtr.memory //100
總結
Swift 從設計上來講就是以安全做爲重要原則的,雖然可能有些囉嗦,可是仍是要重申在 Swift 中直接使用和操做指針應該做爲最後的手段,它們始終是沒法確保安全的。從傳統的 C 代碼和與之無縫配合的 Objective-C 代碼遷移到 Swift 並非一件小工程,咱們的代碼庫確定會時不時出現一些和 C 協做的地方。咱們固然能夠選擇使用 Swift 重寫部分陳舊代碼,可是對於像是安全或者性能相當重要的部分,咱們可能除了繼續使用 C API 之外別無選擇。若是咱們想要繼續使用那些 API 的話,瞭解一些基本的 Swift 指針操做和使用的知識會頗有幫助。
對於新的代碼,儘可能避免使用 Unsafe 開頭的類型,意味着能夠避免不少沒必要要的麻煩。Swift 給開發者帶來的最大好處是可讓咱們用更加先進的編程思想,進行更快和更專一的開發。只有在尊重這種思想的前提下,咱們才能更好地享受這門新語言帶來的種 種優點。顯然,這種思想是不包括處處使用 UnsafePointer 的 :)