结构体和类模块分两篇笔记来学习:
- 第一篇:
- 结构体和类的区别
- 分析类和结构体可变性
- 以一个具体的例子来学习使用类和结构体的区别,以及如何使用写时复制来解决结构体内部引用类型的复制
- 最后学习函数闭包的可变性
- 第二篇:
- 类和结构体的内存分析
- 何时使用类何时使用结构体
本篇开始学习第一部分,go!
1.结构体(和枚举)和类的区别
结构体 (和枚举) 是值类型,而类是引用类型。在设计结构体时,我们可以要求编译器保证不可变性。而对于类来说,我们就得自己来确保这件事情。
内存的管理方式有所不同。结构体可以被直接持有及访问,但是类的实例只能通过引用来间接地访问。结构体不会被引用,但是会被复制。也就是说,结构体的持有者是唯一的,但是类却能有很多个持有者。
除非一个类被标记为 final,否则它都是可以被继承的。而结构体 (以及枚举) 是不能被继承的。想要在不同的结构体或者枚举之间共享代码,我们需要使用不同的技术,比如像是组合,泛型,以及协议扩展等。
2.实体和值
实际开发中有些类型的对象我们需要控制它们的生命周期,或者比较两个对象是否指向同一个内存地址,比如文件句柄、通知中心、网络接口、数据库连接、ViewContrller等,这些都是引用类型。有些类型我们不需要声明周期,比较的时候也只是需要比较所对应的值是否相等,比如URL,二进制数据,日期,错误,字符串,通知以及数字等,这些类型都是值类型,可以使用结构体实现。
引用语义和值语义:当我们将结构体变量传递给一个函数时,函数将接收到结构体的复制,它也只能改变它自己的这份复制。这叫做值语义 (value semantics),有时候也被叫做复制语义。而对于对象来说,它们是通过传递引用来工作的,因此类对象会拥有很多持有者,这被叫做引用语义
值类型的复制优化和值语义的写时复制:编译器对结构体的复制进行优化,只是按照字节进行浅复制。写时复制由开发者实现,想要实现写时复制,需要检测所包含的类是否有共享的引用。
3.不可控制的可变性 (为何不用类)
Objective-C 的很多面向对象编程语言种,数据默认是可变的,这在多线程编程里会造成非常大的麻烦,而解决的方法就是通过不可变性的值类型来保障线程安全。
-
类型的可变性无论是在oc还是swift中都是无法彻底控制的。
class File { let data: NSMutableData init(data: NSMutableData) { self.data = data } }
虽然使用了let来控制不能改变属性所指向的对象,但是可以修改当前所指向对象的值。
4.值类型
引用一个例子做对比引用类型和值类型:
let inputParameters = [
kCIInputRadiusKey: 10,
kCIInputImageKey: image
]
let blurFilter = CIFilter(name: "CIGaussianBlur",
withInputParameters: inputParameters)!
let secondBlurFilter = blurFilter
secondBlurFilter.setValue(20, forKey: kCIInputRadiusKey)
CIFilter是CoreData里的一个滤镜类,第二个滤镜的配置变动会影响第一个滤镜的配置。可以通过复制时手动复制来避免影响:
let otherBlurFilter = blurFilter.copy() as! CIFilter
otherBlurFilter.setValue(20, forKey: kCIInputRadiusKey)”
接下来使用结构体来实现:
struct GaussianBlur {
var inputImage: CIImage
var radius: Double
}
var blur1 = GaussianBlur(inputImage: image, radius: 10)
blur1.radius = 20
var blur2 = blur1
blur2.radius = 30”
这种复制听上去有点浪费,但是编译器会为我们优化,此外如果如果使用写时复制的话,实际对数据的复制只会在其中某个值实际改变的时候才会发生。其实这也是 Swift 数组的工作方式。接下来使用扩展对GaussianBlur 结构体上添加一个 outputImage 属性:
extension GaussianBlur {
var outputImage: CIImage {
let filter = CIFilter(name: "CIGaussianBlur", withInputParameters: [kCIInputImageKey: inputImage,kCIInputRadiusKey: radius])!
return filter.outputImage!
}
上面的代码在每次 outputImage 被访问时都会创建一个新的滤镜。这并不是很高效的做法,如果我们多次访问这个属性,会有好多新的滤镜被创建出来。
为了避免这种性能的浪费,采用另一种高效的做法,不像上面那样将值存在结构体中,这次采用直接存储CIFilter实例,然后通过自定义属性进行修改:
struct GaussianBlur {
private var filter: CIFilter
init(inputImage: CIImage, radius: Double) {
filter = CIFilter(name: "CIGaussianBlur", withInputParameters: [
kCIInputImageKey: inputImage,
kCIInputRadiusKey: radius
])!
}
}
接下来,我们实现 inputImage 和 radius 属性,来直接访问和修改滤镜的属性:
extension GaussianBlur {
var inputImage: CIImage {
get { return filter.valueForKey(kCIInputImageKey) as! CIImage }
set { filter.setValue(newValue, forKey: kCIInputImageKey) }
}
var radius: Double {
get { return filter.valueForKey(kCIInputRadiusKey) as! Double }
set { filter.setValue(newValue, forKey: kCIInputRadiusKey) }
}
}
最后,要取出输出图像,我们可以直接使用滤镜上的 outputImage 属性:
extension GaussianBlur {
var outputImage: CIImage {
return filter.outputImage!
}
}
此时可以正常的满足需求了:
var blur = GaussianBlur(inputImage: image, radius: 25)
blur.outputImage
但是如果复制结构体的话就会有个问题:其中的所有值类型将被复制,而引用类型却只有对于对象的引用会被复制,对象本身不被复制。如果对blur进行复制产生otherBlur时,blur和otherBlur中的滤镜将指向同一个CIFilter实例!
5.写时复制
Swift 在实现值语义的结构体时,其底层使用了可变的对象,这也正是 Swift 中最强大的特性之一。我们从值语义里获益良多,同时又可以保持代码高效。然而,正如我们所看到的,这种做法可能导致意外的数据共享。
通过写时复制这种技术可以实现:在每次结构体被改变时,去复制一个封装后的对象,以此来避免共享。其实Swift 的很多数据结构都是以这种方式工作的。
配合上一节的滤镜例子,我们可以通过写时复制来避免滤镜的共享:
extension GaussianBlur {
private var filterForWriting: CIFilter {
mutating get {
filter = filter.copy() as! CIFilter
return filter
}
}
}
mutating修饰符作用是使方法能够修改结构体的变量。这让我们得以更改滤镜的设置方法,在改变值之前先进行复制:
extension GaussianBlur {
var inputImage: CIImage {
get { return filter.valueForKey(kCIInputImageKey) as! CIImage }
set {
filterForWriting.setValue(newValue, forKey: kCIInputImageKey)
}
}
var radius: Double {
get { return filter.valueForKey(kCIInputRadiusKey) as! Double }
set {
filterForWriting.setValue(newValue, forKey: kCIInputRadiusKey)
}
}
}
上面这种方式可以按照预想工作,但是却带来了性能上的代价:每次我们改变滤镜值的时候,滤镜都会被复制,即便是我们只是做一些本地修改,而没有共享结构体时,这个复制也会发生”
6.高效的写时复制
在 Swift 中有一个方法,isUniquelyReferencedNonObjC,它会检查一个类的实例是不是唯一的引用。我们可以使用它来在保证结构体的值语义的同时,避免不必要的复制。
不幸的是,isUniquelyReferencedNonObjC 函数只对 Swift 对象有用,而 CIFilter 是一个 Objective-C 类。想要绕过这个限制,我们可以创建一个简单的 Box 封装类型,来把任意的 Swift 类封装进去:
final class Box {
var unbox: A
init(_ value: A) { unbox = value }
}
struct GaussianBlur {
private var boxedFilter: Box = {
var filter = CIFilter(name: "CIGaussianBlur",withInputParameters: [:])!
filter.setDefaults()
return Box(filter)
}()
var filter: CIFilter {
get { return boxedFilter.unbox }
set { boxedFilter = Box(newValue) }
}
private var filterForWriting: CIFilter {
mutating get {
if !isUniquelyReferencedNonObjC(&boxedFilter) {
filter = filter.copy() as! CIFilter
}
return filter
}
}
}
这正是 Swift 数组内部的工作方式。当你创建一个新的数组的复制时,它背后的数据是一样的。只有当你要修改这个数组时,才会进行复制,这样一来,对该数组的更改不会影响到其他的数组。当在一个结构体中使用类时,我们需要保证它确实是不可变的。如果办不到这一点的话,我们就需要 (像上面那样的) 额外的步骤。或者就干脆使用一个类,这样我们的数据的使用者就不会期望它表现得像一个值。
7.闭包的可变性
闭包和函数也是引用类型的,如果进行复制,两个函数或闭包对象将共享同样的状态:
var i = 0
func uniqueInteger() -> Int {
i += 1
return i
}
let otherFunction: () -> Int = uniqueInteger
此时uniqueInteger和otherFunction将共同享用i变量。使用对闭包的再次封装,使得返回享用共同变量的闭包的方式来解决问题:
func uniqueIntegerProvider() -> () -> Int {
var i = 0
return {
i += 1
return i
}
}