Swift 中的指標使用

發表於2015-01-24

Apple 期望在 Swift 中指標能夠儘量減少登場機率,因此在 Swift 中指標被對映為了一個泛型型別,並且還比較抽象。這在一定程度上造成了在 Swift 中指標使用的困難,特別是對那些並不熟悉指標,也沒有多少指標操作經驗的開發者 (包括我自己也是) 來說,在 Swift 中使用指標確實是一個挑戰。在這篇文章裡,我希望能從最基本的使用開始,總結一下在 Swift 中使用指標的一些常見方式和場景。這篇文章假定你至少知道指標是什麼,如果對指標本身的概念不太清楚的話,可以先看看這篇五分鐘 C 指標教程 (或者它的中文版本),應該會很有幫助。

初步

在 Swift 中,指標都使用一個特殊的型別來表示,那就是 UnsafePointer<T>。遵循了 Cocoa 的一貫不可變原則,UnsafePointer<T> 也是不可變的。當然對應地,它還有一個可變變體,UnsafeMutablePointer<T>。絕大部分時間裡,C 中的指標都會被以這兩種型別引入到 Swift 中:C 中 const 修飾的指標對應 UnsafePointer (最常見的應該就是 C 字串的 const char * 了),而其他可變的指標則對應 UnsafeMutablePointer。除此之外,Swift 中存在表示一組連續資料指標的UnsafeBufferPointer<T>,表示非完整結構的不透明指標 COpaquePointer 等等。另外你可能已經注意到了,能夠確定指向內容的指標型別都是泛型的 struct,我們可以通過這個泛型來對指標指向的型別進行約束以提供一定安全性。

對於一個 UnsafePointer<T> 型別,我們可以通過 memory 屬性對其進行取值,如果這個指標是可變的UnsafeMutablePointer<T> 型別,我們還可以通過 memory 對它進行賦值。比如我們想要寫一個利用指標直接操作記憶體的計數器的話,可以這麼做:

這裡和 C 的指標使用類似,我們通過在變數名前面加上 & 符號就可以將指向這個變數的指標傳遞到接受指標作為引數的方法中去。在上面的 incrementor 中我們通過直接操作 memory 屬性改變了指標指向的內容。

與這種做法類似的是使用 Swift 的 inout 關鍵字。我們在將變數傳入 inout 引數的函式時,同樣也使用 & 符號表示地址。不過區別是在函式體內部我們不需要處理指標型別,而是可以對引數直接進行操作。

雖然 & 在引數傳遞時表示的意義和 C 中一樣,是某個“變數的地址”,但是在 Swift 中我們沒有辦法直接通過這個符號獲取一個UnsafePointer 的例項。需要注意這一點和 C 有所不同:

指標初始化和記憶體管理

在 Swift 中不能直接取到現有物件的地址,我們還是可以建立新的 UnsafeMutablePointer 物件。與 Swift 中其他物件的自動記憶體管理不同,對於指標的管理,是需要我們手動進行記憶體的申請和釋放的。一個 UnsafeMutablePointer 的記憶體有三種可能狀態:

  • 記憶體沒有被分配,這意味著這是一個 null 指標,或者是之前已經釋放過
  • 記憶體進行了分配,但是值還沒有被初始化
  • 記憶體進行了分配,並且值已經被初始化

其中只有第三種狀態下的指標是可以保證正常使用的。UnsafeMutablePointer 的初始化方法 (init) 完成的都是從其他型別轉換到UnsafeMutablePointer 的工作。我們如果想要新建一個指標,需要做的是使用 alloc: 這個類方法。該方法接受一個 num: Int作為引數,將向系統申請 num 個數的對應泛型型別的記憶體。下面的程式碼申請了一個 Int 大小的記憶體,並返回指向這塊記憶體的指標:

接下來應該做的是對這個指標的內容進行初始化,我們可以使用 initialize: 方法來完成初始化:

在完成初始化後,我們就可以通過 memory 來操作指標指向的記憶體值了。

在使用之後,我們最好儘快釋放指標指向的內容和指標本身。與 initialize: 配對使用的 destroy 用來銷燬指標指向的物件,而與 alloc: 對應的 dealloc: 用來釋放之前申請的記憶體。它們都應該被配對使用:

注意其實在這裡對於 Int 這樣的在 C 中對映為 int 的 “平凡值” 來說,destroy 並不是必要的,因為這些值被分配在常量段上。但是對於像類的物件或者結構體例項來說,如果不保證初始化和摧毀配對的話,是會出現記憶體洩露的。所以沒有特殊考慮的話,不論記憶體中到底是什麼,保證initialize: 和 destroy 配對會是一個好習慣。

指向陣列的指標

在 Swift 中將一個陣列作為引數傳遞到 C API 時,Swift 已經幫助我們完成了轉換,這在 Apple 的官方部落格中有個很好的例子:

對於一般的接受 const 陣列的 C API,其要求的型別為 UnsafePointer,而非 const 的陣列則對應 UnsafeMutablePointer。使用時,對於 const 的引數,我們直接將 Swift 陣列傳入 (上例中的 a 和 b);而對於可變的陣列,在前面加上 & 後傳入即可 (上例中的 result)。

對於傳參,Swift 進行了簡化,使用起來非常方便。但是如果我們想要使用指標來像之前用 memory 的方式直接運算元組的話,就需要藉助一個特殊的型別:UnsafeMutableBufferPointer。Buffer Pointer 是一段連續的記憶體的指標,通常用來表達像是陣列或者字典這樣的集合型別。

指標操作和轉換

withUnsafePointer

上面我們說過,在 Swift 中不能像 C 裡那樣使用 & 符號直接獲取地址來進行操作。如果我們想對某個變數進行指標操作,我們可以藉助 withUnsafePointer 這個輔助方法。這個方法接受兩個引數,第一個是 inout 的任意型別,第二個是一個閉包。Swift 會將第一個輸入轉換為指標,然後將這個轉換後的 Unsafe 的指標作為引數,去呼叫閉包。使用起來大概是這個樣子:

這裡其實我們做了和文章一開始的 incrementor 相同的事情,區別在於不需要通過方法的呼叫來將值轉換為指標。這麼做的好處對於那些只會執行一次的指標操作來說是顯而易見的,可以將“我們就是想對這個指標做點事兒”這個意圖表達得更加清晰明確。

unsafeBitCast

unsafeBitCast 是非常危險的操作,它會將一個指標指向的記憶體強制按位轉換為目標的型別。因為這種轉換是在 Swift 的型別管理之外進行的,因此編譯器無法確保得到的型別是否確實正確,你必須明確地知道你在做什麼。比如:

因為 NSArray 是可以存放任意 NSObject 物件的,當我們在使用 CFArrayGetValueAtIndex 從中取值的時候,得到的結果將是一個 UnsafePointer<Void>。由於我們很明白其中存放的是 String 物件,因此可以直接將其強制轉換為 CFString

關於 unsafeBitCast 一種更常見的使用場景是不同型別的指標之間進行轉換。因為指標本身所佔用的的大小是一定的,所以指標的型別進行轉換是不會出什麼致命問題的。這在與一些 C API 協作時會很常見。比如有很多 C API 要求的輸入是 void *,對應到 Swift 中為 UnsafePointer<Void>。我們可以通過下面這樣的方式將任意指標轉換為 UnsafePointer。

總結

Swift 從設計上來說就是以安全作為重要原則的,雖然可能有些囉嗦,但是還是要重申在 Swift 中直接使用和操作指標應該作為最後的手段,它們始終是無法確保安全的。從傳統的 C 程式碼和與之無縫配合的 Objective-C 程式碼遷移到 Swift 並不是一件小工程,我們的程式碼庫肯定會時不時出現一些和 C 協作的地方。我們當然可以選擇使用 Swift 重寫部分陳舊程式碼,但是對於像是安全或者效能至關重要的部分,我們可能除了繼續使用 C API 以外別無選擇。如果我們想要繼續使用那些 API 的話,瞭解一些基本的 Swift 指標操作和使用的知識會很有幫助。

對於新的程式碼,儘量避免使用 Unsafe 開頭的型別,意味著可以避免很多不必要的麻煩。Swift 給開發者帶來的最大好處是可以讓我們用更加先進的程式設計思想,進行更快和更專注的開發。只有在尊重這種思想的前提下,我們才能更好地享受這門新語言帶來的種種優勢。顯然,這種思想是不包括到處使用 UnsafePointer 的 :)

相關文章