Swift 中的位操作

作者:uraimo,原文鏈接,原文日期:2016-02-05
譯者:Lanford3_3;校對:numbbbbb;定稿:千葉知風

如你所知,Swift 提供了便利的定長整型以及常用的位運算符,所以使用 Swift 進行位操作似乎相當直接。

但你很快就會發現這門語言及它的標準庫總是奉行“安全第一”的原則,所以,相較于你過去的習慣,使用 Swift 對位以及不同的整型的處理需要更多的類型轉換。這篇文章介紹了一些必須掌握的內容。

在我做進一步闡釋之前,首先快速過一遍整型的基礎和位運算。

你可以通過 GitHubzipped 來獲取本文的 Playground 文件

整型和位運算符

Swift 提供了一個包含不同定長和符號類型整型的集合:Int/UIntInt8/UInt8(8 位),Int16/UInt16(16 位),Int32/UInt32(32 位),Int64/UInt64(64 位)。

Int 和 UInt 這兩種類型是有平臺依賴的:在 32 位平臺上等于 Int32/UInt32,而在 64 位平臺上等于 Int64/UInt64。其他整型的長度是特定的,與你編譯的目標平臺無關。

定長類型與位運算符結合使用起來威力十足,他們能讓你所處理的數據的尺寸變得清晰明了,在對單個位進行操作時,你幾乎不會用到依賴于平臺的 Int 或者 UInt。

類型為定長整型的變量能夠使用二進制、八進制或者十六進制值進行初始化,就像這樣:

var int1:UInt8 = 0b10101010
var int2:UInt8 = 0o55
var int3:UInt8 = 0xA7

至于位運算,如你所愿,Swift 提供了:NOT(~(單目運算符)), AND(運算符為 &), OR(運算符為|), XOR(運算符為 ^)以及左移和右移(運算符分別為 << 和 >>)。

這里有個要牢記的重點,對于無符號整型,左移或者右移一定的位數會在移動留下的空白位補 0。而有符號整型在右移時,使用符號位而非 0 來填充空白位。

對于長度超過一個字節的整型,Swift 也提供了一些有用的屬性來進行字節序轉換:littleEndianbigEndianbyteSwapped,分別表示將當前整數轉換為小字節序或大字節序或轉換到相反的字節序。最后一點,有沒有一種方法來判斷我們是在 32 位平臺還是 64 位平臺呢?

答案是肯定的,但是考慮到內建模塊無法訪問,我們只好在兩種平臺對應的定長整型(Int32 與 Int64)中任選其一,通過它與 Int 的長度的比較來進行判斷了:

strideof(Int) == strideof(Int32) // 當前平臺為 32 位平臺?不是的。

在這里我用了 strideof,但在本例中,也可以用 sizeof

整型轉換

Swift 不進行隱式類型轉換。你應該也已經注意到了,在進行混合類型運算時,你需要對表達式中的變量進行顯式的類型轉換,令其足以裝下你的結果。

對于同一表達式中出現多個整數的情況,只有當其他整數的類型已經確定,且都是同一種整型的時候,Swift 才能推斷出未指定類型的整數的類型,和之前一樣,Swift 并不會把變量類型隱式轉換到尺寸更大的整型。

下面這個例子說明了哪些操作是允許的,而哪些是不允許的:

var u8: UInt8 = 1
u8 << 2              //4: 數字 2 被認為是 UInt8 類型,u8 
                     //   被左移了兩位

var by2: Int16 = 1
u8 << by2            //Error: 數據類型不一致,無法編譯
u8 << UInt8(by2)     //2: 這是可行的,我們手動轉換了整型的類型,
                     //   但這是不安全的!

也許你會問,為什么這是不安全的?

因為在把一個大尺寸的整型轉換為較小的整型或者把一個無符號整型轉換為一個有符號整型時,Swift 不會對變量內的值進行任何截短操作,所以如若轉換后的整型無法裝下賦給它的值,就會導致溢出并引發運行時錯誤。

當你對來自用戶輸入或者其他外部組件的數據進行整型的類型轉換時,這點至關重要,必須銘記于心。

幸運的是,Swift 可以通過使用 init(truncatingBitPattern:) 構造器來進行位的截短。當你進行不需要關心整數的實際十進制值的位操作時這相當有用。

var u8: UInt8 = UInt8(truncatingBitPattern: 1000)
u8  // 232

在這個例子中,我們把 Int 類型的 1000(二進制表示為 0b1111101000)轉換為 UInt8 類型的變量,我們只保留了 8 個最低有效位,舍棄了其他位。通過這種方式,我們得到了 232, 二進制表示為 0b11101000

這也同樣作用于所有 Intn 或 UIntn 整型的組合,對于帶符號的 Int ,其符號會被忽略,位序列只被用來初始化新的整數值。對于相同長度的有符號與無符號整型,init(bitPattern:) 也可用,但是結果和一般的截短轉換是一樣的。

這種“安全第一”的方法的唯一缺點就是,當你需要進行很多類型轉換時,這些截短轉換會讓你的代碼變得臃腫。

但幸運的是,在 Swift 中,我們可以給基本類型添加新方法,通過這種方式我們可以給所有整型加入一些實用方法將他們截短為特定的尺寸,舉個例子:

extension Int {
    public var toU8: UInt8{ get{return UInt8(truncatingBitPattern:self)} }
    public var to8: Int8{ get{return Int8(truncatingBitPattern:self)} }
    public var toU16: UInt16{get{return UInt16(truncatingBitPattern:self)}}
    public var to16: Int16{get{return Int16(truncatingBitPattern:self)}}
    public var toU32: UInt32{get{return UInt32(truncatingBitPattern:self)}}
    public var to32: Int32{get{return Int32(truncatingBitPattern:self)}}
    public var toU64: UInt64{get{
            return UInt64(self) //No difference if the platform is 32 or 64
        }}
    public var to64: Int64{get{
            return Int64(self) //No difference if the platform is 32 or 64
        }}
}

extension Int32 {
    public var toU8: UInt8{ get{return UInt8(truncatingBitPattern:self)} }
    public var to8: Int8{ get{return Int8(truncatingBitPattern:self)} }
    public var toU16: UInt16{get{return UInt16(truncatingBitPattern:self)}}
    public var to16: Int16{get{return Int16(truncatingBitPattern:self)}}
    public var toU32: UInt32{get{return UInt32(self)}}
    public var to32: Int32{get{return self}}
    public var toU64: UInt64{get{
        return UInt64(self) //No difference if the platform is 32 or 64
        }}
    public var to64: Int64{get{
        return Int64(self) //No difference if the platform is 32 or 64
        }}
}

var h1 = 0xFFFF04
h1
h1.toU8   // 替代 UInt8(truncatingBitPattern:h1)

var h2:Int32 = 0x6F00FF05
h2.toU16  // 替代 UInt16(truncatingBitPattern:h2)

常見按位運算模式

現在,讓我們通過實踐來了解些常見的按位運算模式,就把這當成談論一些真的很有用但是在 Swift 中又沒法用的東西的借口吧。

字節抽取

AND 和右移(>>)的組合通常用于從較長的序列中截取位或者字節。讓我們看個例子,在這個例子中,我們要從表示顏色的 RGB 值中取出單個顏色元素的值:

let swiftOrange = 0xED903B
let red = (swiftOrange & 0xFF0000) >> 16    //0xED
let green = (swiftOrange & 0x00FF00) >> 8   //0x90
let blue = swiftOrange & 0x0000FF           //0x3B

在這個例子中,我們通過給數據 AND 上一個位掩碼來分離出我們感興趣的位。我們感興趣的位在結果中都是1,其他的都是0。為了得到我們所需要的部分并用8位去表示他,我們需要對 AND 運算的結果進行右移,移動16位得到紅色部分(右移兩個字節),移動8位獲得綠色部分(右移一個字節)。就是這樣,這種掩碼+移位的模式具有廣泛的應用,但是用在子表達式中會使你的表達式很快變得難以閱讀,那么為什么不把它寫成所有整型的下標腳本呢?換言之,為什么不像數組一樣,為整型添加上通過索引(index)來訪問單個字節的功能呢?

舉個例子,讓我們給 Int32 添加下標腳本:

extension UInt32 {
    public subscript(index: Int) -> UInt32 {
        get {
            precondition(index<4,"Byte set index out of range")
            return (self & (0xFF << (index.toU32*8))) >> (index.toU32*8)
        }
        set(newValue) {
            precondition(index<4,"Byte set index out of range")
            self = (self & ~(0xFF << (index.toU32*8))) | (newValue << (index.toU32*8))
        }
    }
}

var i32:UInt32=982245678                        //HEX: 3A8BE12E

print(String(i32,radix:16,uppercase:true))      // Printing the hex value

i32[3] = i32[0]
i32[1] = 0xFF
i32[0] = i32[2]

print(String(i32,radix:16,uppercase:true))      //HEX: 2E8BFF8B

神奇的 XOR

你們中的部分人可能通過簡單而無用的 XOR 密碼對 XOR 有了一些了解。XOR 密碼通過對位流 XOR 上一個 key 進行加密,然后通過再次 XOR 那個 key 來獲取原始數據。為了簡單起見,我們以相同長度的信息和 key 為例:

let secretMessage = 0b10101000111110010010101100001111000 // 0x547C95878
let secretKey =  0b10101010101010000000001111111111010    // 0x555401FFA
let result = secretMessage ^ secretKey                    // 0x12894782

let original = result ^ secretKey                         // 0x547C95878
print(String(original,radix:16,uppercase:true))           // 打印16進制值

XOR 的這個性質還能夠用來做其他事,最簡單的例子是 XOR swap, 即不使用臨時變量來交換兩個整型變量的值:

var x = 1
var y = 2
x = x ^ y
y = y ^ x   // y 現在為 1
x = x ^ y   // x 現在為 2

在 Swift 中你可以用 tuple 來做同樣的事兒(看看這兒的第 11 項),所以這并沒什么用=,=

另外還有件你能用 XOR 來做的事兒,但是我在這兒不細說,簡而言之就是構建一個傳統雙向鏈表的變種: XOR 鏈表。這是 XOR 的一種更有趣的使用方法,可以在 wikipedia 查看更多詳情.

雙重否定:是我們想要的那個集合嗎?

類似于上面的用法的另一種常見模式,是將位掩碼與雙重否定結合使用,以查找輸入的位序列中是否出現了特定的位或者位組合。

let input: UInt8 = 0b10101101
let mask: UInt8 = 0b00001000
let isSet = !!(input & mask)  // 如果輸入序列的第四位為 1,那么 isSet 等于 1
                              // 但這代碼在 Swift 中是錯的

雙重否定是基于 C/C++(及其他一些語言)中邏輯否定的特殊表現的,事實上,在 C/C++ 中布爾型是用整型實現的(0 表示 false, 1 表示 true), 以下引用自 C99 標準:

如果邏輯否運算符 ! 的操作數不為 0,則運算結果為 0,否則其運算結果為 1。運算結果為整型,表達式 !E 等同于 (0==E)。

考慮到這個,雙重否定的作用就變得更加清晰了。如果我們加過掩碼的輸入大于 0 或者等于 0, 第一個邏輯否(NOT)運算就會分別把它轉為 0 或 1(實際上把這個值取反就得到我們想要的布爾值了)。而第二個邏輯否(NOT)則把輸入轉回原始的布爾值,(這里只有 0 或 1 這兩個選擇)。也許這個解釋有點混亂,但是你應該能看懂。

不過 Swift 已經有了一個特有的布爾類型,而邏輯否定只能用于這些邏輯類型,所以,我們該怎么做呢?

讓我們來自定義一個運算符(通常來說,我并不喜歡它們,但在此讓我們破下例),來為 UInt8 類型加上雙重否定!

prefix operator ~~ {}

prefix func ~~(value: UInt8) -> UInt8 {
    return (value > 0) ? 1 : 0
}

~~7  // 1
~~0  // 0

let isSet = ~~(input & mask)   // 正如所料,結果是 1 

作為改進,我們可以返回一個 Bool 而非 UInt8, 這樣就可以在條件語句中直接使用了,但是我們會失去把它嵌套到其他整數表達式的能力。

Bitter: 一個用于位操作的庫

Bitter's logo
Bitter's logo

本文所列出的所有用來進行位操作的替代方法都是Bitter的一部分,這是一個試圖為位操作提供更加 "Swifty" 的接口的庫。

總結下你能在 Bitter 中得到些什么(Bitter 可以通過 CocoaPods, Carthage, SwiftPM 獲取):

  • 用來進行位截短轉換的便利性質
  • 給每個整型都添加字節索引的下標腳本
  • 雙重否定運算符
  • 以及更多……

這個庫還不完善,非常歡迎反饋!請盡管嘗試一下,如果有些功能沒法用或者你想添加別的特性,盡管開 issues。

想說些什么?來推特找我吧。

上 Hacker News 投票

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 http://swift.gg

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容