第五章——结构体与类(可变性)

本文系阅读阅读原章节后总结概括得出。由于需要我进行一定的概括提炼,如有不当之处欢迎读者斧正。如果你对内容有任何疑问,欢迎共同交流讨论。

在多线程编程时,很容易遇到一些bug,而且这些bug往往不会直观地表现出来,从而给bug复现、调试带来了一些困难。多线程中的bug,很多情况下是由于一个线程修改了某个值,而另一个线程读取到的还是旧的值导致的。因此,越来越多的专家认为不可变的变量以及值类型有助于在多线程环境中保证程序更加不容易出错。

举个例子,我们可以用不可变的NSData替换NSMutableData

func processData(x: NSData) {
// 处理x
}
复制代码

但是这样的函数定义并不能保证x就不可变了,因为NSMutableDataNSData的子类,所以一个NSMutableData对象也可以作为参数传入processData函数中,这时x就是一个可变的对象了。

一个解决方案是在processData函数的一开始,就把x复制一份,然后副本进行处理:

func processData(x: NSData) {
let data = x.copy() as! NSData
// 处理data
}
复制代码

这时,我们确保了data是不可变的。但这么做有两个缺点:

  1. 如果参数本身就是不可变的,或者可变参数实际上没有发生改变,那这样的复制是毫无意义的行为。
  2. 每次都要复制很麻烦,也很容易忘记。

定义为let的对象不是真正不可变的。let仅仅表示它不能变为其他对象,但是并不保证它内部的属性不发生变化。而定义为let的结构体是真正不可变的。

值类型

值语义表示,当变量被复制时,复制的是这个变量自己,而不是它的引用。在Swift中,结构体和枚举具有值语义,而对象总是按引用传递。尽管结构体的复制看上去有些浪费,但是编译器会自动为此进行优化。以实现高斯模糊的结构体为例:

struct GaussianBlur {
var input: CIImage
var radius: Double
}

var blur1 = GaussianBlur(input: image, radius: 10)
var blur2 = blur1
blur2.radius = 20	// 如果没有这一行,其实Swift不会复制结构体blur1
复制代码

只有在blur2radious属性发生了变化时,才会真正复制blur1。这就是“写时复制”。在复制时,input的引用被复制,所以两个结构体其实共享了同一个CIImage对象。不过CIImage不是可变的,所以这样做没有关系。

为了返回模糊后的图片,我们改写一下结构体:

struct GaussianBlur {
private var filter: CIFilter

init(inputImage: CIImage, radious: Double) {
filter = CIFilter(name: "CIGaussianBlur",
withInputParameters:[
kCIInputImageKey: inputImage,
kCIInputRadiousKey: radious
])!
}
}

extension GaussianBlur {
var input: CIImage {
get { return filter.valueForKey(kCIInputImageKey) as! CIImage }
set { filter.setValue(newValue, forKey: kCIInputImageKey) }
}

var radius: Double {
get { return filter.valueForKey(kCIInputRadiousKey) as! Double}
set { filter.setValue(newValue, forKey: kCIInputRadiousKey) }
}
}

extension GaussianBlur {
var outputImage: CIImage {
return filter.outputImage!
}
}
复制代码

用法与刚刚类似:

var blur = GaussianBlur(input: image, radius: 10)
blur.outputImage
复制代码

但是这种写法是有问题的,因为结构体复制时会复制所有成员,而filter作为一个CIFilter类型的成员,复制时只会复制引用而不是真正的对象。所以如果我们试图复制GaussianBlur结构体:

var otherBlur = blur
otherBlur.radius = 20
复制代码

就会导致两个结构的radius都变成了20。

写时复制

值语义有利有弊。一方面,它在底层使用对象或C指针,从而允许我们创建值类型的结构体,另一方面它可能导致对象被共享。接下来我们一起学习如何用写时复制技术避免这种对象共享。

Swift中有一个函数叫isUniquelyReferencedNonObjC,它可以判断一个指针是否被唯一引用。有了这个方法,我们只要复制被多引用的对象,这样就避免了不必要的复制。悲催的是,正如这个方法名字所写,它不适用于OC的对象,只有Swift中的对象可以用它。所以,我们先要创建一个简单的包装类型Box,它内部可以包装任何类型的变量:

final class Box<A> {
var unbox: A
init(_ value: A) { unbox = value }
}
复制代码

定义好这个方法之后,在结构体中存储的成员就不是CIFilter本身,而是包装了CIFilterBox了:

private var boxedFilter: Box<CIFilter> = {
var filter = CIFilter(name: "CIGaussianBlur", withInputParameters:[:])!
filter.setDefaults()
return Box(filter)
}()

var filter: CIFilter {
get { return boxedFilter.unbox}
set { boxedFilter = Box(newValue)}
}
复制代码

然后,我们定义一个私有的成员filterForWriting,它只有在需要时才会复制:

private var filterForWriting: CIFilter {
mutating get {
if !isUniquelyReferencedNonObjC(&boxedFilter) {
filter = filter.copy() as! CIFilter
}
return filter
}
}

// Set方法中的是filterForWriting
var input: CIImage {
get { return filter.valueForKey(kCIInputImageKey) as! CIImage }
set { filterForWriting.setValue(newValue, forKey: kCIInputImageKey) }
}

var radius: Double {
get { return filter.valueForKey(kCIInputRadiousKey) as! Double}
set { filterForWriting.setValue(newValue, forKey: kCIInputRadiousKey) }
}
复制代码

这样,如果结构体的inputradius属性被修改,它在内部就会复制一份filter,也就不会影响到别的结构体了。其实这也是Swift实现数组写时复制的方法。有兴趣的读者可以通过===运算符来验证写时复制是否被正确的实现了。

在创建自定义的结构体和类的时候,要考虑它们的可变性,以及值语义。在结构体内部使用类时,要确保它真的是不可变的。如果做不到,就应该考虑使用类而不是结构体。Swift中大多数数据结构都是值类型,而OC的Foundation库中的NSArrayNSString等则需要我们自己手动管理拷贝问题。

闭包与可变性

之前我们一直强调,变量有三种方式存储:结构体、枚举、类。其实这并不完全,除此之外还有第四种:闭包。闭包和类一样,都是引用类型。比如我们在第二章——集合协议中说到的生成器,它是引用类型,所以把一个生成器赋值给另一个生成器并不意味着可以遍历集合两次,这就是为什么需要SequenceType来封装生成器对象的创建过程。

如果要得到多个互不相关的闭包,我们可以这样写:

func uniqueIntegerProvider() -> () -> Int {
var i = 0
return { ++i }
}
复制代码

这里定义了一个函数uniqueIntegerProvider,它没有参数,返回值为() -> Int类型的闭包。每次调用这个函数都可以获得一个新的闭包。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值