У меня сбой, связанный с ленивыми переменами в Swift. Причину катастрофы легко понять, но я не знаю хорошего способа предотвратить ее, не теряя преимуществ, которые я получаю, используя ленивый var.
У меня есть класс, который лениво создает экземпляр службы, когда она используется. Экземпляр службы должен быть остановлен, если он был запущен, но он не обязательно запускается каждый раз.
class MyClass {
lazy var service: MyService = {
// To init and configure this service,
// we need to reference `self`.
let service = MyService(key: self.key) // Just pretend key exists :)
service.delegate = self
return service
}
func thisGetsCalledSometimes() {
// Calling this function causes the lazy var to
// get initialised.
self.service.start()
}
deinit {
// If `thisGetsCalledSometimes` was NOT called,
// this crashes because the initialising closure
// for `service` references `self`.
self.service.stop()
}
}
Как мне избежать этого сбоя, желательно, сохраняя ленивый var, и не вводя слишком много нового обслуживания?
EDIT:
Я не мог представить аварию на детской площадке, но смог, когда встроил этот сценарий в контроллер вида. Для воспроизведения создайте новый проект Xcode с одним шаблоном контроллера представления и замените код в ViewController.swift следующим:
import UIKit
// Stuff to create a view stack:
class ViewController: UINavigationController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let firstController = FirstController()
let navigationController = UINavigationController(rootViewController: firstController)
self.present(navigationController, animated: false, completion: nil)
}
}
class FirstController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton()
button.setTitle("Next screen", for: .normal)
button.addTarget(self, action: #selector(onNextScreen), for: .touchUpInside)
self.view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
}
@objc func onNextScreen() {
let secondController = SecondController()
self.navigationController?.pushViewController(secondController, animated: true)
}
}
// The service and view controller where the crash happens:
protocol ServiceDelegate: class {
func service(_ service: Service, didReceive value: Int)
}
class Service {
weak var delegate: ServiceDelegate?
func start() {
print("Starting")
self.delegate?.service(self, didReceive: 0)
}
func stop() {
print("Stopping")
}
}
class SecondController: UIViewController {
private lazy var service: Service = {
let service = Service()
service.delegate = self
return service
}()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// service.start() // <- Comment/uncomment to toggle crash
}
deinit {
self.service.stop()
}
}
extension SecondController: ServiceDelegate {
func service(_ service: Service, didReceive value: Int) {
print("Value: \(value)")
}
}
Когда приложение будет запущено, оно покажет контроллер представления с кнопкой «Следующий экран». Нажатие на эту кнопку переводит второй контроллер вида в стек навигации. Нажатие кнопки «Назад» в навигационной панели приведет к появлению проблемы:
- Если
service.start()
(в viewWillAppear
) оставить незакомментированным, служба инициализируется, и при нажатии кнопки «Назад» не происходит сбоя во время deinit.
- Если
service.start()
закомментирован, сервис не инициализируется до deinit. Затем при нажатии кнопки «назад» приложение вылетает на линии service.delegate = self
.
В минимальном примере сбой вызывает следующую ошибку, которую я не видел в моем реальном приложении:
objc [88348]: Невозможно сформировать слабую ссылку на экземпляр (0x7facade14650) класса TestDeinitWithLazyVar.SecondController. Возможно, этот объект был чрезмерно освобожден или находится в процессе освобождения.
Интересно, что сбой происходит только тогда, когда задействован UIKit, но я думаю, что пример игровой площадки все еще указывает на проблему: я хотел бы избежать инициализации переменной lazy во время deinit. С этой спецификацией проблемы, как указал @Martin R, этого основанного на флаге решения должно быть достаточно.
Теперь мне интересно, почему он падает с контроллером вида!
РЕДАКТИРОВАТЬ 2:
Похоже, что не UIKit
вызывает сценарий сбоя, а использует класс NSObject
. Вот минимальный пример, который вызывает сбой в Playground:
import Foundation
protocol MyServiceDelegate: class {}
class MyService {
weak var delegate: MyServiceDelegate?
func stop() {}
}
class MyClass: NSObject, MyServiceDelegate {
lazy var service: MyService = {
let service = MyService()
service.delegate = self
return service
}()
deinit {
print("Deiniting...")
self.service.stop()
}
}
func test() {
let myClass = MyClass()
}
test()
ОБНОВЛЕНИЕ 19 июля 2019 года:
Я только что натолкнулся на это предложение об оболочках свойств в Swift , которое предоставило бы некоторые изящные решения проблемы. Например, мы могли бы расширить оболочку отложенного свойства, чтобы предоставить значение, если оно инициализировано, или вернуть nil (примечание: код не тестировался):
extension Lazy<T> {
var ifInitialised: T? {
guard case . initialized(let value) = self else { return nil }
return value
}
}
Тогда мы могли бы просто сделать
deinit {
self.service.ifInitialised?.stop()
}