Swift中的指標操作和使用詳細介紹_Swift

來源:互聯網
上載者:User

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的記憶體有三種可能狀態:

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

其中只有第三種狀態下的指標是可以保證正常使用的。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的。

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.