一:枚举
- 枚举的定义
Swift
中的枚举定义枚举类型很简单:
//枚举
enum Season{
case spring
case summer
case autumn
case winter
}
//也可以这样定义
enum Season{
case spring,summer,autumn,winter
}
同 OC
不同的是,Swift
中的枚举不单单可以是Int
类型,也可以是其他类型
- 关联值 (Associated Values)
枚举的关联值是将枚举的成员值
和其他类型的值
关联存储在一起.
enum Score{
case points(Int)
case grade(Character)
}
var score = Score.points(90)
print(score) //打印 points(90)
score = .grade("A")
print(score) //打印 grade("A")
枚举Score
的两个成员points
和grade
分别关联了Int
类型和Character
类型.枚举的关联值是存储在枚举变量的内存中的
,也就是说90
和A
都保存在枚举变量score
的内存中.
枚举的关联值都存储在枚举变量中,因为枚举的关联值是不断变化的,每一个枚举变量都有对应的关联值,比如:
var score1 = Score.points(90)
var score2 = Score.points(78)
var score3 = Score.points(59)
var score4 = Score.grade("D")
90,78,59,D
分别对应4个枚举变量,这些值都要保存在各自枚举变量的内存中.
- 原始值 (Raw Values)
枚举成员可以用相同类型的默认值预先对应,这个默认值就是原始值:
//原始值
enum Mood: Character{
case smile = "😀"
case cry = "😭"
case heart = "😍"
}
我们可以使用rawValue
访问枚举成员的原始值:
如果枚举的原始值类型是Int , String
,Swift 会自动为枚举成员分配原始值,成员是什么原始值就是什么:
如果是Int
类型,原始值默认是从0开始,如果设置了某一个成员的原始值,那么下一个成员的原始值递增:
- 枚举内存空间
要了解枚举的内存空间是如何分配的,我们需要先搞清楚3个方法:
4.1MemoryLayout.stride(ofValue:)
: 系统分配了多少内存; 等价于MemoryLayout<Int>.stride
4.2MemoryLayout.size(ofValue:)
: 实际占用了多少内存; 等价于MemoryLayout<Int>.size
4.3MemoryLayout.alignment(ofValue:)
: 多少字节对齐; 等价于MemoryLayout<Int>.alignment
以有关联值的枚举类型Score
为例,分别打印一下他的内存大小:
enum Score{
case points(Int)
case grade(Character)
}
var score = Score.points(90)
print("Character 类型占用多少字节内存: \(MemoryLayout<Character>.size)")
// 系统分配了多少内存 24
print(MemoryLayout.stride(ofValue: score))
// 实际占用了多少内存 17
print(MemoryLayout.size(ofValue: score))
// 多少字节对齐 8
print(MemoryLayout.alignment(ofValue: score))
打印结果如下:
Character 类型占用多少字节内存: 16
24
17
8
从打印结果可以看到,系统给Score
分配了24个字节.但是它实际只占用了17个字节,并且是以8字节对齐的.
分析一下这个24个字节是怎么来的?首先Int
类型在Swift
中是占用8个字节的.Character
占用16个字节,难道系统分配的24个字节是Int
+Character
算出来的?当然不是这样!这个24是Character
的16 个字节 加上一个标识位 1 个字节 , 也就是 17 然后再以 8 字节对齐后的结果.
这个标识位是怎么来的呢?
因为case points(Int)
情况是需要8个字节来存储的;而case grade(Character)
是需要16个字节来存储的.但是不管是哪种情况 16 个字节都够存储.所以case points(Int)
和case grade(Character)
都存储在这16个字节中.但是都共用这 16 个字节,系统怎么区分是case points(Int)
还是case grade(Character)
呢?所以这就需要一个额外的标识位来区分是哪种成员.
- 再来看看下面这种情况:
//枚举
enum Season{
case spring
case summer
case autumn
case winter
}
var season = Season.autumn
// 系统分配了多少内存
print(MemoryLayout.stride(ofValue: season))
// 实际占用了多少内存
print(MemoryLayout.size(ofValue: season))
// 多少字节对齐
print(MemoryLayout.alignment(ofValue: season))
打印结果
1
1
1
打印结果全是1,像这种简单的枚举,系统只需要1个标识位来标识哪种情况就可以了.根本就不需要额外的空间.
- 如果加上原始值呢?
//枚举
enum Season: String{
case spring
case summer
case autumn
case winter
}
var season = Season.autumn
// 系统分配了多少内存
print(MemoryLayout.stride(ofValue: season))
// 实际占用了多少内存
print(MemoryLayout.size(ofValue: season))
// 多少字节对齐
print(MemoryLayout.alignment(ofValue: season))
打印结果
1
1
1
打印结果还是1.因为原始值的成员是固定的,是不会改变的,比如:
var season1 = Season.autumn
var season2 = Season.autumn
var season3 = Season.autumn
season1
, season2
, season3
的原始值都是autumn
,既然如此,编译器肯定不会让枚举变量都存储一个相同的值,这是很浪费内存的.况且根本也不需要存储,只需要1个字节的标识位来区分具体是哪种情况就好了.
有人可能会疑惑,那枚举的原始值存储在什么地方呢?其实我们根本就不用纠结这个问题,也许原始值根本就没有被存储,比如说原始值的rawValue
可能是这样实现的:
enum Season: String{
case spring
case summer
case autumn
case winter
//匹配后直接返回
func rawValue() -> String{
switch self {
case 0:
return "spring"
case 1:
return "summer"
case 2:
return "autumn"
case 3:
return "winter"
}
}
}
总结:
枚举的关联值会占用枚举变量的内存,因为关联值是变化的;而枚举的原始值不会占用枚举变量的内存.
二:可选项 (Optional)
可选项也就是可选类型,在类型后面加一个问号 ?
来定义一个可选项,它允许将值设置为nil
.
如果不是可选项类型,是不允许赋值nil
的:
可选项的初始值就是nil
,下面两句代码是等价的:
var age: Int?
var age: Int? = nil
- 强制解包
- 我们可以吧可选项理解为一个盒子,是对其他类型的一种包装:
如果为nil
,就是一个空盒子
如果不为nil
,盒子里装的就是:被包装类型的数据
被包装的数据是不能直接使用的,他是Optional
类型:
我们要想使用被包装的数据,就需要使用感叹号 !
来进项强制解包:
需要注意的是,如果对值为nil
(空盒子)进行强制解包,会报错:
所以我们在解包时就要判断可选项是否包含值,以免报错.
var age: Int?
if age != nil{
print("age 有值")
}else{
print("age 为nil")
}
还有一种更方便的方法判断可选项是否有值:可选项绑定
- 可选项绑定
var age: Int? = 10
if let age = age{
print(age)
}else{
print("age 为 nil")
}
可以使用if let age = age
进行可选项绑定判断可选项是否有值,如果可选项有值就自动解包,并把解包后的值赋给临时的常量 let 或者 变量 var,并返回 true,否则返回false
需要注意的是,可选项绑定不能和&&
预算符一起使用:
如果在可选项绑定时需要判断多个条件同时满足,可以用逗号 ,
关联:
- 空合并运算符 ??
我们可以用两个问号??
来表示空合并运算符,它的格式是:
-
a ?? b
. 要求如下:
. 1:a
必须为可选项,b
可以是可选项也可以是非可选项.
. 2:b
跟a
的存储类型必须相同 - 运算规则如下:
. 1: 如果a
不为nil
就返回a
,返回a
的同时还要看看b
是不是可选项.如果b
不是可选项,返回a
时将自动解包
. 2: 如果a
为nil
就返回b
可能有些绕,我们做一下联系熟悉一下:
示例1:左侧不是可选项
示例2:左侧和右侧类型不一致
示例3:a
为非空可选项,b
为非可选项
示例4:a
为非空可选项,b
为可选项
总结:空合并运算符的返回结果取决于
b,如果
b是可选类型,返回的结果就是可选类型;如果
b不是可选类型,返回的结果就不是可选类型.
-
??
和if let
配合使用,
如果我们需要判断a , b
任何一个有值都为true
,之前我们都是这样:
var a: Int?
var b: Int?
if a != nil || b != nil{
print("a,b至少有一个有值")
}else{
print("a,b都为nil")
}
我们可以使用可选项绑定和??
配合使用:
var a: Int?
var b: Int?
if let _ = a ?? b{
print("a,b至少有一个有值")
}else{
print("a,b都为nil")
}
-
guard
语句
guard
语句特别适合用来提前退出
,它的语法如下:
解释:
. 1: 当guard
语句的条件为false
时,就会执行大括号里面的代码,和if
语句相反.
. 2: 当guard
语句的条件为true
时,就会跳过guard
语句
我们之前写登录语句时,通常都会像这样写:
而用guard
语句实现起来就会简单很多:
var userInfo = ["userName": "Tom" , "passWord": "123456"]
func login2(user:[String : String]){
//guard 语句解包后的 userName 作用域是整个方法体的,所以不用定义临时变量存储
guard let userName = user["userName"] else {
print("用户名为空")
return
}
guard let passWard = user["passWord"] else {
print("密码为空")
return
}
//走到这里,就说明用户名和密码都有值
print("登录成功,用户名\(userName),密码:\(passWard)")
}
login2(user: userInfo)
注意:如果 guard 语句条件为 false 进入大括号后,最后一定不要忘了退出作用域.
-
隐式解包
如果我们能确定一个可选项肯定有值,就不需要每次再去判断可选项是否为nil
.可以在类型后面加一个感叹号 !
,让系统自动去解包,这就是隐式解包
-
字符串插值
可选项在字符串插值或者打印时,编译器会发出警告,有三种方法可以消除警告:
警告
方法一:强制解包
方法二:字符串的describing
方法
方法三:空合并运算符 ??
- 多重可选项
??
多重可选项和空合并运算符都是两个问号 ?
,它的意思是包装了盒子的盒子
.
比如说如下代码:
var age1: Int? = 10
var age2: Int?? = age1
var age3: Int?? = 10
他们的本质如下:
事实上age2
和age3
是相等的:
但是如果像这样一开始就赋值nil
,那么age2
和age3
就不是相等的了:
var age1: Int? = nil
var age2: Int?? = age1
var age3: Int?? = nil
print(age2 == age3) //false
他们的本质如下:
知道他们的本质了,我们来练习一下:
var age1: Int? = nil
var age2: Int?? = age1
var age3: Int?? = nil
let num1 = (age2 ?? 1) ?? 2
let num2 = (age3 ?? 1) ?? 2
思考一下num1,num2
的值分别是什么?
我们从左至右一步步拆解:age2 ?? 1
,age2
是一个大盒子里面包装着一个小的空的盒子,所以,age2
不为空(因为它里面有一个空盒子,有东西就不为空
),所以返回age2
,而??
右边是 1 ,也就是非可选项,所以age2
会解包为age1
,也就是nil
,然后nil ?? 2
最后返回 2,所以num1
的值为 2. num2
的值大家自己分解分解.
我们还可以通过LLDB指令frame variable -R 简写(fr v -R)
查看他们的结构: