Swift 5.1 aggressive release of values assigned to underscore

In recent Xcode 11 betas, Swift 5.1 has become more aggressive about releasing values assigned to the underscore. This means I must leave a warning in the code if I'm not using the value directly, but expecting a side effect (e.g., with Combine).


For example, the following unit test passes, but contains the warning on line 6: "Initialization of immutable value 'unusedButNeeded' was never used; consider replacing with assignment to '_' or removing it."


func testPassthroughSubjectSequence() {
    let expectedResult = [1, 2, 3]
    var actualResult: [Int] = []

    let subject = PassthroughSubject<Int, Never>()
    let unusedButNeeded = subject.sink { actualResult.append($0) }
    for value in expectedResult {
        subject.send(value)
    }

    XCTAssertEqual(actualResult, expectedResult)
}


However, if I take the advise of the warning and change that line to assign to '_' the test fails because the value is released before the closure is invoked. This worked in earlier compiler versions because apparently the release was delayed.


_ = subject.sink { actualResult.append($0) }


Is there some way to suppress this warning, or prevent the new aggressive release behavior in this case?


Thanks,

Nick

Accepted Reply

Seems assigning to an unused local variable is not the right way to keep reference to a value, in Swift.


You may need to write something like this:

func testPassthroughSubjectSequence() {
    let expectedResult = [1, 2, 3]
    var actualResult: [Int] = []
    
    let subject = PassthroughSubject<Int, Never>()
    withExtendedLifetime(subject.sink { actualResult.append($0) }) {
        for value in expectedResult {
            subject.send(value)
        }
    }
    
    XCTAssertEqual(actualResult, expectedResult)
}

Replies

Seems assigning to an unused local variable is not the right way to keep reference to a value, in Swift.


You may need to write something like this:

func testPassthroughSubjectSequence() {
    let expectedResult = [1, 2, 3]
    var actualResult: [Int] = []
    
    let subject = PassthroughSubject<Int, Never>()
    withExtendedLifetime(subject.sink { actualResult.append($0) }) {
        for value in expectedResult {
            subject.send(value)
        }
    }
    
    XCTAssertEqual(actualResult, expectedResult)
}

Sure, but if there are legitimate reasons to assign to variables you don't reference, there should be a way to suppress the warning.


Thanks for suggesting withExtendedLifetime, it does get pretty obscure from a readability point of view when you end up with closures inside the call, then another outside.

You really need the

withExtendedLifetime(…)
here. The
sink(…)
method is returning an
AnyCancellable
object. Swift immediately releases that object, which will cancel your subscription. This results in a race between the code doing the send and that cancellation. The only way to guarantee that the sending code wins that race is to guarantee that the
AnyCancellable
is not released, and what’s what
withExtendedLifetime(…)
is for.

If want to avoid the extra indentation you could add infrastructure for deferring these cancellables:

var deferredCancellables: [AnyCancellable] = []

func deferCancellable(cancellable: AnyCancellable) {
    self.deferredCancellables.append(cancellable)
}

override func tearDown() {
    deferredCancellables.removeAll()
}

Your test code can then do this:

func testPassthroughSubjectSequence {
    …
    let unusedButNeeded = subject.sink …
    self.deferCancellable(unusedButNeeded)
    …
}

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

For posterity, I’m depending on an implementation detail of the compiler here. This isn’t C++ where the value is guaranteed to be destructed when it goes out of scope. Perhaps in this case the most readable solution is simply to explicitly cancel the sink before finishing the test. Otherwise, I find it more readable to keep the variable and call withExtendedLifetime on it.


func testPassthroughSubjectSequence() {
  let expectedResult = [1, 2, 3]
  var actualResult: [Int] = []

  let subject = PassthroughSubject<Int, Never>()
  let unusedButNeeded = subject.sink { actualResult.append($0) }

  withExtendedLifetime(unusedButNeeded) {
       for value in expectedResult {
            subject.send(value)
       }
  }
  
  XCTAssertEqual(actualResult, expectedResult)  
}