【问题标题】:Implicitly unwrapped optional var destroyed by compiler before end of scope?在范围结束之前被编译器销毁的隐式解包的可选变量?
【发布时间】:2021-02-10 16:26:46
【问题描述】:

通过 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!

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

将 var 定义为常量:

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 及其 DisposeBags 的情况),那么我会遇到奇怪的行为,意外的行为!

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

这是一个更真实的场景,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) 中使用后立即被清除。

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

【问题讨论】:

    标签: swift variables scope rx-swift


    【解决方案1】:

    但作为程序员,我们习惯于函数内部的局部变量在离开作用域之前可用。

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

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

    var someObject: SomeClass! = SomeClass()
    
    withExtendedLifetime(someObject) {
        someObject.doSth()
        print("done")
    }
    

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-11-26
      • 2016-12-24
      • 1970-01-01
      • 2012-09-25
      • 2011-09-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多