【Swift进阶笔记】类和结构体

WechatIMG196.jpeg

结构体是值类型,类是引用类型

值类型和引用类型

一个值类型变量是一个指向内存中某个位置的名字,而这个位置保存了一个值

截屏2022-01-04 下午2.24.43.png

一个引用类型变量含有一个指针,指向内存中某个地方真正的实例,本质是不含有”事物“本身,只是对其的一个引用。

例子对比:

class ScoreClass {
    var home: Int
    var guest: Int
    
    init(home: Int, guest: Int) {
        self.home = home
        self.guest = guest
    }
}

struct ScoreStruct {
    var home: Int
    var guest: Int
    // 编译器会生成成员初始化方法 (Memberwise initializer)
}
var score1 = ScoreClass(home: 0, guest: 0)
var score2 = score1
score2.guest += 1
debugPrint(score1.guest) // 1
var score1 = ScoreStruct(home: 0, guest: 0)
var score2 = score1
score2.guest += 1
debugPrint(score1.guest) // 0

可变性

var scoreClass = ScoreClass(home: 0, guest: 0)
var scoreStruct = ScoreStruct(home: 0, guest: 0)

scoreClass.home += 1
scoreStruct.guest += 1

这里都使用var声明属性,但这里,对类和结构体的修改有一个重要的区别;对于结构体修改的永远只是局部变量,也就是我只修改了局部scoreStruct的局,对于类实例的修改可能会产生全局性的影响;这个修改会影响其它那些,持有指向同一个市里的 引用的对象

如果把俩个实例用保存到let定义的变量中,类实例还可以修改,但是结构体实例却不行了

let scoreClass = ScoreClass(home: 0, guest: 0)
let scoreStruct = ScoreStruct(home: 0, guest: 0)

scoreClass.home += 1 // 可以工作
scoreStruct.guest += 1 

使用let声明一个变量,意味着初始化之后,就不能改变它的值了。由于scoreClass的变量的值是一个指向ScoreClass实例的引用,所以这意味着我们只是不能再给scoreClass赋值另外一个引用而已。但修改我们所创建的 ScoreClass实例的属性时,并不需要改变scoreClass变量的值。我们只需要通用引用得到实例,然后再实例上修改用var声明的属性。

由于结构体值类型,所以scoreStruct变量不包含一个对其它地方的实例的引用,而是ScoreStruct实例本身,由于用let声明的变量的值,在初始化之后就不能再修改,修改结构体的属性,从语言上等同于对变量赋值一个新的结构体属性

scoreStruct.guest += 1
等同于
scoreStruct = ScoreStruct(home: 0, guest: 1)
  • 类型为类的变量的值,是一个指向实例的引用;而类型为结构体的变量的值,是结构体实例本身
  • 修改一个结构体的属性,即使修改的是多层的嵌套属性,都等同于给变量赋值一个全新的结构体实例
可变方法
extension ScoreStruct {
    mutating func scoreGuest() {
        self.guest += 1
    }
}

在结构体上用func关键字定义普通的方法,默认会隐式传入self参数,默认是不可变的,我们必须使用mutating func关键字创建一个可变方法

inout参数
extension ScoreStruct {
    func scoreGuest(score: inout ScoreStruct) {
        score.guest += 1
    }
}

score是不可变的,可以用var赋值一个局部变量,但是这样并不会对原来的值产生影响,如果需要对原来的值产生影响可以用inout关键字

使用inout关键字需要做俩件事情

  • 作为 inout 参数传递的变量必须是用 var 定义的
  • 当把这个变量传递给函数时,必须在变量名前加上 & 符号。站在调用者的角度,& 符号可以清楚的表明,这个函数会修改传入的变量的值。 这里跟OC的取值操作符意义不同

生命周期

Swift通过ARC来追踪一个实例的引用计数,当引用计数将至0(例如所包含的变量都离开了作用域,或被设置成了nil),Swift将会调用deinit方法并释放内存

循环引用
class MyWindow {
    var rootView: RootView?
}

class RootView {
    var window: MyWindow
    
    init(window: MyWindow) {
        self.window = window
    }
}
var window: MyWindow? = MyWindow() // window引用计数1
window = nil   // 引用计数0
var window: MyWindow? = MyWindow()  // widow 1
var rootView: RootView? =  RootView(window: window) // window 2 view 1
window?.rootView = rootView     // window 2 view 2

rootView = nil // window 2 view 1
window = nil // window 1 view 1

这里已经无法通过变量访问到实例,但是他们还是互相强引用着对方,叫做循环引用,需要特别留意循环引用造成内存泄漏的可能性,因为在程序的生命周期中,这俩个对象永远不会被销毁。

弱引用

为了打破循环引用,我们需要将一个引用变为弱引用或用unowned引用。把一个对象赋值给一个弱引用变量,并不会改变实例的引用计数。在Swift里,弱引用变量是归零(zeroing)的, 一旦所指向的对象被销毁,变量自动被设置成nil,这也是弱引用对象必须是可选值的原因

为了修复上面的在window类的rootView属性改为弱引用,意味着该属性不会强引用view,并且view一旦被销毁,会被强制销毁为nil

class MyWindow {
    weak var rootView: RootView?

    deinit {
        debugPrint("window deinit")
    }
}

class RootView {
    var window: MyWindow?
    
    init(window: MyWindow?) {
        self.window = window
    }
    
    deinit {
        debugPrint("view deinit")
    }
}

当使用代理(delegate)的时候,弱引用非常有用.代理对象(例如一个tableView)需要一个指向它的代理的引用,但它不应该拥有该代理,否则会产生一个循环引用。因此指向代理的通常是弱引用,而另一个对象(例如view controller)的职责是确保对象在需要的时候存在

unowned引用

有时候我们希望引用既是一个弱引用,但同时又不是一个可选值。例如view永远一个window(所以此属性不应该是可选值),但又不希望强引用着window,这时候可以使用unowned

class MyWindow {
    var rootView: RootView?

    deinit {
        debugPrint("window deinit")
    }
}

class RootView {
    unowned var window: MyWindow
    
    init(window: MyWindow) {
        self.window = window
    }
    
    deinit {
        debugPrint("view deinit")
    }
}

对于unowned的使用,我们是确保"被引用者"生命周期比'"引用者"长',我们必须确保window的生命周期比view长,如果window在view之前被销毁,再次访问unowned对象,会崩溃。

要注意的是,这里的崩溃和未定义的行为不一致。在对象中,Swift运行运行另一个引用计数来跟踪unowned引用。当对象没有任何强引用的时候,对象会释放所有资源(例如,对其它对象的引用)。然而,只要对象还存在unowned引用存在,其自身存在的内存就不会被回收,这块内存会标记为无效,也称作僵尸内存(zoombie memory),当我们访问unowned标记的僵尸内存时,就会发生运行时错误。

我们也可以通过unowned(unsafe)来绕过这个保护机制,当访问一个unowned(unsafe)标记的引用时,行为是未定义的。

闭包和循环引用

在Swift中,类不是唯一的引用类型,函数(也包括闭包)也是。如果一个闭包捕获了一个引用类型的变量,那么闭包就会对该变量强引用

class MyWindow {
    weak var rootView: RootView?
    var onRorate: (() -> ())? = nil
}

class RootView {
    var window: MyWindow
    
    init(window: MyWindow) {
        self.window = window
    }
}

var window: MyWindow? = MyWindow()
    var rootView: RootView? =  RootView(window: window!)
    window?.onRorate = {
        debugPrint("\(rootView)")
    }
  }

截屏2022-01-07 下午1.45.33.png

  • 把view对window的引用改为弱引用,因为没有其它引用存在,window会在创建之后被释放

  • 把onRatate标记为weak,但是Swift不允许函数类型标记为weak

  • 使用捕获列表(capture list), 并在捕获列表中弱引用view,这样确保闭包不会被强引用view.

window?.onRorate = { [weak view] in
      debugPrint("\(rootView)")
}
捕获列表可以做的不仅仅只是将变量标记为 weak 或 unowned。比如,如果我们想有一个指向 window 的弱引用变量的话,可以直接在捕获列表中初始化这样一个变量;或者甚至可以在其中定义完全不相关的变量,像以下这样:

window?.onRotate = { [weak view, weak myWindow=window, x=5*5] in
      print("We now also need to update the view: \(view)")
      print("Because the window \(myWindow) changed")
}
在unowned和弱引用之间做选择
  • 去选择对象的生命周期,如果你不能保证一个生命周期比另一个长,使用弱引用唯一安全的选择
  • 如果保证非强引用的对象与持有该引用对象的生命周期一致,甚至更长,unowned通常更方便.因为它的类型不需要可选值,并且声明为let,而弱引用必须是var声明的可选值
  • 生命周期相同的情况是很常见的,特别当两个对象之间是父子关系时。当父对象使用强引用来控制其子对象的生命周期,并且我们可以保证没有其他对象知道子对象存在的话,子对象对父对象的引用就可以是 unowned。
  • 相比弱引用,unowned 引用的开销也小一点,通过它访问属性或调用方法的速度会快一点点。不过,应该只在对效率非常敏感的代码路径中,才把这个优点作为考虑的因素之一
  • 使用 unowned 引用的缺点也很明显,如果你错误地判断了对象的生命周期,你的程序可能会崩溃。就个人而言,即使可以用 unowned 引用,我们也经常发现自己更喜欢用弱引用,因为在每个使用弱引用的地方,它都强迫我们必须检查引用是否仍然有效。特别是当重构时,unowned 引用很容易打破之前对于对象生命周期的设想,并引入会使程序崩溃的 bug。

在结构体和类之中做选择

  • 要共享一个实例所有权,我们必须使用类。否则可以使用结构体

  • 结构体不如类强大,但是提供了简洁性:没有引用,没有生命周期,没有子类

  • 结构体提供了更好的性能

    例如,如果 Int 是一个类的话,元素类型为 Int 的数组会占用更多的内存,因为除了实例本身需要的内存之外,数组中要保存指向实际实例的引用 (指针),以及每个实例需要的额外开销 (例如,保存它的引用计数)。更重要的是,迭代这样一个数据会慢得多,因为对于每个元素,访问它的代码都必须经由额外的间接层,因此可能无法有效地利用 CPU 缓存。特别是如果数组中的 Int 实例被分配在内存中的位置都相距很远的话,情况就更糟糕

具有值语义的类

实现不可变类,让其行为更像一个值类型

首先所有属性都声明为let,其次避免子类重新引入改变其行为,用final标记禁止子类化

final class ScoreClass {
    let home: Int
    let guest: Int
    
    init(home: Int, guest: Int) {
        self.home = home
        self.guest = guest
    }
}

let score1 = ScoreClass(home:0, guest:0)
let score2 = score1

Foundation中的NSArray没有暴露任何可变Api,基本可以当做值类型来使用,但是因为NSArray中的子类有个可变的NSMutableArray类型,“所以除非一个 NSArray 的实例是你亲手创建的,否则我们就无法假定确实是在处理这样一个类型的实例。这就是为什么在上面,要把我们的类声明为 final,并且这也是为什么从一个你无法控制的 API 得到一个 NSArray 后,在做下一步之前,我们建议你先对得到的结果做一个 copy 操作

具有引用语义的结构体


struct ScoreStruct2 {
    var home: Int
    var guest: Int
    var formatter: NumberFormatter
    
    init(home: Int, guest: Int) {
        self.home = home
        self.guest = guest
        formatter = NumberFormatter()
        formatter.minimumIntegerDigits = 2
    }
    
    var pretty: String {
        let h = formatter.string(from: home as NSNumber)
        let g = formatter.string(from: guest as NSNumber)
        return "\(h)-\(g)"
    }
}

func test3() {
    let score1 = ScoreStruct2(home: 2, guest: 1)
    print(score1.pretty)
    let score2 = score1
    score2.formatter.minimumIntegerDigits = 3
    print(score1.pretty)
}

Optional("02")-Optional("01")

Optional("002")-Optional("001")

会发生这种情况的原因是,NumberFormatter 是一个类,也就是说,在结构体中的 scoreFormatter 属性,包含的是指向一个 NumberFormatter 实例的引用。当我们把 score1 赋值给 score2 变量时,产生了一份 score1 的拷贝。虽然一个结构体会拷贝它所有属性值,但因为 scoreFormatter 的值只是一个引用,所以 score2 和 score1 中持有的引用,背后都指向同一个 NumberFormatter 实例

写时复制优化

对于值类型,因为赋值或作为函数的参数传递,都会产生一份拷贝,所以会有大量的赋值操作。虽然编译器试图更智能地去对待是否要复制这件事,让它可以在能证明即使不复制也是安全的时候来避免产生复制操作,但对于一个值类型的实现者来说,有另外一种优化的方式来解决这个问题,那就是使用一种名为写时复制的技术来实现该类型。这对于那些持有大量数据的类型尤其重要,像是标准库中的集合类型 (Array,Dictionary,Set 和 String),它们都在实现中使用了写时复制

写时复制的意思是,在结构体中的数据,一开始是在多个变量之间共享的:只有在其中一个变量修改了它的数据时,才会产生对数据的复制操作。因为数组是用写时复制实现的,所以如果我们创建一个数组,并把它赋值给另外一个变量的话,数组的数据实际上并不会被复制:

var x = [1, 2, 3]
var y = x

从内部来看,x 和 y 持有的数组都包含一个指向同一块内存缓冲区的引用。此缓冲区是保存数组中实际元素的地方。当我们修改 x (或者 y) 时,因为数组监测到有多个变量共享一块缓冲区,所以在做修改之前,会先产生一份这个缓冲区的拷贝。这意味着我们可以独立地修改这两个变量,并且那些昂贵的复制元素的操作,都只会发生在它必须发生的时候:

x.append(5)
y.removeLast()
x // [1, 2, 3, 5]
y // [1, 2]
写时复制的权衡
  • 值类型的一个有点就是不会产生引用计数方面的开销

  • 因为实现写实复制的结构体,依赖于保存内部的一个引用,所以这个结构体没产生一份拷贝都会增加这个内部引用的引用计数。实际上我们放弃了值语义不需要引用计数的特点来减轻值类型复制语义所带来的成本

  • 增加或减少一个引用计数都是一相对较慢操作(这里慢是相对于字节复制到栈上另一个位置的操作,因为这样的操作肯定是线程安全的,需要锁的开销)

  • 标准库中所有可变长度类型(数组,字典,集合,字符串)都采用写时复制,所以每个带有这些属性的结构体都会带来引用计数的开销,当含有多种这种类型的属性的时候,这种开销会发生很多次

  • SwiftNIO中一个 HTTP 请求就是用结构体来实现的,它包含了多个属性,像是 HTTP 方法和头。当这样一个结构体被复制时,不仅要复制它所有的字段,而且所有内部的数组,字典和字符串的引用计数也都会增加。当传递这种类型的值时 (这种操作很常见),这种开销会导致性能的显著下降,而用类实现它的话,性能会好很多,因为相比在传递时要复制所有字段,用类的话,只需要复制引用本身就可以了,并且也只需要更新这一个引用的引用计数。

实现写时复制

一个简单的结构退

struct HTTPRequest {
    var path: String
    var headers: [String: String]
}

最小化引用计数的开销,把所有属性封装到一个 私有的Storage类中

struct HTTPRequest {
    fileprivate class Storage {
        var path: String
        var headers: [String: String]
        
        init(path: String, headers: [String: String]) {
            self.path = path
            self.headers = headers
        }
    }
    
    private var storage: Storage
    
    init(path: String, headers: [String: String]) {
        storage = Storage(path: path, headers: headers)
    }
}

这样做HTTPRequest只会包含storage的一个属性,并在复制时只增加Storage实例的一个引用计数,

然后把属性暴露给外部,增加计算属性

extension HTTPRequest {
  var path: String {
    get { return storage.path }
    set { /* to do */ }
   }
  var headers: [String: String] {
    get { return storage.headers }
    set { /* to do */ }
  }
}

这个实现中,setter是最重要的部分:因为存储在内部的Storage实例可能被多格变量所共享,所以在这些setter中,我们并不能简单的在Storage实例上设置新值。

由于把请求相关数据都保存在一个类的实例中,这样实现的细节不应该暴露出去,所以我们必须保证这个基于类的结构体行为和纯结构体版本是一致的,这意味着,修改HTTPRequest类型变量的一个属性时,受到改变影响的应该只是这个变量而已。

首先,每次setter被调用时候,就会生成一份内部Storage类实例的拷贝。为了生成拷贝,在Storage上添加一个copy方法

extension HTTPRequest.Storage {
    func copy() -> HTTPRequest.Storage {
        print("Making a copy")
        return HTTPRequest.Storage(path: path, headers: headers)
    }
}

在设置新值,产生一份拷贝并赋值给storage属性

extension HTTPRequest {
    var path: String {
        get { return storage.path }
        set {
            storage = storage.copy()
            storage.path = newValue
        }
    }
    var headers: [String: String] {
        get { return storage.headers }
        set {
            storage = storage.copy()
            storage.headers = newValue
        }
    }
}

虽然HTTPRequest结构体背后是由一个类所支持的,但是它完全表现出了值语义,他的所有属性就好像定义在接头体本身一样

let req1 = HTTPRequest(path: "/home", headers: [:])
var req2 = req1
req2.path = "/users"
assert(req1.path == "/home") 

当前还不够高效,无论是否有其它变量引用,只要我们修改属性,就会创建一份内部storage的实例的拷贝

var req = HTTPRequest(path: "/home", headers: [:])
for x in 0..<5 {
req.headers["X-RequestId"] = "\(x)"
}
/*
Making a copy...
Making a copy...
Making a copy...
Making a copy...

所有这些拷贝都是不需要的,因为只有req这个变量持有指向Storage实例的引用。

为了实现一个高效的写时复制,我们需要知道,一个对象 (在我们的例子中是 Storage 实例) 是否被唯一引用,也就是说,它是否只有一个所有者。如果是的话,我们可以直接修改对象,否则,我们则在修改之前,创建一份对象的拷贝。

我们可以使用 **isKnownUniquelyReferenced **函数来检查一个引用类型的实例是否只有一个所有者。 如果你把一个 Swift 的类实例传递给此函数,并且这个实例没有其他强引用的话,这个函数就返回 true,反之,如果有其他强引用存在,此函数返回 false。

使用 isKnownUniquelyReferenced 时,请务必牢记以下这些细微的地方:

虽然这个函数是线程安全的,但是,你必须保证传入的变量不会被另外一个线程所访问,这个限制不单单只是针对 isKnownUniquelyReferenced,它适用于所有的 inout 参数。换言之,isKnownUniquelyReferenced 不能防止竞争条件 (race condition)。以下的代码就是不安全的,因为两个队列会同时修改同一个变量:

var numbers = [1, 2, 3]
queue1.async { numbers.append(4) }
queue2.async { numbers.append(5) }
isKnownUniquelyReferenced 的参数是一个 inout 参数,因为在 Swift 中,这是在函数参数的上下文中引用一个变量的唯一方式。如果是普通 (非 inout) 参数的话,当调用函数时,编译器就会产生一份参数的拷贝,也就是说,isKnownUniquelyReferenced 这个函数肯定会返回 false,因为在函数中,被测试对象的引用,不可能是唯一的。

unowned 引用和弱引用并不被计算在内,即我们不能把此类变量作为参数传入这个函数[…]”

摘录来自: Chris Eidhof.

extension HTTPRequest {
    private var storageForWriting: HTTPRequest.Storage {
        mutating get {
            if !isKnownUniquelyReferenced(&storage) {
                self.storage = storage.copy()
            }
            return storage
        }
    }
    
    var path: String {
        get { return storage.path }
        set {
            storageForWriting.path = newValue
        }
    }
    var headers: [String: String] {
        get { return storage.headers }
        set {
            storageForWriting.headers = newValue
        }
    }
}
var req = HTTPRequest(path: "/home", headers: [:])
var copy = req
for x in 0..<5 {
req.headers["X-RequestId"] = "\(x)"
} // Making a copy...

你可能感兴趣的:(【Swift进阶笔记】类和结构体)