Swift中的指標操作及使用

edithfang發表於2015-01-22
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對它進行賦值。比如我們想要寫一個利用指標直接操作記憶體的計數器的話,可以這麼做:

func incrementor(ptr: UnsafeMutablePointer<Int>) {  
    ptr.memory += 1  
}  
var a = 10  
incrementor(&a)  
a  // 11  


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

與這種做法類似的是使用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  

指標初始化和記憶體管理

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

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

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

var intPtr = UnsafeMutablePointer<Int>.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<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  

指標操作和轉換

  • withUnsafePointer

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

var test = 10  
test = withUnsafeMutablePointer(&test, { (ptr: UnsafeMutablePointer<Int>) -> 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<Void>。由於我們很明白其中存放的是String物件,因此可以直接將其強制轉換為CFString。

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

var count = 100  
var voidPtr = withUnsafePointer(&count, { (a: UnsafePointer<Int>) -> UnsafePointer<Void> in  
    return unsafeBitCast(a, UnsafePointer<Void>.self)  
})  
// voidPtr 是 UnsafePointer<Void>。相當於 C 中的 void *  
// 轉換回 UnsafePointer<Int>  
var intPtr = unsafeBitCast(voidPtr, UnsafePointer<Int>.self)  
intPtr.memory //100  

總結

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

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

作者:王巍(@onevcat),iOS和Unity3D開發者。
相關閱讀
評論(1)

相關文章