Make self weak in methods in Swift

后端 未结 5 2007
遇见更好的自我
遇见更好的自我 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 {
      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.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.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.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(
      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: Input0, _ input1: Input1) throws -> Output
    where Input == (Input0, Input1) {
      try self( (input0, input1) )
    }
    

提交回复
热议问题