Make self weak in methods in Swift

后端 未结 5 1989
遇见更好的自我
遇见更好的自我 2020-12-05 07:06

I have a Swift class that needs to store a table of its own methods. Unfortunately this is causing a reference cycle, because its table retains references to self

相关标签:
5条回答
  • 2020-12-05 07:45

    With Swift 5.2, callAsFunction allows for nice syntax for this, until argument labels come into play.

    public struct WeakMethod<Reference: AnyObject, Input, Output> {
      public init(
        reference: Reference?,
        method: @escaping Method
      ) {
        self.reference = reference
        self.method = method
      }
    
      public weak var reference: Reference?
      public var method: Method
    }
    
    public extension WeakMethod {
      struct ReferenceDeallocatedError: Error { }
    
      typealias Method = (Reference) -> (Input) -> Output
    
      /// - Throws: ReferenceDeallocatedError
      func callAsFunction(_ input: Input) throws -> Output {
        guard let reference = reference
        else { throw ReferenceDeallocatedError() }
    
        return method(reference)(input)
      }
    }
    
    public extension WeakMethod where Input == () {
      init(
        reference: Reference?,
        method: @escaping (Reference) -> () -> Output
      ) {
        self.reference = reference
        self.method = { reference in
          { _ in method(reference)() }
        }
      }
    
      /// - Throws: ReferenceDeallocatedError
      func callAsFunction() throws -> Output {
        try self( () )
      }
    }
    
    final class WeakMethodTestCase: XCTestCase {
      func test_method() throws {
        var reference: Reference? = Reference()
    
        let assign1234 = WeakMethod(reference: reference, method: Reference.assign1234)
    
        try assign1234()
        XCTAssertEqual(reference?.property, 1234)
    
        reference = nil
        XCTAssertThrowsError( try assign1234() ) {
          XCTAssert($0 is WeakMethod<Reference, (), Void>.ReferenceDeallocatedError)
        }
      }
    
      func test_closure_noParameters() throws {
        var reference: Reference? = Reference()
    
        let assign1234 = WeakMethod(reference: reference) {
          reference in { reference.property = 1234 }
        }
    
        try assign1234()
        XCTAssertEqual(reference?.property, 1234)
    
        reference = nil
        XCTAssertThrowsError( try assign1234() ) {
          XCTAssert($0 is WeakMethod<Reference, (), Void>.ReferenceDeallocatedError)
        }
      }
    
      func test_closure_1Parameter() throws {
        var reference: Reference? = Reference()
    
        let assign = WeakMethod(reference: reference) {
          reference in { reference.property = $0 }
        }
    
        try assign(1234)
        XCTAssertEqual(reference?.property, 1234)
    
        reference = nil
        XCTAssertThrowsError( try assign(1234) ) {
          XCTAssert($0 is WeakMethod<Reference, Int, Void>.ReferenceDeallocatedError)
        }
      }
    }
    
    private final class Reference {
      var property = 1
    
      func assign1234() {
        property = 1234
      }
    }
    

    Unfortunately, as you keep adding parameters, you'll need to keep adding initializer+method pairs, like so:

    init<Input0, Input1>(
      reference: Reference?,
      method: @escaping (Reference) -> (Input0, Input1) -> Output
    )
    where Input == (Input0, Input1) {
      self.reference = reference
      self.method = { reference in
        { method(reference)($0.0, $0.1) }
      }
    }
    
    /// - Throws: ReferenceDeallocatedError
    func callAsFunction<Input0, Input1>(_ input0: Input0, _ input1: Input1) throws -> Output
    where Input == (Input0, Input1) {
      try self( (input0, input1) )
    }
    
    0 讨论(0)
  • 2020-12-05 07:49

    In Swift 4 (I am not sure when the syntax became available), simply do { [weak self] (params) in } to make self weak. It basically is to [unowned self], what Self? is to Self!. The compiler even requires self?.foo instead of simply self.foo.

    0 讨论(0)
  • 2020-12-05 07:51

    Robs answer worked for me. I did refactor it to be a little more OO though so I thought I would share it here in case it helps someone else:

    public protocol WeakCallback{ 
        func invoke()
    }
    
    public class WeakCallbackInstance<T: AnyObject> : WeakCallback{
        private let callback: ()->Void
        private weak var target: T?
    
        public init(target: T, action: (T)->()->Void){
    
            self.target = target
            callback = { [weak target] in
                action(target!)()
            }
        }
    
        public func invoke(){
            callback()
        }
    }
    
    class ExampleUsage{
    
        func usage(){
            var callbacks = [WeakCallback]()
    
            let one = WeakCallbackInstance(target: DummyCallbackOne(), action:DummyCallbackOne.callbackOne)
            let two = WeakCallbackInstance(target: DummyCallbackTwo(), action:DummyCallbackTwo.callbackTwo)
    
            callbacks.append(one)
            callbacks.append(two)
            callbacks.first?.invoke()
        }
    }
    
    class DummyCallbackOne{
        func callbackOne(){
        }
    }
    
    class DummyCallbackTwo{
        func callbackTwo(){
        }
    }
    
    0 讨论(0)
  • 2020-12-05 07:53

    You can certainly build a function for this. I don't know if it makes it dramatically better, but it is less error-prone.

    func methodPointer<T: AnyObject>(obj: T, method: (T) -> () -> Void) -> (() -> Void) {
      return { [unowned obj] in method(obj)() }
    }
    ...
    myCallbacks.append(methodPointer(self, CycleInducingClass.myInternalFunction))
    

    Alternately, you could manage your callbacks as method pointers:

    typealias Callback = (CycleInducingClass) -> () -> Void
    ...
    myCallbacks.append(CycleInducingClass.myInternalFunction)
    

    In that case, you'd need to pass self when you called them (which may be fine if you don't actually do this a lot):

    self.myCallbacks[0](self)()
    

    All of this is based on the fact that a method on type T with signature (input) -> (output) is equivalent to a function with the signature (T) -> (input) -> (output).

    In case you're curious (I was), overriding works correctly in this case. So if you subclass CycleInducingClass and override myInternalFunction, the correct version will be called. (That actually surprises me a little, and I don't yet know exactly why it works, but it does.)

    EDIT: Here's the answer to that: https://devforums.apple.com/message/1036509#1036509

    0 讨论(0)
  • 2020-12-05 07:58

    wrapped no param function with block

    myCallbacks.append({ [unowned self] in self.myInternalFunction() })
    

    wrapped param function with block

    myCallbacks.append({ [unowned self] page in self.reloadData(page: page) })
    
    0 讨论(0)
提交回复
热议问题