【问题标题】:Swift: How to deallocate properties when init throw?Swift:初始化抛出时如何释放属性?
【发布时间】:2020-09-10 08:31:17
【问题描述】:

此示例代码存在内存泄漏。

pointer1pointer2 在 Person 初始化成功之前分配。如果init 函数抛出错误。 deinit 函数永远不会被执行。所以 pointer1pointer2 永远不会被释放。

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")
        }
    }

}

有时逻辑很复杂。在我的真实案例中。有很多allocatethrowsifguard。有些很难控制。

有什么办法可以避免这种内存泄漏?

这里有一个类似的问题:https://forums.swift.org/t/deinit-and-failable-initializers/1199

【问题讨论】:

  • 您可以在抛出错误之前调用father.deallocate()。但我想知道您想通过“手动内存管理”实现什么目标。为什么不简单地将father 声明为(可能是隐式解包的)可选项,并在适当的时候赋值?
  • 谢谢,@MartinR。我将“父亲”改为“aPointer”以减少混淆。我理解你。但实际上我有很多像“aPointer”这样的属性,并且在 init 函数中有很多抛出逻辑。每次抛出都很难手动解除分配。

标签: swift memory-leaks init throws deinit


【解决方案1】:

在您的具体示例中,解决方案很简单。在解决所有可能的故障之前不要分配任何内存:

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;在一个类中需要很多这些是非常可疑的)。

也就是说,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 来处理各种事情(例如“父亲”而不是“整数”)。如果你继续沿着这条路走,你会发现 UnsafeMutablePointers 消失了,问题就消失了。但是您不必走那么远,SharedPointer 会为您完成这项工作。

【讨论】:

  • 感谢您的建议。但有时逻辑非常复杂。在我的情况下。有很多allocatethrowsifguard。有些很难控制。
  • 那么我强烈建议重新设计以拆分分配并将它们放入自己拥有的类型中。这是C++处理多年的经典问题,通用的解决方案是RAII。不允许混合逻辑和分配。重新设计以将它们分成各自的部分。如果存在根据情况分配不同的东西的情况,那么您发现的是一个单独的类型来保存它。
  • 这是一个很好的解决方案。谢谢,@Rob Napier。在我的情况下确实如此。不仅有UnsafePointers,还有一些手动allocatedeallocate 用于libffiMy real project 很难全部打包到SharedPointers。不过谢谢你的好主意。
【解决方案2】:

我找到了解决问题的方法。

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")
        }
    }

}

【讨论】:

    【解决方案3】:

    另一种解决方案。

    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 { $0.deallocate() }
                throw error
            }
        }
    
        deinit {
            pointer1.deallocate()
            pointer2.deallocate()
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2020-03-05
      • 1970-01-01
      • 1970-01-01
      • 2014-10-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-12-07
      相关资源
      最近更新 更多