昨晚跟 张瑞鹏 谈起了Rust语言,其实一直对Rust语言充满了敬畏。Google了一下Rust vs Swift似乎也没什么靠谱的回答,可能二者目前区别不是很大。不过 Quora 上的一个回答引发了我的兴趣。
这里撇开Swift中的即时编译、交互式编程,只关注内存管理这方面。( Chris Lattner 在他的博客中也提到这两者特性只是他个人的一时兴起。)
Troy Harvey 说Rust的所有权系统(Ownership system)可以100%的防止内存泄露的发生,而Swift中使用的自动引用计数 98%可以杜绝内存泄露的发生(由于存在循环强引用)。
这里就来探索Swift中的ARC与此前OC中的ARC发生了哪些改变?这里把重点放在如何在Swift中解决循环强引用。
在之前的三篇文章中,可以得知,在OC中 weak
可以用来破除两个强引用的循环引用。在Swift中,则又多了一个无主引用(unowned reference)。
弱引用(weak reference)和无主引用(unowned reference)都允许循环引用中的某一个实例引用另外一个实例而不保持强引用,这样实例就能够互相引用而不会产生循环强引用的副作用。
那么弱引用(weak reference)和无主引用(unowned reference)在Swift中又有什么区别呢?一般来说,对于在生命周期中会变为 nil
的实例使用弱引用(weak reference)。对于在初始化赋值后再也不会变为 nil
的实例,使用无主引用(unowned reference)。
无主引用永远都是有值的,这与弱引用不同。因此,无主引用总是被定义为非可选类型。可以在声明前加入 unowned
关键字以表示一个无主引用。
由于无主引用是非可选类型,所以不需要在使用时将其展开。无主引用总是可以被直接访问。不过ARC无法在实例被销毁后将无主引用设为 nil
,因为非可选类型不允许被赋值为 nil
。
文档中的一个例子如下:
如下定义了 Customer
和 CreditCard
两个类,用于表示银行的客户和客户的信用卡。这两个类中,每一个都将另一个类的实例作为自身的属性。在这个数据模型中,有一点与其他循环强引用的例子不同:一个客户可能有或者没有信用卡,但是一张信用卡总是对应着一个确定的客户。为了表示这种关系, Customer
类有一个可选类型的 card
属性,但是 CreditCard
类有一个非可选类型的 customer
属性。
由于信用卡总是对应着一个客户,因此将 customer
属性定义为无主引用,
classCustomer{ letname:String varcard:CreditCard? init(name:String) { self.name = name } deinit{ print("/(name)is being deinitialized.") } } classCreditCard{ letnumber:UInt64 unownedletcustomer:Customer init(number:UInt64, customer:Customer) { self.number = number self.customer = customer } deinit{ print("Card #/(number)is being deinitialized.") } }
在这个模型中, Customer
实例持有对 CreditCard
实例的强引用, 而 CreditCard
实例持有对 Customer
实例的无主引用。
还有可能存在另一种场景:在这种场景中,两个属性都必须有值,并且初始化完成后永远不会为 nil
。在这种场景中,需要一个类使用无主属性,而另外一个类使用隐式解析可选属性。
如下定义了 Country
和 City
两个类,在这个模型中,每个城市必须属于一个国家,每个国家必须有一个首都城市。
classCountry{ letname:String varcapitalCity:City! init(name:String, capitalName:String){ self.name = name self.capitalCity =City(name: capitalName, country:self) } } classCity{ letname:String unownedletcountry:Country init(name:String, country:Country) { self.name = name self.country = country } }
Country
的构造函数调用了 City
的构造函数。然而只有 Country
的实例完全初始化完后, Country
的构造函
数才能把 self
传给 City
的构造函数。为了满足这种需求,通过在类型结尾处加上感叹号( City!
)的方式,将 Country
的 capitalCity
属性声明为隐式解析可选类型的属性。这表示像其他可选类型一样, capitalCity
属性的默认值为 nil
,但是不需要展开它的值就能访问它。
由于 capitalCity
默认值为 nil
,一旦 Country
的实例在构造函数中给 name
属性赋值后,整个初始化过程就完成了。这代表一旦 name
属性被赋值后, Country
的构造函数就能引用并传递隐式的 self
。 Country
的构造函数在赋值 capitalCity
时,就能将 self
作为参数传递给 City
的构造函数。
以上的意义在于你可以通过一条语句同时创建 Country
和 City
的实例,而不产生循环强引用,并且 capitalCity
的属性能被直接访问,而不需要通过感叹号来展开它的可选值。
当一个闭包被赋值给类实例的某个属性,并且这个闭包体中又使用了这个类实例时,也会出现循环引用。
如下的例子用来展示一个由闭包引起的循环引用的发生:
classHTMLElement{ letname:String lettext:String? lazy varasHTML:Void->String= { iflettext =self.text { return"</(self.name)>/(text)<//(self.name)>" } else{ return"</(self.name)/>" } } init(name:String, text:String? =nil) { self.name = name self.text = text } deinit{ print("/(name)is being deinitialized.") } }
这个例子中闭包在其闭包体内使用了 self
(引用了 self.name
和 self.tex t
),因此闭包捕获了 self
,这意味着闭包又反过来持有了 HTMLElement
实例的强引用。这样两个对象就产生了循环强引用。
Note: 虽然闭包多次使用了 self
,但它只捕获 HTMLElement
实例的一个强引用。
Swift 中对此提供了一中优雅的解决方案叫做闭包捕获列表( closuer capture list
)。
定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。
捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,可以声明每个捕获的引用为弱引用或无主引用。
Note: Swift 有如下要求:只要是在闭包内使用的成员,就要用 self.someProperty
或者 self.someMethod()
(而不只是 someProperty
或 someMethod
)。这提醒你可能会一不小心就捕获了 self
。
于是,我们可以改造此前的 asHTML
闭包。
classHTMLElement{ letname:String lettext:String? lazy varasHTML:Void->String= { [unownedself]in iflettext =self.text { return"</(self.name)>/(text)<//(self.name)>" } else{ return"</(self.name)/>" } } init(name:String, text:String? =nil) { self.name = name self.text = text } deinit{ print("/(name)is being deinitialized.") } }
捕获列表常见的语法如下:
lazy varsomeClosure:(Int, String)->String = { [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String)->Stringin //closure body goes here }
如果闭包没有指明参数列表或者返回类型,则会通过上下文推断,那么可以把捕获列表和关键字 in
放在闭包最开始的地方:
lazy varsomeClosure:Void->String= { [unownedself,weakdelegate =self.delegate!]in // closure body goes here }