关于ios:Swift-Talk理解值类型

8次阅读

共计 4310 个字符,预计需要花费 11 分钟才能阅读完成。

咱们应用写时复制 copy on write 的思维,对 NSMutableData 进行封装,以此来了解咱们的规范库的实现形式。

规范库中提供的所有的根本汇合类型都是值类型,通过写时复制的思维保障了他的高效性。汇合类型是咱们比拟罕用到的数据类型,所以理解他的性能个性很重要,咱们来一起看一下写时复制是如何工作的,并且尝试本人手动实现一个。

援用类型

举个例子,咱们比拟一下 Swift 的 Data(构造体)和 Foundation 库中的 NSMutableData(类)。首先咱们应用一些字节数据来初始化 NSMutableData 实例。

var sampleBytes: [UInt8] = [0x0b,0xad,0xf0,0x0d]
let nsData = NSMutableData(bytes: sampleBytes, length: sampleBytes.count)

咱们应用了 let 来申明 nsData,然而像 NSMutableData 这样的援用类型不受 let/var 的管制。对于援用类型来说,用 let 申明代表 nsData 这个指针不能在指向别的内存,然而他指向的这个内存中的数据是能够变动的。也就是说咱们仍然能够往 nsData 中 append 数据。

nsData.append(sampleBytes, length: sampleBytes.count)

当咱们再申明一个对象,扭转其中一个对象,另一个对象也会发生变化。

let nsOtherData = nsData
nsData.append(sampleBytes, length: sampleBytes.count)
// nsOtherData 也会变 

如果咱们想产生一个独立的正本,咱们须要应用 mutableCopy(返回一个 Any 类型),咱们须要把返回值强转成咱们须要的 NSMutableData 类型。

let nsOtherData = nsData.mutableCopy() as! NSMutableData
nsData.append(sampleBytes, length: sampleBytes.count)
// nsOtherData 不变 

值类型

首先咱们也是通过 sampleBytes 来初始化一个 Data。

let data = Data(bytes: sampleBytes, count: sampleBytes.count)

如果咱们应用 let 关键字,那编译器就不会容许咱们调用类型 append 这样的办法。所以如果要扭转 data 的值,要应用 var。

var data = Data(bytes: sampleBytes, count: sampleBytes.count)
data.append(contentsOf: sampleBytes)

Data 和 NSData 最次要的不同之处是:把值赋给另一个变量时或者作为参数传到办法中,Data 总是会生成一个新的正本,然而 NSData 只会生成一个新的援用,然而两个援用指向同一个内存区域。

当咱们创立 Data 的一个正本的时候,他的所有的字段都会被复制,然而又不是立即复制,因为 Data 内存有对理论内存空间的援用,所以当构造体被复制时,也只是会生成一个新的援用,只有咱们对这个新的援用批改数据是,理论的数据才会被复制。

实现写时复制

咱们本人实现一个 Data 类型来帮咱们了解写时复制是如何工作的,咱们外部应用 NSMutableData 来理论的存储数据(只是为了更快的实现,理论的 Data 外部必定是用到更底层的数据结构来存储数据)。扭转数据的办法咱们只实现一个 append 办法。

struct MyData {var data = NSMutableData()
    
    func append(_ bytes: [UInt8]) {data.append(bytes, length: bytes.count)
    }
}

咱们能够创立一个 MyData

let data = MyData()

为了能更好的打印出 data 中存储的数据,咱们能够让 MyData 实现 CustomDebugStringConvertible 协定。

extension MyData: CustomDebugStringConvertible {
    var debugDescription: String {return String(describing: data)
    }
}

当初咱们能够调用 append 办法了。

data.append(sampleBytes)

但这是有问题的,首先咱们的 MyData 是构造体,而且创立 data 应用的是 let,咱们不应该能够批改他的值。

而且看上面的代码,他的复制行为也是有问题的,在咱们申明了一个新的援用时,并没有取得一个齐全独立的正本。

var copy = data
copy.append(sampleBytes)

print(data)
print(copy)
// copy 调用 append, data 也会扭转 

所以说咱们尽管创立了一个构造体,然而他并没有体现出值语义来。

目前,咱们在把 data 赋给一个新的变量时,尽管他是所有字段都复制,然而咱们 MyData 外部的 data 是一个 NSMutableData 援用类型,所以说 data 和 copy 这两个变量的值当初都蕴含对同一个 NSMutableData 实例的援用。

为了解决这个问题,咱们要先解决写时复制的’写时‘问题。当咱们在调用 append 办法增加数据时,咱们要把外部进行理论存储性能的 data 进行深拷贝,此时 咱们的 append 办法就必须加上 mutating 关键字,要不然编译器不容许批改构造体的变量。

struct MyData {var data = NSMutableData()
    
    mutating func append(_ bytes: [UInt8]) {print("make a copy")
        data = data.mutableCopy() as! NSMutableData
        data.append(bytes, length: bytes.count)
    }
}

当初咱们要从新生成一个 var 类型的 data 来调用 append 办法,因为编译器不容许 let 类型的调用带 mutating 关键字的办法。

var data = MyData()
var copy = data
copy.append(sampleBytes)

在咱们持续之前,进行一个小的重构,并将生成 NSMutableData 实例正本的代码提取到一个独自的属性中。

struct MyData {var data = NSMutableData()
    var dataForWriting: NSMutableData {
        mutating get {print("make a copy")
            data = data.mutableCopy() as! NSMutableData
            return data
        }
    }
    
    mutating func append(_ bytes: [UInt8]) {dataForWriting.append(bytes, length: bytes.count)
    }
}

让写时复制更高效

目前咱们的写时复制是非常简单的,就是每次当咱们调用 append 的时候,都会拷贝,不论咱们是不是这个实例的惟一持有者。

for _ in 0..<10 {data.append(sampleBytes)
}
// making a copy 会打印 10 次 

其实真正须要执行复制操作的是当咱们把 data 赋值给另一个变量后,这时调用 append 办法,因为此时有两个援用,所以须要进行深拷贝。当拷贝完结后,这两个都是援用指向的都是齐全独立的备份了,所以再一次调用时就不须要拷贝了。

所以说咱们的 MyData 构造没有问题,然而屡次拷贝会升高性能。咱们能够应用 isKnownUniquelyReferenced 这个办法来帮忙咱们实现想要的成果。

var dataForWriting: NSMutableData {
    mutating get {if isKnownUniquelyReferenced(&data) {return data}
        print("make a copy")
        data = data.mutableCopy() as! NSMutableData
        return data
    }
}

尽管咱们当初加上了 isKnownUniquelyReferenced 查看,然而运行一下测试代码还是会 copy 屡次,那是因为 isKnownUniquelyReferenced 办法只是对 Swift 类型有成果,如果是传入的 OC 类型的对象,总是会返回 false,所以咱们应该应用一个 Swift 类型来包装一下这个 data 类型。

final class Box<A> {
    let unbox: A
    init(_ value: A) {self.unbox = value}
}

咱们应用这个 Box 类来包装 NSMutableData , 最终咱们的 MyData 变成上面这样子

struct MyData {var data = Box(NSMutableData())
    var dataForWriting: NSMutableData {
        mutating get {if isKnownUniquelyReferenced(&data) {return data.unbox}
            print("make a copy")
            data = Box(data.unbox.mutableCopy() as! NSMutableData)
            return data.unbox
        }
    }
    
    mutating func append(_ bytes: [UInt8]) {dataForWriting.append(bytes, length: bytes.count)
    }
}

当初咱们的代码只对 NSMutableData 实例 copy 一次。

var data = MyData()
var copy = data
for _ in 0..<10 {data.append(sampleBytes)
}
// Prints:
// making a copy 一次 

规范库中数组和字典的实现形式其实也是相似的,只是他们用了更低级的数据结构来存储,咱们这样手动实现一次写时复制,有助于咱们更好了解他们外部的性能。

写时复制留神点

写时复制很高效,然而他不是适应于所有的场景,比如说咱们下面的 for 循环是能够的,然而如果咱们应用 reduce 来实现下面的循环,他就不起作用了。

(0..<10).reduce(data) { result, _ in
    var copy = result
    copy.append(sampleBytes)
    return copy
}

这个实现形式会生成 10 个正本,因为当咱们调用 append 时,总是有两个变量——copy 和 result——援用指向同一个实例。

所以咱们应该留神咱们代码中那些产品大量不必要正本的中央,不过咱们个别都不会这么写,所以说问题不大。

正文完
 0