在范围结束之前隐式解包的可选 var 被编译器销毁?

Implicitly unwrapped optional var destroyed by compiler before end of scope?

使用 swift 编译器优化隐式解包的可选变量不会在整个范围内存活,而是在使用后立即释放。

这是我的环境:

swift --version

产出

Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28)
Target: x86_64-apple-darwin20.2.0

Xcode 版本为 Version 12.3 (12C33)

考虑这个显示问题的最基本的例子:

final class SomeClass {
    
    func doSth() {}
    
    deinit {
        print("deinit")
    }
}

func do() {
    var someObject: SomeClass! = SomeClass()

    someObject.doSth()
    
    print("done")
}

这应该输出

done
deinit

但是,在发布版本中(启用 Swift 代码优化“-O”),它会以相反的方式打印:

deinit
done

只有 var someObject: SomeClass! 是这种情况。

该代码的以下更改全部正确输出(意味着当函数范围离开时对象被释放):

将变量定义为常数:

func doSthSpecial() {
    let someObject: SomeClass! = SomeClass()

    someObject.doSth()
    
    print("done")
}

明确定义 var 为可选:

func doSthSpecial() {
    var someObject: SomeClass? = SomeClass()

    someObject.doSth()
    
    print("done")
}

像可选的一样访问:

func doSthSpecial() {
    var someObject: SomeClass! = SomeClass()

    someObject?.doSth()
    
    print("done")
}

这最后三个实现全部输出

done
deinit

按照这个顺序。

不知怎的,这让我无语了‍♂️。 我理解这种优化,这是有道理的。但作为一名程序员,我们习惯于函数内部的局部变量在离开作用域之前一直可用。

我这里遇到的问题是关于存储在这种隐式展开的可选变量中的对象的生命周期。如果我的代码依赖于此对象的生命周期(例如 RxSwift 及其 DisposeBag 的情况),那么我会出现奇怪的行为,意外的行为!

我可以认为这是 Swift 中的错误,但您怎么看?错误还是没有错误?

这是一个更真实的场景 RxSwift,您可以在其中使用这样的结构:

import UIKit
import RxSwift

final class SomeClass {
    
    func doSth() {}
    
    deinit {
        print("deinit")
    }
}

class ViewController: UIViewController {
    
    let providePassword = PublishSubject<String>()
    lazy var askForPassword: Observable<String> = {
        return Observable.create { observer in
            _ = self.providePassword.subscribe(observer)
            
            return Disposables.create()
        }
        .debug(">>> ask for password signal")
    }()

    private func performAsyncSyncTask() {
        DispatchQueue.global().async {
            var disposeBag: DisposeBag! = DisposeBag()
            
            let sema = DispatchSemaphore(value: 0)
            
            self.askForPassword
                .subscribe(onNext: { pw in
                    print(pw)
                    sema.signal()
                })
                .disposed(by: disposeBag)
                    
            _ = sema.wait(timeout: DispatchTime.distantFuture)
            
            disposeBag = nil
        }
    }
    
    @IBAction func startAskPassword(sender: AnyObject) {
        self.performAsyncSyncTask()
    }
    
    @IBAction func sendPassword(sender: AnyObject) {
        self.providePassword.on(.next("hardcoded pw"))
    }
}

这里的问题是:当执行 self.performAsyncSyncTask() 时,它订阅了 askForPassword 但因为在优化构建中,隐式展开的可选变量在 .disposed(by: disposeBag) 中使用后立即被清除。

这会在订阅后立即销毁信号。

But as a programmer we are used to local variables inside of functions being available until leaving the scope.

自从首次为 ObjC 发布 ARC 以来,情况就不是这样了。 ARC 总是可以选择在最后一次使用后释放对象(并且经常使用它)。这是设计使然,不是 Swift 中的错误(或者在 ObjC 中,它也是如此)。

在 Swift 中,如果您希望将对象的生命周期延长到其最后一次使用之后,withExtendedLifetime 明确用于此目的。

var someObject: SomeClass! = SomeClass()

withExtendedLifetime(someObject) {
    someObject.doSth()
    print("done")
}

请记住,对象对它们进行平衡 retain/autorelease 调用是合法的,这也可能导致它们超出其范围。这在 Swift 中不太常见,但仍然合法,并且如果您将 Swift 对象传递给 ObjC 就会发生(这可能发生在许多您可能意想不到的地方)。

你应该非常小心 deinit 何时被调用。它可能会让您感到惊讶,甚至在所有情况下都没有承诺(例如,在 Mac 上程序退出期间不会调用 deinit,这往往会让 C++ 开发人员感到惊讶)。

IMO performAsyncSyncTask 是一个危险的模式,应该重新设计,所有权更清晰。我没有做足够的 RxSwift 工作来立即重新设计它,但是在 DispatchSemaphore 上阻塞整个线程似乎是与任何响应式系统集成的错误方法。线程是一种有限的资源,这会迫使系统创建更多线程,而这个线程被阻塞什么都不做。