Swift 中的指針使用

jopen 9年前發布 | 48K 次閱讀 Swift Apple Swift開發

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的 :)

來自:http://onevcat.com/2015/01/swift-pointer/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!