Swift相关:
1、Enum
Swift的枚举非常好用!
1.1 枚举的基本用法
swift 中通过 enum 关键字来声明一个枚举,它和类、结构体一样,枚举中也可以添加异变方法(mutaing)、计算属性、扩展(extension),遵循协议;枚举是值类型,存储在栈区。
enum Swagger {
case Zed
case Fiona
case Jax
}
// 和这个写法一样
enum Swagger {
case Zed, Fiona, Jax
}
1.2 关联值
在swift中,枚举不能定义存储属性,如果我们想用枚举类型来表示更复杂的类型,就需要通过关联值来实现,它可以为枚举值存储一些必要的数据:
enum Swagger {
// zed的攻击,防御和能量
case zed(attack: Int, defense: Int, power: Int)
case jax(attack: Int, defense: Int)
}
模式匹配
var zed = Swagger.zed(attack: 100, defense: 60, power: 200)
var jax = Swagger.jax(attack: 120, defense: 100)
func league(sg: Swagger) {
switch sg {
case .zed(let attack, let defense, var power):
if power > 80 { power -= 80 }
print("\(attack) \(defense) \(power)")
case let .jax(attack, defense):
print("\(attack) \(defense)")
}
}
func test(){
league(sg: zed)
league(sg: jax)
}
这种模式匹配的方式,可以把关联值或枚举值定义为 let 和 var,只是取决于后续是否需要修改,也可以使用 defalut 关键字
enum Weak: String {
case MONDAY
case TUEDAY
case WEDDAY
case THUDAY
case FRIDAY
case SATDAY
case SUNDAY
}
let currentWeak: Weak = Weak.MONDAY
switch currentWeak{
case .SATDAY, .SUNDAY: print("Happy day")
default : print("Sad day")
}
1.3 原始值
枚举成员使用相同类型的默认值,这个默认值叫做原始值
enum SwagNum: String {
case one = "one"
case two = "two"
case three = "three"
}
var one = SwagNum.one
print(one.rawValue) // one
在枚举 Season 的后面加上 : 并指定具体的类型,这个时候枚举的原始值默认就是这个类型。这个类型可以是字符串、字符、任意的整数值,或者是浮点类型。我们可以通过 rawValue 拿到枚举成员的原始值。
注意:原始值并不占用枚举变量的内存。
隐式原始值(Implicitly Assigned Raw Values)
如果枚举的原始值类型是 Int、String,Swift 会自动分配原始值。如果枚举的原始值是 Int 类型的,自动分配的原始值从第一个成员开始,下标从 0 计算,依次 +1。代码如下
enum SwagNum: Int {
case one, two, three, four
}
print(SwagNum.one.rawValue) // 0
print(SwagNum.two.rawValue) // 1
print(SwagNum.three.rawValue) // 2
print(SwagNum.four.rawValue) // 3
假设把 three 的原始值指定成 5,那么从 three 开始,下标从 5 开始计算,依次 +1。
enum SwagNum: Int {
case one, two, three = 5, four
}
print(SwagNum.one.rawValue) // 0
print(SwagNum.two.rawValue) // 1
print(SwagNum.three.rawValue) // 5
print(SwagNum.four.rawValue) // 6
1.4 枚举的内存布局
接下来我们看一下枚举的大小,定义一个枚举 Password,如下:
enum Password {
case number(Int, Int, Int, Int)
case other
}
print(MemoryLayout<Password>.size) // 33
print(MemoryLayout<Password>.stride) // 40
print(MemoryLayout<Password>.alignment) // 8
通过打印发现,枚举占用 33 个字节,系统分配了 40 个字节,并且 8 字节对齐。那这是怎么一回事呢?
number 有四个 Int 类型的关联值,那一个 Int 类型占 8 个字节,四个 Int 类型占 32 个字节。成员 other 没有关联值,占 1 个字节。
那为什么占 33 个字节呢? 32 个字节已经可以存储 number 或者 other 了。
假设我定义一个枚举变量,每个枚举变量的值为 number,而 number 占 32 个字节。如果我用这 32 个字节来存储 other,没有任何问题。这个时候我反过来,假设这个枚举变量的值为 other,而 other 占 1 个字节。此时 1 个字节确实可以存储 other,但存储不了 number。因为系统不确定你这个枚举的变量将来存储的是 number 还是 other,所以,直接分配给这个枚举 33 个字节。而这个枚举是 8 字节对齐,所以系统最后分配了 40 个字节来存储枚举变量的值。
枚举也有一个比较特殊的地方,枚举占用的内存大小和分配的内存大小和关联值的位置也有关系:
enum Password {
case number(Int, Int, Bool, Bool)
}
print(MemoryLayout<Password>.size) // 18
print(MemoryLayout<Password>.stride) // 24
print(MemoryLayout<Password>.alignment) // 8
enum Password {
case number(Int, Bool, Int, Bool)
}
print(MemoryLayout<Password>.size) // 25
print(MemoryLayout<Password>.stride) // 32
print(MemoryLayout<Password>.alignment) // 8
注意,成员 number 的关联值,前两个为 Int 类型的时候,枚举占用 18 个字节,系统分配了 24 个字节。当我把第二个 Int 类型的关联值放到第三个位置的时候,枚举占用 25 个字节,系统分配了 32 个字节。
明明没有往枚举里添加新的成员,只是把位置互换,枚举的内存大小就发生了改变。这是为什么呢?
首先,Int 类型占 8 个字节,Bool 类型占 1 个字节。所以第一种情况前面两个关联值是 Int 类型的时候,枚举占用 18 个字节(8 + 8 + 1 + 1 = 18)。第二种情况,把 Int 类型放到了第三个位置,那应该也等于 18 个字节才对(8 + 1 + 8 + 1 = 18),但通过打印,实际占用的是 25 个字节。这个怎么解释呢?
以第二个为例,枚举在计算内存的大小时候是这样的:
- 第一个关联值占 8 字节,这个时候内存对齐为 8 字节。
- 到了第二个关联值的时候,8 + 1 = 9,因为当前的枚举已经是 8 字节对齐了,这个时候系统会进行内存对齐,所以当到了第二个的时候,此时应该枚举的内存应该是 16 字节对齐。
- 接下来到第三个关联值,因为是 Int 类型,所以 16 + 8 = 24,到这里就算进行内存对齐也占用 24 个字节。
- 最后一个关联值为 Bool 类型,占 1 个字节,所以 24 + 1 = 25,最终这个枚举的 number 成员占用 25 个字节,并且,它自身也是占用 25 个字节(因为只有一个成员 number)。
那为什么到最后一个的时候不进行内存对齐了呢,那是因为后面已经没有了关联值了,25 个字节的内存已经足够存储 number 的值,所以没有必要在这里进行内存对齐,在最后进行分配的时候,系统会整体的对枚举进行内存分配。
所以,枚举的内存大小和关联值的大小有关,并且还与关联值的存储位置有关系
1.5 关联值和原始值的区别
枚举的关联值和原始值在本质上的区别就是,关联值占用枚举的内存,而原始值不占用枚举的内存,
并且 rawValue 本质上是一个计算属性。rawValue 的实现大概应该是这样:
enum SwagNum: Int {
case one, two, three
var rawValue: Int {
get {
switch self {
case .one:
return 1
case .two:
return 2
case .three:
return 3
}
}
}
}
print(SwagNum.one.rawValue) // 1
print(SwagNum.two.rawValue) // 2
print(SwagNum.three.rawValue) // 3
枚举的原始值不占内存大小的原因是rawValue 是一个计算属性,而计算属性的本质就是方法,方法并不占用枚举的内存。
1.6 递归枚举
递归枚举是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。可以在枚举成员前加上 indirect 来表示该成员可递归。
我们来看下面的代码:
indirect enum Swagger {
case number(Int)
case sum(Swagger, Swagger)
case diff(Swagger, Swagger)
}
我们也可以单独对枚举的某个成员加上 indirect 修饰,代码如下:
enum Swagger {
case number(Int)
indirect case sum(Swagger, Swagger)
indirect case diff(Swagger, Swagger)
}
在使用时可以定义一个方法,这个方法用来处理枚举的成员,如:
let one = Swagger.number(1)
let two = Swagger.number(2)
let three = Swagger.number(3)
let sum = Swagger.sum(one, two)
let diff = Swagger.diff(sum, three)
func calculate(_ num: Swagger) -> Int {
switch num {
case let .number(value):
return value
case let .sum(left, right):
return calculate(left) + calculate(right)
case let .diff(left, right):
return calculate(left) - calculate(right)
}
}
print(calculate(diff))
2、Optional
2.1 可选值
可选值,一般也叫可选类型,它允许将值设置为 nil。在类型后面添加一个问号(?)来定义一个可选值:
var name: String? = "Swagger"
name = nil
var age: Int?
age = 10
除了可以用问号(?)表达之外,也可以使用 Optional 关键字来表示:
var name: Optional<String> = "Swagger"
name = nil
var age: Optional<Int> // age 默认就是 nil
age = 10
2.2 ?? 运算符 (空合并运算符)
( a ?? b ) 将对可选类型 a 进行空判断,如果 a 包含一个值就进行解包,否则就返回一个默认值 b
- 表达式 a 必须是 Optional 类型
- 默认值 b 的类型必须要和 a 存储值的类型保持一致
2.3 可选链
我们都知道在 OC 中给一个 nil 对象发送消息什么也不会发生,在 Swift 中是没有办法向一个 nil 对象直接发送消息,但是借助可选链可以达到类似的效果。我们看下面两段代码:
let str: String? = "abc"
let upperStr = str?.uppercased() // Optional<"ABC">
var str: String?
let upperStr = str?.uppercased() // nil
2.4 隐式解析可选类型
隐式解析可选类型是可选类型的一种,使用的过程中和非可选类型一样。它们之间唯一的区别是,隐式解析可选类型其实就是告诉Swift编译器,在运行访问时,值不会为nil。