Swift: 初始化抛出时如何释放属性?
Swift: How to deallocate properties when init throw?
此示例代码存在内存泄漏。
pointer1 和 pointer2 在 Person 成功初始化之前分配。如果 init
函数抛出错误。 deinit
函数永远不会被执行。所以 pointer1 和 pointer2 将永远不会被释放。
import XCTest
class Person {
// case1
let pointer1: UnsafeMutablePointer<Int> = UnsafeMutablePointer<Int>.allocate(capacity: 1)
// case2
let pointer2: UnsafeMutablePointer<Int>
let name: String
init(name: String) throws {
// case2
self.pointer2 = UnsafeMutablePointer<Int>.allocate(capacity: 1)
if name == "UnsupportName" {
throw NSError()
}
self.name = name
}
deinit {
pointer1.deallocate()
pointer2.deallocate()
}
}
class InterestTests: XCTestCase {
func testExample() {
while true {
_ = try? Person(name: "UnsupportName")
}
}
}
有时候逻辑很复杂。在我的真实案例中。有很多allocate
和throws
与if
和guard
。有些很难控制。
有什么方法可以避免这种内存泄漏吗?
这是一个类似的问题:https://forums.swift.org/t/deinit-and-failable-initializers/1199
在您的具体示例中,解决方案很简单。在解决所有可能的故障之前不要分配任何内存:
class Person {
let aPointer: UnsafeMutablePointer<Int> // Do not allocate here.
let name: String
init(name: String) throws {
// Validate everything here
guard name != "UnsupportName" else {
throw NSError()
}
// After this point, no more throwing:
self.name = name
// Move the allocation here
self.aPointer = UnsafeMutablePointer.allocate(capacity: 1)
}
deinit {
aPointer.deallocate()
}
}
但更通用的解决方案是像使用其他地方一样使用 do/catch 来管理错误:
class Person {
let aPointer = UnsafeMutablePointer<Int>.allocate(capacity: 1)
let name: String
init(name: String) throws {
do {
if name == "UnsupportName" {
throw NSError()
}
self.name = name
} catch let e {
self.aPointer.deallocate()
throw e
}
}
deinit {
aPointer.deallocate()
}
}
我很想将 .allocate
移到 init
中,只是为了让它更清楚地显示正在发生的事情。关键是你应该首先分配所有内存,然后再抛出任何东西(这样你就知道你可以全部释放),或者在最后一次抛出之后分配所有内存(这样你就知道你没有任何东西可以释放)。
查看您添加的解决方案,没问题,但建议围绕它的危险逻辑。最好将其展开以将分配放入他们自己的对象中(这几乎肯定也会摆脱 UnsafeMutablePointers;在一个 class 中需要很多这样的对象是非常可疑的)。
也就是说,IMO 有更简洁的方法来沿着这条路径构建错误处理。
extension UnsafeMutablePointer {
static func allocate(capacity: Int, withCleanup cleanup: inout [() -> Void]) -> UnsafeMutablePointer<Pointee> {
let result = allocate(capacity: capacity)
result.addTo(cleanup: &cleanup)
return result
}
func addTo(cleanup: inout [() -> Void]) {
cleanup.append { self.deallocate() }
}
}
这让 UnsafeMutablePointers 可以将清理信息附加到一个数组中,而不是创建大量 defer
块,这会增加在清理期间丢失一个块的风险。
这样,您的 init 看起来像:
init(name: String) throws {
var errorCleanup: [() -> Void] = []
defer { for cleanup in errorCleanup { cleanup() } }
// deallocate helper for case1
pointer1.addTo(cleanup: &errorCleanup)
// case2
self.pointer2 = UnsafeMutablePointer<Int>.allocate(capacity: 1, withCleanup: &errorCleanup)
// case ...
if name == "UnsupportName" {
throw NSError()
}
self.name = name
// In the end. set deallocate helpers to nil
errorCleanup.removeAll()
}
当然,这会带来调用 allocate(capacity:)
而不是 allocate(capacity:withCleanup:)
的危险。所以你可以通过将它包装成另一种类型来解决这个问题;自动释放自身的引用类型。
class SharedPointer<Pointee> {
let ptr: UnsafeMutablePointer<Pointee>
static func allocate(capacity: Int) -> SharedPointer {
return .init(pointer: UnsafeMutablePointer.allocate(capacity: capacity))
}
init(pointer: UnsafeMutablePointer<Pointee>) {
self.ptr = pointer
}
deinit {
ptr.deallocate()
}
}
有了这个,这就变成了(不需要 deinit):
class Person {
// case1
let pointer1 = SharedPointer<Int>.allocate(capacity: 1)
// case2
let pointer2: SharedPointer<Int>
let name: String
init(name: String) throws {
// case2
self.pointer2 = SharedPointer<Int>.allocate(capacity: 1)
if name == "UnsupportName" {
throw NSError()
}
self.name = name
}
}
您可能想要编写各种帮助程序来处理 .ptr
。
当然,这可能会导致您构建特定版本的 SharedPointer 来处理各种事情(例如 "father" 而不是 "int")。如果你继续沿着这条路走下去,你会发现 UnsafeMutablePointers 蒸发了,问题就消失了。但您不必走那么远,SharedPointer 会为您完成这项工作。
我找到了解决问题的方法。
import XCTest
class Person {
// case1
let pointer1: UnsafeMutablePointer<Int> = UnsafeMutablePointer<Int>.allocate(capacity: 1)
// case2
let pointer2: UnsafeMutablePointer<Int>
let name: String
init(name: String) throws {
// deallocate helper for case1
var deallocateHelper1: UnsafeMutablePointer<Int>? = self.pointer1
defer {
deallocateHelper1?.deallocate()
}
// case2
self.pointer2 = UnsafeMutablePointer<Int>.allocate(capacity: 1)
var deallocateHelper2: UnsafeMutablePointer<Int>? = self.pointer2
defer {
deallocateHelper2?.deallocate()
}
// case ...
if name == "UnsupportName" {
throw NSError()
}
self.name = name
// In the end. set deallocate helpers to nil
deallocateHelper1 = nil
deallocateHelper2 = nil
}
deinit {
pointer1.deallocate()
pointer2.deallocate()
}
}
class InterestTests: XCTestCase {
func testExample() {
while true {
_ = try? Person(name: "UnsupportName")
}
}
}
另一个解决方案。
class Person {
let name: String
let pointer1: UnsafeMutablePointer<Int>
let pointer2: UnsafeMutablePointer<Int>
init(name: String) throws {
var pointers: [UnsafeMutablePointer<Int>] = []
do {
let pointer1 = UnsafeMutablePointer<Int>.allocate(capacity: 1)
pointers.append(pointer1)
let pointer2 = UnsafeMutablePointer<Int>.allocate(capacity: 1)
pointers.append(pointer2)
if name == "Unsupported Name" {
throw NSError()
}
self.pointer1 = pointer1
self.pointer2 = pointer2
self.name = name
} catch {
pointers.forEach { [=10=].deallocate() }
throw error
}
}
deinit {
pointer1.deallocate()
pointer2.deallocate()
}
}
此示例代码存在内存泄漏。
pointer1 和 pointer2 在 Person 成功初始化之前分配。如果 init
函数抛出错误。 deinit
函数永远不会被执行。所以 pointer1 和 pointer2 将永远不会被释放。
import XCTest
class Person {
// case1
let pointer1: UnsafeMutablePointer<Int> = UnsafeMutablePointer<Int>.allocate(capacity: 1)
// case2
let pointer2: UnsafeMutablePointer<Int>
let name: String
init(name: String) throws {
// case2
self.pointer2 = UnsafeMutablePointer<Int>.allocate(capacity: 1)
if name == "UnsupportName" {
throw NSError()
}
self.name = name
}
deinit {
pointer1.deallocate()
pointer2.deallocate()
}
}
class InterestTests: XCTestCase {
func testExample() {
while true {
_ = try? Person(name: "UnsupportName")
}
}
}
有时候逻辑很复杂。在我的真实案例中。有很多allocate
和throws
与if
和guard
。有些很难控制。
有什么方法可以避免这种内存泄漏吗?
这是一个类似的问题:https://forums.swift.org/t/deinit-and-failable-initializers/1199
在您的具体示例中,解决方案很简单。在解决所有可能的故障之前不要分配任何内存:
class Person {
let aPointer: UnsafeMutablePointer<Int> // Do not allocate here.
let name: String
init(name: String) throws {
// Validate everything here
guard name != "UnsupportName" else {
throw NSError()
}
// After this point, no more throwing:
self.name = name
// Move the allocation here
self.aPointer = UnsafeMutablePointer.allocate(capacity: 1)
}
deinit {
aPointer.deallocate()
}
}
但更通用的解决方案是像使用其他地方一样使用 do/catch 来管理错误:
class Person {
let aPointer = UnsafeMutablePointer<Int>.allocate(capacity: 1)
let name: String
init(name: String) throws {
do {
if name == "UnsupportName" {
throw NSError()
}
self.name = name
} catch let e {
self.aPointer.deallocate()
throw e
}
}
deinit {
aPointer.deallocate()
}
}
我很想将 .allocate
移到 init
中,只是为了让它更清楚地显示正在发生的事情。关键是你应该首先分配所有内存,然后再抛出任何东西(这样你就知道你可以全部释放),或者在最后一次抛出之后分配所有内存(这样你就知道你没有任何东西可以释放)。
查看您添加的解决方案,没问题,但建议围绕它的危险逻辑。最好将其展开以将分配放入他们自己的对象中(这几乎肯定也会摆脱 UnsafeMutablePointers;在一个 class 中需要很多这样的对象是非常可疑的)。
也就是说,IMO 有更简洁的方法来沿着这条路径构建错误处理。
extension UnsafeMutablePointer {
static func allocate(capacity: Int, withCleanup cleanup: inout [() -> Void]) -> UnsafeMutablePointer<Pointee> {
let result = allocate(capacity: capacity)
result.addTo(cleanup: &cleanup)
return result
}
func addTo(cleanup: inout [() -> Void]) {
cleanup.append { self.deallocate() }
}
}
这让 UnsafeMutablePointers 可以将清理信息附加到一个数组中,而不是创建大量 defer
块,这会增加在清理期间丢失一个块的风险。
这样,您的 init 看起来像:
init(name: String) throws {
var errorCleanup: [() -> Void] = []
defer { for cleanup in errorCleanup { cleanup() } }
// deallocate helper for case1
pointer1.addTo(cleanup: &errorCleanup)
// case2
self.pointer2 = UnsafeMutablePointer<Int>.allocate(capacity: 1, withCleanup: &errorCleanup)
// case ...
if name == "UnsupportName" {
throw NSError()
}
self.name = name
// In the end. set deallocate helpers to nil
errorCleanup.removeAll()
}
当然,这会带来调用 allocate(capacity:)
而不是 allocate(capacity:withCleanup:)
的危险。所以你可以通过将它包装成另一种类型来解决这个问题;自动释放自身的引用类型。
class SharedPointer<Pointee> {
let ptr: UnsafeMutablePointer<Pointee>
static func allocate(capacity: Int) -> SharedPointer {
return .init(pointer: UnsafeMutablePointer.allocate(capacity: capacity))
}
init(pointer: UnsafeMutablePointer<Pointee>) {
self.ptr = pointer
}
deinit {
ptr.deallocate()
}
}
有了这个,这就变成了(不需要 deinit):
class Person {
// case1
let pointer1 = SharedPointer<Int>.allocate(capacity: 1)
// case2
let pointer2: SharedPointer<Int>
let name: String
init(name: String) throws {
// case2
self.pointer2 = SharedPointer<Int>.allocate(capacity: 1)
if name == "UnsupportName" {
throw NSError()
}
self.name = name
}
}
您可能想要编写各种帮助程序来处理 .ptr
。
当然,这可能会导致您构建特定版本的 SharedPointer 来处理各种事情(例如 "father" 而不是 "int")。如果你继续沿着这条路走下去,你会发现 UnsafeMutablePointers 蒸发了,问题就消失了。但您不必走那么远,SharedPointer 会为您完成这项工作。
我找到了解决问题的方法。
import XCTest
class Person {
// case1
let pointer1: UnsafeMutablePointer<Int> = UnsafeMutablePointer<Int>.allocate(capacity: 1)
// case2
let pointer2: UnsafeMutablePointer<Int>
let name: String
init(name: String) throws {
// deallocate helper for case1
var deallocateHelper1: UnsafeMutablePointer<Int>? = self.pointer1
defer {
deallocateHelper1?.deallocate()
}
// case2
self.pointer2 = UnsafeMutablePointer<Int>.allocate(capacity: 1)
var deallocateHelper2: UnsafeMutablePointer<Int>? = self.pointer2
defer {
deallocateHelper2?.deallocate()
}
// case ...
if name == "UnsupportName" {
throw NSError()
}
self.name = name
// In the end. set deallocate helpers to nil
deallocateHelper1 = nil
deallocateHelper2 = nil
}
deinit {
pointer1.deallocate()
pointer2.deallocate()
}
}
class InterestTests: XCTestCase {
func testExample() {
while true {
_ = try? Person(name: "UnsupportName")
}
}
}
另一个解决方案。
class Person {
let name: String
let pointer1: UnsafeMutablePointer<Int>
let pointer2: UnsafeMutablePointer<Int>
init(name: String) throws {
var pointers: [UnsafeMutablePointer<Int>] = []
do {
let pointer1 = UnsafeMutablePointer<Int>.allocate(capacity: 1)
pointers.append(pointer1)
let pointer2 = UnsafeMutablePointer<Int>.allocate(capacity: 1)
pointers.append(pointer2)
if name == "Unsupported Name" {
throw NSError()
}
self.pointer1 = pointer1
self.pointer2 = pointer2
self.name = name
} catch {
pointers.forEach { [=10=].deallocate() }
throw error
}
}
deinit {
pointer1.deallocate()
pointer2.deallocate()
}
}