在swift中,类,结构体,枚举以及闭包捕获变量都可以作为存储结构化数据的方法.但在标准库中,绝大多数类型都是结构体,类和枚举只占很小一部分,并且很多foundation的类都对应成了结构体.另外枚举的特性和结构体非常相似,所以放在一起对比.
对比结构体(枚举)和类:
1.体是值类型,类是引用类型,在设计结构体时,我们可以要求编译器保证不可变性。而对于类来说,我们就得自己来确保这件事情。
2.结构体是直接持有并访问,类只能通过引用间接访问,结构体不会被持有只能被复制,但是类可以被多个指针引用,有多个持有者.
3.类可以通过继承来共享代码,但是结构体想要共享代码需要按情况使用不同的技术,比如组合,泛型,协议扩展.
值类型
值类型和引用类型
在OC中,我们经常需要一些有明确生命周期的对象,比如viewController,创建它,改变它,最后销毁它,如果想要比较他们,则需要比较他们是否指向同样的地址,创建和销毁时都需要做专门的操作.
但是也有一些对象不需要明确的生命周期,创建之后就不再更改,比如NSURL,并且在销毁时也不需要做什么操作,比较两个URL也不用比较地址,只要对比url即可,因此,在swift中,URL是一个结构体.
swift结构体
值永远不会改变,它们具有不可变的特性。这 (在绝大多数情况下) 是一件好事,因为使用不变的数据可以让代码更容易被理解。不可变性也让代码天然地具有线程安全的特性,因为不能改变的东西是可以在线程之间安全地共享的.
swift中结构体是值类型,但是结构体的属性却可以是可变的,你可以使用var来声明结构体的属性,这是为什么.
实际上这个var虽然指属性可以被修改,但是修改这个属性和创建一个新的结构体是等价的,当修改了这个 属性之后,新的结构体被创建出来,这个属性使用了新的值,然后替换了原来的结构体.结构体本身没有被修改,他就好像一个Int型变量,值从0换成1,0还是0,1还是1.
因此,结构体只能有一个持有者,当把结构体传给函数时,函数获得结构体的一份复制,函数也只能修改它的这份复制,这叫做值语义.而对象传递引用,可以有多个持有者,叫做引用语义.
值总是需要复制,因此编译器自然会做优化,需要注意的是,这个值语义优化并不是写时复制,写时复制是开发者自己实现的,后面会讲到.
如果一个结构体只由其他结构体组成,那编译器可以确保不可变性。同样地,当使用结构体时,编译器也可以生成非常快的代码。举个例子,对一个只含有结构体的数组进行操作的效率,通常要比对一个含有对象的数组进行操作的效率高得多。这是因为结构体通常要更直接:值是直接存储在数组的内存中的。而对象的数组中包含的只是对象的引用。最后,在很多情况下,编译器可以将结构体放到栈上,而不用放在堆里。
可变性
可变性经常是导致异常的原因.
let mutableArray: NSMutableArray = [1,2,3]
for _ in mutableArray {
mutableArray.removeLastObject()
}
这是一段会crash的代码,迭代器以数组为基础工作,改变数组会改变迭代器的状态,引发异常.
var mutableArray = [1, 2, 3]
for _ in mutableArray {
mutableArray.removeLast()
}
这段代码不会crash,因为mutableArray是值类型,迭代器持有的mutableArray是值语义,它一直都是三个元素,
在类中,我们可以使用 var 和 let 来控制属性的可变和不可变性。比如,我们可以创建一个和Foundation 中Scanner 类似的扫描器,不过区别是它将读取二进制数据。在 Scanner 类中,你可以从一个字符串中扫描值,每次成功获得一个扫描值后就进行步进。类似地,我们的BinaryScanner 类将持有一个位置属性 (它是可变的,因为它是用 var 声明的),以及原始的数据 (它是不可变的,因为它是用 let 声明的)。
同时它还有一个函数,来扫描字节,并且改变position.
class BinaryScanner {
var position: Int
let data: Data
init(data: Data) {
self.position = 0
self.data = data
}
func scanByte() -> UInt8? {
guard position < data.endIndex else { return nil }
position += 1
return data[position-1]
}
}
定义一个函数来使用BinaryScanner,然后来试一下.
func scanRemainingBytes(scanner: BinaryScanner) {
while let byte = scanner.scanByte() {
print(byte)
}
}
let scanner = BinaryScanner(data: Data("hi".utf8))
/* 104 105 */
现在到了关键的时候,position是可变的,如果在两个不同的线程调用scanRemainingBytes的话,就会进入竞态条件,在线程1中,position < data.endIndex为真,但是在position += 1之前,线程2先执行了position += 1,然后cpu切换到线程1,这时就会越界.
for _ in 0..<Int.max {
let newScanner = BinaryScanner(data: Data("hi".utf8))
DispatchQueue.global().async {
scanRemainingBytes(scanner: newScanner)
}
scanRemainingBytes(scanner: newScanner)
}
这里套在一个0..<Int.max迭代里是因为遇到越界的概率不大.
结构体
首先简单的回忆一下,一个值类型的变量赋值给另一个变量时,值会被复制
var a = 42
var b = a
b += 1
b // 43
a // 42
在swift中,结构体就是这么工作的.
struct Point {
var x: Int
var y: Int
}
let origin = Point(x: 0, y: 0)
//origin.x = 10 // 错误
虽然x和y是var定义的,但是结构体origin是let,因此依然不能改变它的属性,好处也很明显,看到"let x = 结构体构造"的时候就知道x是个不能被修改的值类型.
同变量a和b的例子一样,换成结构体也成立
var thirdPoint = origin
thirdPoint.x += 10
thirdPoint // (x: 10, y: 0)
origin // (x: 0, y: 0)
**构造方法 **
上面定义的Point,swift自动生成了基于成员的构造方法,但是如果自定义构造方法,就没有swift提供的构造方法了.
struct Size {
var width: Int
var height: Int
init(w:Int, h:Int){
width = w
height = h
}
}
var p1 = Point.init(w: 10, h: 10)
这个时候就只有init(x:Int, y:Int)这一个构造方法了,不过可以通过扩展Size来保留原来的构造方法
extension Point{
init(w:Int, h:Int){
width = w
height = h
}
}
这样两种构造方法就都有了
可变语义
一开始的时候说到,改变结构体的成员,实际上是生成了一个新的结构体,那么如果给类的一个结构体成员变量设置didSet会发生什么.
首先构造一个嵌套的结构体,加一个构造方法
struct Rectangle {
var origin: Point
var size: Size
}
extension Rectangle {
init(x: Int = 0, y: Int = 0, width: Int, height: Int) {
origin = Point(x: x, y: y)
size = Size(width: width, height: height)
}
}
然后声明一个全局变量,设置didSet,修改两次成员的值
var screen = Rectangle.init(width: 100, height: 100){
didSet {
print("Screen changed: \(screen)")
}
}
screen.origin.x += 10
screen.size.height += 10
//Screen changed: Rectangle(origin: TestS.Point(x: 10, y: 0), size: TestS.Size(width: 100, height: 100))
//Screen changed: Rectangle(origin: TestS.Point(x: 10, y: 0), size: TestS.Size(width: 100, height: 110))
运行可以看到打印了两次,也就是说即便修改的是结构体深处的成员,didSet也会执行,那么如果Point的成员也设置didSet呢.
给Size的width属性添加didSet然后修改screen.size.width,运行
struct Size {
var width: Int{
didSet{
print("width changed: \(width)")
}
}
var height: Int
}
screen.size.width += 10
//width changed: 110
//Screen changed: Rectangle(origin: TestS.Point(x: 0, y: 0), size: TestS.Size(width: 110, height: 100))
可以看到两层didSet都执行了;
对结构体进行改变,在语义上来说,与重新为它进行赋值是相同的。即使在一个更大的结构体上只有某一个属性被改变了,也等同于整个结构体被用一个新的值进行了替代。在一个嵌套的结构体的最深层的某个改变,将会一路向上反映到最外层的实例上,并且一路上触发所有它遇到的 willSet 和 didSet.
这种影响还会继续扩展,比如标准库的内建集合类型是结构体,比如数组,向数组添加元素,修改某个元素都会触发数组的didSet,因此,如果数组中放的是值类型,这个值类型内部发生变化最终也会触发数组的didSet
var screens: [Rectangle] = [] {
didSet { print("Screens array changed: \(screens)")
}
}
screens.append(Rectangle(width: 320, height: 480)) // Screens array changed: [(0, 0, 320, 480)]
screens[0].origin.x += 100 // Screens array changed: [(100, 0, 320, 480)]
如果Rectangle是类就不会触发didset,因为数组存储的引用没有发生变化.
可变方法
假设我们要为 Rectangle 添加一个 translate 方法,用来将矩形以给定的偏移量进行位移。我们需要在矩形当前的原点值上增加这个偏移量,所以我们首先需要添加一个 + 操作符的重载,用来将两个 Point 相加到一起,并返回一个新的 Point.
然后添加translate方法.
func +(lhs: Point, rhs: Point) -> Point {
return Point(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
extension Rectangle {
func translate(by offset: Point) {
// 错误:不能赋值属性: 'self' 是不可变的
origin = origin + offset
}
}
但是translate这里标红了,并且提示给func添加mutating关键字.
extension Rectangle {
mutating func translate(by offset: Point) {
origin = origin + offset
}
}
screen.translate(by: Point(x: 10, y: 10)) screen // (10, 10, 320, 480)
函数并不知道实例将会被声明为可变还是不可变 ,加上mutating相当于告诉函数,这个结构体将创建出可变的实例,并且这个函数要描述实例被修改的行为.对此编译器会进行相关的处理.
let otherScreen = screen
// 错误:不能对不可变的量使⽤可变成员
otherScreen.translate(by: Point(x: 10, y: 10))
因此mutating标记的方法只有可变实例才能调用 .
回想一下内建集合一章,我们现在可以理解将 let 和 var 应用到集合上的区别了。数组的append 方法被定义为mutating,所以当数组被定义为 let 时,编译器不让我们调用这个方法。属性的 setter 自身就是 mutating 的,你无法调用一个 let 变量的 setter:
let point = Point.zero
// 错误:⽆法赋值属性:'point' 是⼀个 'let' 常量
point.x = 10
mutating 同时也是 willSet 和 didSet “知道” 合适进行调用的依据:任何 mutating 方法的调用或者隐式的可变 setter 都会触发这两个事件。
在很多情况下,一个方法会同时有可变和不可变版本。比如数组有 sort() 方法 (这是个mutating 方法,将在原地排序) 以及 sorted() 方法 (返回一个新的数组)。我们也可以为我们的translate(by:_) 提供一个非 mutating 的版本。这次我们不再改变 self,而是创建一个复制,改变它,然后返回这个新的 Rectangle:
extension Rectangle {
func translated(by offset: Point) -> Rectangle {
var copy = self
copy.translate(by: offset)
return copy
}
}
screen.translated(by: Point(x: 20, y: 20)) // (30, 30, 320, 480)
mutating是如何工作的
首先引入一个例子,定义一个全局函数,来将一个矩形在两个轴方向上各移动 10 个点,传进来的是值语义,只能使用translated(by:)
func translatedByTenTen(rectangle: Rectangle) -> Rectangle {
return rectangle.translated(by: Point(x: 10, y: 10))
}
screen = translatedByTenTen(rectangle: screen)
如过rectangle是var,那么就可以使用translate()了,而mutating就是在做这件事,可以把 self 想像为一个传递给Rectangle 所有方法的额外的隐式参数。你不需要自己去传递这个参数,但是在函数体内部你可以随时使用self,而mutating 关键字可以将隐式的 self 参数变为可变的.
将一个或多个参数标记为 inout 来达到相同的效果。就和一个普通的参数一样,值被复制并作为参数被传到函内。不过,我们可以改变这个复制 (就好像它是被var 定义的一样)。然后当函数返回时,Swift 会将这个 (可能改变过的) 值进行复制并将其返回给调用者,同时将原来的值覆盖掉.
mutating标记func,实际上是用inout标记了隐式的self参数.
func translateByTwentyTwenty(rectangle: inout Rectangle) {
rectangle.translate(by: Point(x: 20, y: 20))
}
translateByTwentyTwenty(rectangle: &screen)
那些像是 += 这样,可以对左侧值进行变更的运算符,需要其参数为 inout。下面是 Point 的+= 实现。如果我们将 inout 去掉的话,编译器就不会允许我们对 lhs 进行赋值了
func +=(lhs: inout Point, rhs: Point) {
lhs = lhs + rhs
}
var myPoint = Point.zero
myPoint += Point(x: 10, y: 10)
myPoint // (x: 10, y: 10)
现在再来看最开始的问题代码
for _ in 0..<Int.max {
let newScanner = BinaryScanner(data: Data("hi".utf8))
DispatchQueue.global().async {
scanRemainingBytes(scanner: newScanner)
}
scanRemainingBytes(scanner: newScanner)
}
如果 BinaryScanner 是一个结构体,而非类的话,每次 scanRemainingBytes 的调用都将获取它自己的 newScanner 的独立的复制。这样一来,这些调用将能够在数组上保持安全的迭代,而不必担心结构体被另一个方法或者线程所改变。因为两个线程现在并没有共享一个单一的position 值,每个循环中将会把每个字节打印两次。
网友评论