一:枚舉(Enum)
1. 枚舉的基本用法
Swift
中通過 enum
關鍵字來聲明一個枚舉
enum LJLEnum {
case test_one
case test_two
case test_three
}
Swift
中的枚舉則更加靈活,并且不需給枚舉中的每一個成員都提供值。如果一個值(所謂“原始”值)要被提供給每一個枚舉成員,那么這個值可以是字符串、字符、任意的整數值,或者是浮點類型。
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)
// 打印結果
0
1
10
11
12
可以看到 RawValue
原始值跟 OC
一樣都是從 0
、 1
、2
開始,當指定值時,后面的枚舉值的 RawValue
會在當前值的基礎上進行累加操作,因此 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)
// 打印結果
one
two
Hello World
four
five
可以看出系統已經默認給了每一個枚舉成員分配了一個字符串,并且該字符串與枚舉成員值的字符串一致。
將上述代碼簡化一下,通過 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
函數的調用,根據傳進來的枚舉成員值,通過模式匹配的方式走到不同的代碼分支,在不同的代碼分支中把不同的字符串給到當前對應的代碼分支返回值。
3. 關聯值
用枚舉表達更復雜的情況,
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
關鍵字進行匹配
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
關鍵字
let currentWeek: Week = Week.MONDAY
switch currentWeak {
case .SATDAY, .SUNDAY: print("Happy Day")
default : print("SAD DAY")
}
匹配關聯值
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 沒有關聯值枚舉(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)
// 打印結果
1
1
可以看出不管是大小( size
) 還是步長 stride
都是 1
,在 Swift
中進行枚舉布局的時候一直是嘗試使用最少的空間來存儲 enum
,對于當前的 case
數量來說, UInt8
能夠表示 256 cases
,也就意味著如果一個默認枚舉類型且沒有關聯值的 case
少于 256
,當前枚舉類型的大小都是 1
字節。
通過上面的打印我們可以看到,當前變量
a
, b
, c
三個變量的地址相差 1
位,并且存儲的內容分別是 00
, 01
, 02
,這與我們上面說的布局理解是一致的
5.2 單個關聯值枚舉(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
字節,也就是 UInt8
,所以當前能表達 256
個 case
的情況,對于 Bool
類型來說,只需要使用低位的 0
, 1
這兩種情況,其他剩余的空間就可以用來表示沒有負載的 case
值。
可以看到不同的
case
值確實是按照我們在開始得出來的那個結論進行布局的。對于
Int
類型的負載來說,其實系統是沒有辦法推算當前的負載所要使用的位數,也就意味著當前 Int
類型的負載是沒有額外的剩余空間的,這個時候我們就需要額外開辟內存空間來去存儲我們的 case
值,也就是 8 + 1 = 9
字節。可以看出變量
a
、 b
、 c
、 d
、 e
的地址相差 16
位,這和上面打印的步長信息相一致。
5.3 多個關聯值枚舉(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)")
打印結果
我們可以看到兩個
Bool
關聯值的枚舉的大小為 1
,根據上面單個關聯值枚舉的所述不難理解。對于兩個 Int
關聯值的枚舉的大小為 9
,是因為創建一個枚舉值時有且只有一個關聯值,但是還需要 1
個字節去存儲其他 case
枚舉值( three
和 four
) 所以當前只需要 8
字節 + 1
字節我們可以看到當前內存存儲的分別是
00
、01
、 40
、 41
、 80
、 81
,這里在存儲當前的 case
時候會使用到 common spare bits
,首先 bool
類型需要 1
字節,也就是 8
位,對于 bool
類型來說,我們存儲的無非就是 0
或 1
,只需要用到 1
位,所以剩余的 7
位,這里我們都統稱為 common spare bits
,對于當前的 case
數量來說我們完全可以把所有的情況放到 common spare bits
所以這里只需要 1
字節就可以存儲所有的內容了。對于
00
、 01
、 40
、 41
、 80
、 81
,其中 0
、 4
、 8
稱之為 tag value
, 0
、 1
稱之為 tag index
。
一般來說,有多個負載的枚舉時,當前枚舉類型的大小取決于當前最大關聯值的大小。
enum LJLEnum{
case one(Bool)
case two(Int)
case three
case four
}
print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")
// 打印結果
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)")
// 打印結果
LJLEnum.Size:24
這里為什么不是 sizeof(Int) * 2 + sizeof(rawVlaue)
= 17
呢?對于 two (Int, Bool, Int)
類型的由于字節對齊的原因所以它的存儲大小為 8 * 3
= 24
,又由于中間的 Bool
實際值占用 1
個字節因此中間 8
字節還有剩余控件去存儲其他的 case
值。所以這里是 24
字節。那么將 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
字節就能放下其他的 case
,那為什么這里不字節對齊呢?其實是因為最后會因為步長 24
的原因會對這里進行補齊。
注意對于只有一個 case
的枚舉,不需要用任何東?來去區分當前的 case
,所以它的大小是 0
。
enum LJLEnum{
case one
}
print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")
// 打印結果
LJLEnum.Size:0
6. 遞歸枚舉
遞歸枚舉是一種枚舉類型,它有一個或多個枚舉成員使用該枚舉類型的實例作為關聯值。使用遞歸枚舉時,編譯器會插入一個間接層。你可以在枚舉成員前加上 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
關鍵字來表明它的所有成員都是可遞歸的。
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)
在匯編中查看
可以發現這里調用了
swift_allocObject
, 在之前的文章 Swift探索(一): 類與結構體(上) 中我們就探討過, swift_allocObject
就是在堆空間中分配內存空間。對于 indirect
關鍵字放在 enum
前面也就意味著當前這個 enum
的大小都是用引用類型在堆空間中存儲。當 indirect
關鍵字放在 case
前面,那么就只有這個 case note
是存儲在堆空間中,其中 case empty
是存儲在 __DATA.__common
(存儲沒有初始化過的符號聲明)的 section
中
二:可選值(Optional)
1. 什么是可選值
class Person {
var age: Int?
var name: Optional<String> = nil
}
age
和 name
我們就稱之為可選值
2.可選值的本質
在
Swift
源碼中可以發現可選值實際上就是一個枚舉,并且有兩個 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
守護一定有值。如果沒有,直接返回。 通常判斷是否有值之后,會做具體的邏輯實現,通常代碼多如果用 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
對象發送消息什么也不會發生,但是在 Swift
中是沒有辦 法向一個 nil
對象直接發送消息,但是借助可選鏈可以達到類似的效果。
let str: String? = "abc"
let upperStr = str?.uppercased()
print(upperStr)
var str2: String?
let upperStr2 = str2?.uppercased()
print(upperStr2)
// 打印結果
Optional("ABC")
nil
同意可選鏈對數組和喜愛
var closure: ((Int) -> ())?
print(closure?(1)) // closure 為 nil 不執行
let dict = ["one": 1, "two": 2]
print(dict["one"]) // Optional(1)
print(dict["three"]) // nil
// 打印結果
nil
Optional(1)
nil
3. ?? 運算符(空合并運算符)
( a ?? b
) 將對可選類型 a
進行空判斷,如果 a
包含一個值就進行解包,否則就返回一個默認值 b
。
- 表達式
a
必須是Optional
類型 - 默認值
b
的類型必須要和a
存儲值的類型保持一致
var age: Int? = 10
var x = age ?? 0
print(x)
// 打印結果
10
4. 運算符重載
源碼中我們可以看到除了重載了 ??
運算符, Optional
類型還重載了 ==
, ?=
等等運算符,實際開發中我們可以通過重載運算符簡化我們的表達式。
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)
// 打印結果
Vector(x: 30, y: 50)
Vector(x: -30, y: -50)
infix operator **: AdditionPrecedence
// 運算組名稱: LJLPrecedence 優先級低于: AdditionPrecedence 結合方式: 左結合
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)
// 打印結果
Vector(x: 200, y: 600)
Vector(x: 300, y: 1000)
5. 隱士解析可選類型
隱式解析可選類型是可選類型的一種,使用的過程中和非可選類型無異。它們之間唯一的區別是,隱式解析可選類型是你告訴對 Swift
編譯器,在運行時訪問時,值不會為 nil
。
var age: Int
var age1: Int!
var age2: Int?
let x = age1 % 2
let y = age2 % 2
其中
age1
不用做解包的操作,編譯器已經做了
@IBOutlet weak var btn: UIButton!
IBOutlet
類型是 Xcode
強制為可選類型的,因為它不是在初始化時賦值的,而是在加載視圖的時候。可以把它設置為普通可選類型,但是如果這個視圖加載正確,它是不會為空的。
6. 可選值有關的高階函數
-
map
:這個方法接受一個閉包,如果可選值有內容則調用這個閉包進行轉換
var dict = ["one": "1", "two": "2"]
let result = dict["one"].map{ Int($0) }
print(result)
// 打印結果
Optional(Optional(1))
上面的代碼中我們從字典中取出字符串 1
,并將其轉換為 Int
類型,但因為 String
轉換成 Int
不一定能成功,所以返回的是 Int?
類型,而且字典通過鍵不一定能取得到值,所以 map
返回的也是一個 Optional
,所以最后上述代碼 result
的類型為 Int??
類型。
-
flatMap
:可以把結果展平成為單個可選值
var dict = ["one": "1", "two": "2"]
let result = dict["one"].flatMap{ Int($0) } // Optional(1)
print(result)
// 打印結果
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)
// 打印結果
["1", "2", "3"]
[1, 2, 3]