结构体和类模块分两篇笔记来学习:
- 第一篇:
- 结构体和类的区别
- 分析类和结构体可变性
- 以一个具体的例子来学习使用类和结构体的区别,以及如何使用写时复制来解决结构体内部引用类型的复制
- 最后学习函数闭包的可变性
- 第二篇:
- 类和结构体的内存分析
- 何时使用类何时使用结构体
本篇开始学习第二篇类和结构体的内存分析以及何时使用类何时使用结构体,go!
类和结构体的内存分析
-
结构体的内存分析
swift中大多类型都是值类型,要么是结构体,要么是枚举。他们的内存管理比较简单,因为它们只有一个持有者,它们的内存都是自动的创建和回收,不需要考虑引用计数,也不需要考虑循环引用的问题。举个例子如下:
struct Person {
let name: String
var parents: [Person]
}
var john = Person(name: "John", parents: [])
john.parents = [john]
print(john)”
示例中当把john加入到数组时,其实他被复制了,并不会引起循环引用的问题,而如果Person声明为类,则会引起循环应用。
_ Swift 的结构体一般被存储在栈上,而非堆上。不过这也有例外,如果结构体的尺寸是动态的,或者结构体太大的话,它还是会被存储到堆上。另外,如果结构体值被函数所持有 (就像用闭包的例子中那样),那么为了持久化,即使程序已经离开了定义该值的作用域,这个值也会被存储在堆上。_
-
类的内存使用及循环引用的问题
对于类,swift中使用引用计数来进行内存管理,每次创建一个对象的引用,引用计数就会增1,当引用无效时,就会减1,当引用计数为0时,对象就会被系统自动回收。因为有引用计数,所以大多数情况下,我们不需要考虑内存问题,但是也有意外,如下示例:
//声明一个View类,包含Window属性
class View {
var window: Window
init(window: Window) {
self.window = window
}
}
//声明一个Window类,包含View属性
class Window {
var rootView: View?
}
//window引用计数+1
var window: Window? = Window()
//window引用计数+1,view引用计数+1
var view: View? = View(window: window!)
//view引用计数+1
window?.rootView = view
//view引用计数-1
view = nil
//window引用计数-1
window = nil
代码执行完之后,view和window的引用计数都为1。此时两者都互相有着对彼此的强引用,也就产生了引用循环,造成内存泄漏。
-
weak 引用解决引用循环
weak 引用表示当被引用的对象被释放时,将引用设置为 nil。比如,我们可以将 rootView 声明为 weak,这样 window 就不会强引用 view,当 view 被释放时,这个属性也将自动地变为 nil。进而循环引用的问题就解决了。
class Window { weak var rootView: View? }
-
unowned 引用解决引用循环
因为 weak 引用的变量可以变为 nil,所以它们必须是可选值类型,但是有些时候这并不是你想要的。例如,也许我们知道我们的 view 将一定有一个 window,这样这个属性就不应该是可选值,而同时我们又不想一个 view 强引用 window。这种情况下,我们可以使用 unowned 关键字,这将不持有引用的对象,但是却假定该引用会一直有效
class View { unowned var window: Window init(window: Window) { self.window = window } } class Window { var rootView: View? } var window: Window? = Window() var view: View? = View(window: window!) window?.rootView = view view = nil window = nil
此时不需要可选值,也不会产生循环引用,但是需要保证window的生命周期比view长,如果先销毁了window,然后继续访问view上的unowned类型的话,程序就会崩溃。
一个 weak 变量总是需要被定义为 var,而 unowned 变量可以使用 let 来定义。不过,只有在你确定你的引用将一直有效时,才应该使用 unowned。
如何选择使用类还是结构体?
使用类还是结构体,要根据具体的问题和数据种类来定,接下来根据一个具体的问题,分别使用类、结构体、优化的结构体三种实体类型分别解决一个银行账户转账的实际问题。
-
类
首先定义Account类:
typealias USDCents = Int
class Account {
var funds: USDCents = 0
init(funds: USDCents) {
self.funds = funds
}
然后声明俩账户对象:
let alice = Account(funds: 100)
let bob = Account(funds: 0)
接着创建一个transfer的转账函数,返回类型为Bool,表明是否转账成功。
func transfer(amount: USDCents, source: Account, destination: Account)-> Bool{
guard source.funds >= amount else { return false }
source.funds -= amount
destination.funds += amount
return true
}
需求看似已解决,但是这种解决方式不是线程安全的,并发线程有可能造成账户莫名的增加或者减少余额。
-
纯结构体
首先创建Account结构体,此时我们不需要声明构造器,因为编译器会帮我们按照成员自动生成构造器。
struct Account { var funds: USDCents }
但是 transfer 函数就要复杂一些,它依然接受一个数值和两个帐号。我们使用 var 来创建输入帐号参数的复制,这样它们就能够在函数内部进行更改了。不过,这样的变更不会改变传进来的原来的值。为了将更改过的帐号信息回传给调用者,我们将返回一个更新过的帐号的多元组,如果转账没有成功,我们返回 nil。
func transfer(amount: USDCents, source: Account, destination: Account)-> (source: Account, destination: Account)?{
guard source.funds >= amount else { return nil }
var newSource = source
var newDestination = destination
newSource.funds -= amount
newDestination.funds += amount
return (newSource, newDestination)
}
因为结构体是值,我们知道一个帐号不会被另外的线程更改。至少,这两个帐号的状态都是稳定的。
-
inout 结构体
使用结构体和带有 inout 参数的函数的情况。它使用和上面一样的结构体,但是转账的函数略有不同。我们不将帐号参数使用 var 的方式进行复制,而是将它们标记为 inout。这样一来,这些值在传入时将被复制,在函数的内部,它们不会被其他线程更改。在函数返回时,它们会被复制回原来的值
func transfer(amount: USDCents, inout source: Account, inout destination: Account)-> Bool{ guard source.funds >= amount else { return false } source.funds -= amount destination.funds += amount return true }