一:枚舉(Enum)
1. 枚舉的基本用法
Swift
中通過 enum
關(guān)鍵字來聲明一個枚舉
enum LJLEnum {
case test_one
case test_two
case test_three
}
Swift
中的枚舉則更加靈活架馋,并且不需給枚舉中的每一個成員都提供值绳锅。如果一個值(所謂“原始”值)要被提供給每一個枚舉成員,那么這個值可以是字符串、字符欲逃、任意的整數(shù)值,或者是浮點類型饼暑。
enum Color: String {
case red = "Red"
case blue = "Blue"
case green = "Grren"
}
enum LJLEnum: Double {
case a = 10.0
case b = 22.2
case c = 33.3
case d = 44.4
}
2. 原始值 RawValue
隱士 RawValue
是建立在 Swift
的類型判斷機制上的
enum LJLEnum: Int {
case one, two, three = 10, four, five
}
print(LJLEnum.one.rawValue)
print(LJLEnum.two.rawValue)
print(LJLEnum.three.rawValue)
print(LJLEnum.four.rawValue)
print(LJLEnum.five.rawValue)
// 打印結(jié)果
0
1
10
11
12
可以看到 RawValue
原始值跟 OC
一樣都是從 0
稳析、 1
洗做、2
開始,當指定值時彰居,后面的枚舉值的 RawValue
會在當前值的基礎(chǔ)上進行累加操作诚纸,因此 four
和 five
的值為 11
和 12
。
將枚舉類型改成 String
類型
enum LJLEnum: String {
case one, two, three = "Hello World", four, five
}
print(LJLEnum.one.rawValue)
print(LJLEnum.two.rawValue)
print(LJLEnum.three.rawValue)
print(LJLEnum.four.rawValue)
print(LJLEnum.five.rawValue)
// 打印結(jié)果
one
two
Hello World
four
five
可以看出系統(tǒng)已經(jīng)默認給了每一個枚舉成員分配了一個字符串陈惰,并且該字符串與枚舉成員值的字符串一致畦徘。
將上述代碼簡化一下,通過 LLDB
命令 swiftc xxx.swift -emit-sil
編譯成 sil
文件
enum LJLEnum: String {
case one, two = "Hello World", three
}
var x = LJLEnum.one.rawValue
sil
文件代碼
// 枚舉的聲明
enum LJLEnum : String {
case one, two, three
init?(rawValue: String)
typealias RawValue = String
var rawValue: String { get }
}
// LJLEnum.rawValue.getter
sil hidden @$s4main7LJLEnumO8rawValueSSvg : $@convention(method) (LJLEnum) -> @owned String {
// %0 "self" // users: %2, %1
bb0(%0 : $LJLEnum):
debug_value %0 : $LJLEnum, let, name "self", argno 1 // id: %1
switch_enum %0 : $LJLEnum, case #LJLEnum.one!enumelt: bb1, case #LJLEnum.two!enumelt: bb2, case #LJLEnum.three!enumelt: bb3 // id: %2
bb1: // Preds: bb0
%3 = string_literal utf8 "one" // user: %8
%4 = integer_literal $Builtin.Word, 3 // user: %8
%5 = integer_literal $Builtin.Int1, -1 // user: %8
%6 = metatype $@thin String.Type // user: %8
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%7 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %8
%8 = apply %7(%3, %4, %5, %6) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %9
br bb4(%8 : $String) // id: %9
bb2: // Preds: bb0
%10 = string_literal utf8 "Hello World" // user: %15
%11 = integer_literal $Builtin.Word, 11 // user: %15
%12 = integer_literal $Builtin.Int1, -1 // user: %15
%13 = metatype $@thin String.Type // user: %15
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%14 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %15
%15 = apply %14(%10, %11, %12, %13) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %16
br bb4(%15 : $String) // id: %16
bb3: // Preds: bb0
%17 = string_literal utf8 "three" // user: %22
%18 = integer_literal $Builtin.Word, 5 // user: %22
%19 = integer_literal $Builtin.Int1, -1 // user: %22
%20 = metatype $@thin String.Type // user: %22
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%21 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %22
%22 = apply %21(%17, %18, %19, %20) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %23
br bb4(%22 : $String) // id: %23
// %24 // user: %25
bb4(%24 : $String): // Preds: bb3 bb2 bb1
return %24 : $String // id: %25
} // end sil function '$s4main7LJLEnumO8rawValueSSvg'
我們可以看到 rawValue.getter
函數(shù)的調(diào)用奴潘,根據(jù)傳進來的枚舉成員值旧烧,通過模式匹配的方式走到不同的代碼分支,在不同的代碼分支中把不同的字符串給到當前對應的代碼分支返回值画髓。
3. 關(guān)聯(lián)值
用枚舉表達更復雜的情況掘剪,
enum Shape {
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
var circle = Shape.circle(radius: 10.0)
var square = Shape.rectangle(width: 5, height: 5)
4. 模式匹配
enum Week: String {
case MONDAY
case TUEDAY
case WEDDAY
case THUDAY
case FRIDAY
case SATDAY
case SUNDAY
}
通過 Switch
關(guān)鍵字進行匹配
let currentWeek: Week
switch currentWeek {
case .MONDAY: print(Week.MONDAY.rawValue)
case .TUEDAY: print(Week.TUEDAY.rawValue)
case .WEDDAY: print(Week.WEDDAY.rawValue)
case .THUDAY: print(Week.THUDAY.rawValue)
case .FRIDAY: print(Week.FRIDAY.rawValue)
case .SUNDAY: print(Week.SUNDAY.rawValue)
case .SATDAY: print(Week.SUNDAY.rawValue)
}
如果不想匹配所有的 case
,使用 defalut
關(guān)鍵字
let currentWeek: Week = Week.MONDAY
switch currentWeak {
case .SATDAY, .SUNDAY: print("Happy Day")
default : print("SAD DAY")
}
匹配關(guān)聯(lián)值
enum Shape {
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
let shape = Shape.circle(radius: 10.0)
// 方式一:
switch shape {
case let .circle(radius):
print("Circle radius:\(radius)")
case let .rectangle(width, height):
print("rectangle width:\(width), height\(height)")
}
// 方式二:
switch shape {
case .circle(let radius):
print("Circle radius:\(radius)")
case .rectangle(let width, let height):
print("rectangle width:\(width), height\(height)")
}
5. 枚舉的大小
5.1 沒有關(guān)聯(lián)值枚舉(No-payload enums)
enum Week: String {
case MONDAY
case TUEDAY
case WEDDAY
case THUDAY
case FRIDAY
case SATDAY
case SUNDAY
}
print(MemoryLayout<Week>.size)
print(MemoryLayout<Week>.stride)
// 打印結(jié)果
1
1
可以看出不管是大小( size
) 還是步長 stride
都是 1
奈虾,在 Swift
中進行枚舉布局的時候一直是嘗試使用最少的空間來存儲 enum
夺谁,對于當前的 case
數(shù)量來說, UInt8
能夠表示 256 cases
肉微,也就意味著如果一個默認枚舉類型且沒有關(guān)聯(lián)值的 case
少于 256
匾鸥,當前枚舉類型的大小都是 1
字節(jié)。
通過上面的打印我們可以看到碉纳,當前變量
a
勿负, b
, c
三個變量的地址相差 1
位劳曹,并且存儲的內(nèi)容分別是 00
奴愉, 01
, 02
铁孵,這與我們上面說的布局理解是一致的
5.2 單個關(guān)聯(lián)值枚舉(Single-payload enums)
enum LJLBoolEnum {
case one(Bool)
case two
case three
case four
}
enum LJLIntEnum {
case one(Int)
case two
case three
case four
}
print("BoolEnum.Size:\(MemoryLayout<LJLBoolEnum>.size)")
print("BoolEnum.stride:\(MemoryLayout<LJLBoolEnum>.stride)")
print("IntEnum.Size:\(MemoryLayout<LJLIntEnum>.size)")
print("IntEnum.stride:\(MemoryLayout<LJLIntEnum>.stride)")
Swift
中的 enum
中的 Single-payload enums
會使用負載類型中的額外空間來記錄沒有負載的 case 值锭硼。比如這里的 BoolEnum
,首先 Bool
類型是 1
字節(jié)蜕劝,也就是 UInt8
檀头,所以當前能表達 256
個 case
的情況,對于 Bool
類型來說岖沛,只需要使用低位的 0
暑始, 1
這兩種情況,其他剩余的空間就可以用來表示沒有負載的 case
值婴削。
可以看到不同的
case
值確實是按照我們在開始得出來的那個結(jié)論進行布局的蒋荚。對于
Int
類型的負載來說,其實系統(tǒng)是沒有辦法推算當前的負載所要使用的位數(shù)馆蠕,也就意味著當前 Int
類型的負載是沒有額外的剩余空間的期升,這個時候我們就需要額外開辟內(nèi)存空間來去存儲我們的 case
值,也就是 8 + 1 = 9
字節(jié)互躬。可以看出變量
a
播赁、 b
、 c
吼渡、 d
容为、 e
的地址相差 16
位,這和上面打印的步長信息相一致寺酪。
5.3 多個關(guān)聯(lián)值枚舉(Mutil-payload enums)
enum LJLDoubleBoolEnum {
case one(Bool)
case two(Bool)
case three
case four
}
enum LJLDoubleIntEnum {
case one(Int)
case two(Int)
case three
case four
}
print("DoubleBoolEnum.Size:\(MemoryLayout<LJLDoubleBoolEnum>.size)")
print("DoubleBoolEnum.stride:\(MemoryLayout<LJLDoubleBoolEnum>.stride)")
print("DoubleIntEnum.Size:\(MemoryLayout<LJLDoubleIntEnum>.size)")
print("DoubleIntEnum.stride:\(MemoryLayout<LJLDoubleIntEnum>.stride)")
打印結(jié)果
我們可以看到兩個
Bool
關(guān)聯(lián)值的枚舉的大小為 1
坎背,根據(jù)上面單個關(guān)聯(lián)值枚舉的所述不難理解。對于兩個 Int
關(guān)聯(lián)值的枚舉的大小為 9
寄雀,是因為創(chuàng)建一個枚舉值時有且只有一個關(guān)聯(lián)值得滤,但是還需要 1
個字節(jié)去存儲其他 case
枚舉值( three
和 four
) 所以當前只需要 8
字節(jié) + 1
字節(jié)我們可以看到當前內(nèi)存存儲的分別是
00
、01
盒犹、 40
懂更、 41
、 80
急膀、 81
沮协,這里在存儲當前的 case
時候會使用到 common spare bits
,首先 bool
類型需要 1
字節(jié)卓嫂,也就是 8
位慷暂,對于 bool
類型來說,我們存儲的無非就是 0
或 1
晨雳,只需要用到 1
位行瑞,所以剩余的 7
位,這里我們都統(tǒng)稱為 common spare bits
悍募,對于當前的 case
數(shù)量來說我們完全可以把所有的情況放到 common spare bits
所以這里只需要 1
字節(jié)就可以存儲所有的內(nèi)容了蘑辑。對于
00
、 01
坠宴、 40
洋魂、 41
、 80
喜鼓、 81
副砍,其中 0
、 4
庄岖、 8
稱之為 tag value
豁翎, 0
、 1
稱之為 tag index
隅忿。
一般來說心剥,有多個負載的枚舉時邦尊,當前枚舉類型的大小取決于當前最大關(guān)聯(lián)值的大小。
enum LJLEnum{
case one(Bool)
case two(Int)
case three
case four
}
print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")
// 打印結(jié)果
LJLEnum.Size:9
當前 LJLEnum
的大小就等于 sizeof(Int)
+ sizeof(rawVlaue)
= 9
enum LJLEnum{
case one(Bool)
case two(Int, Bool, Int)
case three
case four
}
print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")
// 打印結(jié)果
LJLEnum.Size:24
這里為什么不是 sizeof(Int) * 2 + sizeof(rawVlaue)
= 17
呢优烧?對于 two (Int, Bool, Int)
類型的由于字節(jié)對齊的原因所以它的存儲大小為 8 * 3
= 24
蝉揍,又由于中間的 Bool
實際值占用 1
個字節(jié)因此中間 8
字節(jié)還有剩余控件去存儲其他的 case
值。所以這里是 24
字節(jié)畦娄。那么將 bool
放在后面呢又沾?我們試一試
enum LJLEnum{
case one(Bool)
case two(Int, Int, Bool)
case three
case four
}
print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")
print("LJLEnum.Stride:\(MemoryLayout<LJLEnum>.stride)")
可以看到大小為
17
,其實也不難理解熙卡,bool
放后面那么它的 1
字節(jié)就能放下其他的 case
杖刷,那為什么這里不字節(jié)對齊呢?其實是因為最后會因為步長 24
的原因會對這里進行補齊驳癌。
注意對于只有一個 case
的枚舉滑燃,不需要用任何東?來去區(qū)分當前的 case
,所以它的大小是 0
喂柒。
enum LJLEnum{
case one
}
print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")
// 打印結(jié)果
LJLEnum.Size:0
6. 遞歸枚舉
遞歸枚舉是一種枚舉類型不瓶,它有一個或多個枚舉成員使用該枚舉類型的實例作為關(guān)聯(lián)值。使用遞歸枚舉時灾杰,編譯器會插入一個間接層蚊丐。你可以在枚舉成員前加上 indirect
來表示該成員可遞歸。
enum BinaryTree<T> {
case empty
indirect case node(left: BinaryTree, value: T, right: BinaryTree)
}
var node = BinaryTree<Int>.node(left: BinaryTree<Int>.empty, value: 10, right: BinaryTree<Int>.empty)
也可以在枚舉類型開頭加上 indirect
關(guān)鍵字來表明它的所有成員都是可遞歸的艳吠。
indirect enum BinaryTree<T> {
case empty
case node(left: BinaryTree, value: T, right: BinaryTree)
}
var node = BinaryTree<Int>.node(left: BinaryTree<Int>.empty, value: 10, right: BinaryTree<Int>.empty)
在匯編中查看
可以發(fā)現(xiàn)這里調(diào)用了
swift_allocObject
麦备, 在之前的文章 Swift探索(一): 類與結(jié)構(gòu)體(上) 中我們就探討過, swift_allocObject
就是在堆空間中分配內(nèi)存空間昭娩。對于 indirect
關(guān)鍵字放在 enum
前面也就意味著當前這個 enum
的大小都是用引用類型在堆空間中存儲凛篙。當 indirect
關(guān)鍵字放在 case
前面,那么就只有這個 case note
是存儲在堆空間中栏渺,其中 case empty
是存儲在 __DATA.__common
(存儲沒有初始化過的符號聲明)的 section
中
二:可選值(Optional)
1. 什么是可選值
class Person {
var age: Int?
var name: Optional<String> = nil
}
age
和 name
我們就稱之為可選值
2.可選值的本質(zhì)
在
Swift
源碼中可以發(fā)現(xiàn)可選值實際上就是一個枚舉呛梆,并且有兩個 case
一個 none
,一個 some
3. 可選值的基本使用
func getOddValue(_ value: Int) -> Int? {
if value % 2 == 0 {
return .some(value)
} else {
return .none
}
}
var array = [1, 2, 3, 4, 5, 6]
for element in array {
let value = getOddValue(element)
switch value {
case .some(let value):
array.remove(at: array.firstIndex(of: value)!)
case .none:
print("vlaue not exist")
}
}
如果每一個可選值都用模式匹配的方式來獲取值在代碼書寫上就比較繁瑣磕诊,還可以使用 if let
的方式來進行可選值綁定
func getOddValue(_ value: Int) -> Int? {
if value % 2 == 0 {
return .some(value)
} else {
return .none
}
}
var array = [1, 2, 3, 4, 5, 6]
for element in array {
if let value = getOddValue(element) {
array.remove(at: array.firstIndex(of: value)!)
}
}
除了使用 if let
還可以使用 gurad let
填物,和 if let
剛好相反,gurad let
守護一定有值霎终。如果沒有滞磺,直接返回。 通常判斷是否有值之后莱褒,會做具體的邏輯實現(xiàn)击困,通常代碼多如果用 if let
憑空多了一層分支, gurad let
是降低分支層次的辦法
var name: String?
var age: Int?
var height: Double?
func testIfLet() {
if let name1 = name {
if let age1 = age {
if let height1 = height {
print("姓名: \(name1), 年齡:\(age1), 身高:\(height1)cm")
} else {
print("height 為空")
}
} else {
print("age 為空")
}
} else {
print("name 為空")
}
}
func testGuardLet() {
guard let name1 = name else {
print("name 為空")
return
}
guard let age1 = age else {
print("age 為空")
return
}
guard let height1 = height else {
print("height 為空")
return
}
print("姓名: \(name1), 年齡:\(age1), 身高:\(height1)cm")
}
2. 可選鏈
在 OC
中我們給一個 nil
對象發(fā)送消息什么也不會發(fā)生广凸,但是在 Swift
中是沒有辦 法向一個 nil
對象直接發(fā)送消息阅茶,但是借助可選鏈可以達到類似的效果蛛枚。
let str: String? = "abc"
let upperStr = str?.uppercased()
print(upperStr)
var str2: String?
let upperStr2 = str2?.uppercased()
print(upperStr2)
// 打印結(jié)果
Optional("ABC")
nil
同意可選鏈對數(shù)組和喜愛
var closure: ((Int) -> ())?
print(closure?(1)) // closure 為 nil 不執(zhí)行
let dict = ["one": 1, "two": 2]
print(dict["one"]) // Optional(1)
print(dict["three"]) // nil
// 打印結(jié)果
nil
Optional(1)
nil
3. ?? 運算符(空合并運算符)
( a ?? b
) 將對可選類型 a
進行空判斷,如果 a
包含一個值就進行解包脸哀,否則就返回一個默認值 b
坤候。
- 表達式
a
必須是Optional
類型 - 默認值
b
的類型必須要和a
存儲值的類型保持一致
var age: Int? = 10
var x = age ?? 0
print(x)
// 打印結(jié)果
10
4. 運算符重載
源碼中我們可以看到除了重載了 ??
運算符, Optional
類型還重載了 ==
企蹭, ?=
等等運算符,實際開發(fā)中我們可以通過重載運算符簡化我們的表達式智末。
struct Vector {
let x: Int
let y: Int
}
extension Vector {
static func + (fistVector: Vector, secondVector: Vector) -> Vector {
return Vector(x: fistVector.x + secondVector.x, y: fistVector.y + secondVector.y)
}
static prefix func - (vector: Vector) -> Vector {
return Vector(x: -vector.x, y: -vector.y)
}
static func - (fistVector: Vector, secondVector: Vector) -> Vector {
return fistVector + -secondVector
}
}
var x = Vector(x: 10, y: 20)
var y = Vector(x: 20, y: 30)
var z = x + y
print(z)
var w = -z
print(w)
// 打印結(jié)果
Vector(x: 30, y: 50)
Vector(x: -30, y: -50)
根據(jù)官方文檔創(chuàng)建一個自定義運算符 已有運算符
infix operator **: AdditionPrecedence
// 運算組名稱: LJLPrecedence 優(yōu)先級低于: AdditionPrecedence 結(jié)合方式: 左結(jié)合
precedencegroup LJLPrecedence {
lowerThan: AdditionPrecedence
associativity: left
}
struct Vector {
let x: Int
let y: Int
}
extension Vector {
static func + (fistVector: Vector, secondVector: Vector) -> Vector {
return Vector(x: fistVector.x + secondVector.x, y: fistVector.y + secondVector.y)
}
static prefix func - (vector: Vector) -> Vector {
return Vector(x: -vector.x, y: -vector.y)
}
static func - (fistVector: Vector, secondVector: Vector) -> Vector {
return fistVector + -secondVector
}
static func ** (fistVector: Vector, secondVector: Vector) -> Vector {
return Vector(x: fistVector.x * secondVector.x, y: fistVector.y * secondVector.y)
}
}
var x = Vector(x: 10, y: 20)
var y = Vector(x: 20, y: 30)
var z = x ** y
var w = x + y ** x
print(z)
print(w)
// 打印結(jié)果
Vector(x: 200, y: 600)
Vector(x: 300, y: 1000)
5. 隱士解析可選類型
隱式解析可選類型是可選類型的一種谅摄,使用的過程中和非可選類型無異。它們之間唯一的區(qū)別是系馆,隱式解析可選類型是你告訴對 Swift
編譯器送漠,在運行時訪問時,值不會為 nil
由蘑。
var age: Int
var age1: Int!
var age2: Int?
let x = age1 % 2
let y = age2 % 2
其中
age1
不用做解包的操作闽寡,編譯器已經(jīng)做了
@IBOutlet weak var btn: UIButton!
IBOutlet
類型是 Xcode
強制為可選類型的,因為它不是在初始化時賦值的尼酿,而是在加載視圖的時候爷狈。可以把它設置為普通可選類型裳擎,但是如果這個視圖加載正確涎永,它是不會為空的。
6. 可選值有關(guān)的高階函數(shù)
-
map
:這個方法接受一個閉包鹿响,如果可選值有內(nèi)容則調(diào)用這個閉包進行轉(zhuǎn)換
var dict = ["one": "1", "two": "2"]
let result = dict["one"].map{ Int($0) }
print(result)
// 打印結(jié)果
Optional(Optional(1))
上面的代碼中我們從字典中取出字符串 1
羡微,并將其轉(zhuǎn)換為 Int
類型,但因為 String
轉(zhuǎn)換成 Int
不一定能成功惶我,所以返回的是 Int?
類型妈倔,而且字典通過鍵不一定能取得到值,所以 map
返回的也是一個 Optional
绸贡,所以最后上述代碼 result
的類型為 Int??
類型盯蝴。
-
flatMap
:可以把結(jié)果展平成為單個可選值
var dict = ["one": "1", "two": "2"]
let result = dict["one"].flatMap{ Int($0) } // Optional(1)
print(result)
// 打印結(jié)果
Optional(1)
注意這個方法是作用在 Optional
的方法,而不是作用在 Sequence
上的
作用在 Sequence
上的 flatMap
方法在 Swift4.1
中被更名為 compactMap
恃轩,該方法可以將序列中的 nil
過濾出去
let array = ["1", "2", "3", nil]
let result = array.compactMap{ $0 } // ["1", "2", "3"]
print(result)
let array1 = ["1", "2", "3", "four"]
let result1 = array1.compactMap{ Int($0) } // [1, 2, 3]
print(result1)
// 打印結(jié)果
["1", "2", "3"]
[1, 2, 3]