本文作者:苯酚

nil 的语义

在 Objective-C 中,nil 表明空对象,它实质是一个指向 0x00000000 的指针。但关于非指针的值类型,OC 中是无法表明_没有值_这个概念的,比方 NSInteger,它可所以 0,也可所以其他任何值,但就是不存在_没有值_。

Swift 作为一种强类型的语言,它从一开始就引入了_没有值_这个概念,虽然仍是用 nil 关键字,但实践语义上有所不同。比方 Int?,它可所以 nil,也可所以 00 是一个详细的值,而 nil 不是。然而,计算机作为一个二进制的机器,它内存中保存的非 0 即 1,如何表明_没有值_呢?换句话说,nil 在内存中终究是什么?咱们能够经过简略的代码找出它在内存中的真相。

nil 在内存中的表明

/// 以下方法取 value 的地址,并从地址处向后取它在内存中的巨细 size 个字节,转为对应的数组
func bytes<T>(of value: T) -> [UInt8] {
    var value = value
    let size = MemoryLayout<T>.size
    return withUnsafePointer(to: &value, {
        $0.withMemoryRebound(
            to: UInt8.self,
            capacity: size,
            {
                Array(UnsafeBufferPointer(
                    start: $0, count: size))
            })
    })
}
var int: Int? = 0
bytes(of: int)    // [0, 0, 0, 0, 0, 0, 0, 0, 0]
int = nil
bytes(of: int)    // [0, 0, 0, 0, 0, 0, 0, 0, 1]

从上面咱们能够得知,可选的 Int? 类型比一般 Int 类型多占一个字节,用来表明是不是 没有值。假如这样的话,在 structclass 中用可选类型岂不是会糟蹋较多内存空间?由于内存对齐的原因,多一个字节,就要糟蹋剩余的 7 字节,比方:

struct N {
    var b: Int? = 2
    var a: Int? = 3
}
var n = N()
bytes(of: n)    // [2, 0, 0, 0, 0, 0, 0, 0, 0, 76, 68, 3, 1, 0, 0, 0, 
                //  3, 0, 0, 0, 0, 0, 0, 0, 0]

以上原本能够用 16 字节表明的结构体,实践上占了 25 字节(考虑结尾处内存对齐,其实占了 32 字节)。咱们在实践开发中,或许会在 class 中声明很多的可选字段,假如都这样的话,那内存使用率也太低了,有优化手法吗?

答案是有的,并且 Swift 编译器已经默默帮咱们做了。

nil 的优化

Bool

Bool 类型理论上只用 0 1 两个值,一个 bit 即可,但它却占了一整个 byte ,剩余的几个 bit 是能够用来区别是否有值的。

var b: Bool? = false
bytes(of: b) // [0]
b = true
bytes(of: b) // [1]
b = nil
bytes(of: b) // [2]

从以上成果得知,Swift 用 2 表明 Bool? 的_没有值_,所以没有内存糟蹋。这样也使得 Bool? 不再是两态的开关,而是一个三态的开关。于是经常在代码中看到看起来比较蠢的写法:

var value: Bool?
if value == true {
}

由于一般来说是不建议 Bool 值与 true 判别等的,它本身已经是 Bool 了。而在 Swift 中又用起来是那么天然……

String

String 类型不同于 Int 这种——0 也是合法值,String 的内存值为 0 是能够表明_没有值_的,所以它也没有内存糟蹋

var s: String? = "abc"
bytes(of: s)  // [97, 98, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 227]
s = ""
bytes(of: s)  // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224]
s = nil
bytes(of: s)  // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

String 在 Swift 中是一个结构体,无论字符串多长,String 变量本身只占 16 字节,短的字符串经过类似 OC 中 Tagged Pointer 的技能直接存在指针中,长的字符串需求指向堆内存地址。

Class

Class 类型同 OC 中的相同,是指针类型,空指针能够表明_没有值_,没有内存糟蹋。

class MyObject {
    var b: Int? = 2
    var a: Int? = 3
}
var o: MyObject? = .init()
bytes(of: o)  // [160, 142, 188, 2, 0, 96, 0, 0]
o = nil
bytes(of: o)  // [0, 0, 0, 0, 0, 0, 0, 0]

无论 Class 中有多少成员变量,Class 变量本身(即指向它的指针)只占 8 字节(64位体系中)。

Enum

枚举类型一般是有限的,最终总能够找到一个不在枚举范围内的值表明 没有值,也能够没有内存糟蹋。

enum Edge {
    case left
    case right
    case top
    case bottom
}
var e: Edge? = .left
bytes(of: e)  // [0]
e = .bottom
bytes(of: e)  // [3]
e = nil
bytes(of: e)  // [4],用越界值表明 nil,没有值

当然并不是一切 Enum 类型都能这样,带关联值的就或许不可。

结语

综上所述,Swift 编译器会尽或许地优化可选值的内存占用,日常开发并不需求太多关怀,但是部分状况仍要求开发者尽量少使用可选值,如结构体中接连几个可选 Int 的状况,假如 0 也能满意代码逻辑,就使用非可选值,并用 0 初始化它吧!

// 糟蹋的内存比较可观
struct My {
   var a: Int?
   var b: Int?
   var c: Int?
   var d: Int?
}

本文发布自网易云音乐技能团队,文章未经授权制止任何方式的转载。咱们常年接收各类技能岗位,假如你准备换作业,又刚好喜欢云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!