Dispatch_once в Swift: почему это ошибка и как писать правильный singleton
Если вы когда-нибудь задавались вопросом «Как сделать синглтон в Swift через dispatch_once?» — вы не одиноки. На Stack Overflow этот вопрос собрал тысячи просмотров, а в коде многих проектов до сих пор встречаются странные конструкции, пытающиеся эмулировать старый Objective-C подход. Но хорошая новость в том, что в Swift есть гораздо более элегантное и безопасное решение. Плохая новость: многие разработчики продолжают совершать одни и те же ошибки, пытаясь перенести привычки из Objective-C в Swift.
В этой статье мы разберём топ-5 ошибок при использовании dispatch_once в Swift, покажем, как их избежать, и дадим готовый шаблон правильного синглтона. Поехали!
Что такое dispatch_once и почему он не нужен в Swift?
dispatch_once — это функция из Grand Central Dispatch (GCD), которая гарантирует, что блок кода выполнится ровно один раз за всё время работы приложения. В Objective-C это был стандартный способ создания потокобезопасных синглтонов:
+ (instancetype)sharedInstance {
static MyClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[MyClass alloc] init];
});
return sharedInstance;
}
Однако в Swift, начиная с версии 1.0, dispatch_once не поддерживается для использования в пользовательском коде. Да, вы можете импортировать Dispatch и попытаться его вызвать, но компилятор этого не позволит. Почему? Потому что Swift предлагает встроенные средства, которые делают dispatch_once ненужным.
Топ-5 ошибок при использовании dispatch_once в Swift
Ошибка 1: Попытка вызвать dispatch_once напрямую
Самая распространённая ошибка — попытка написать что-то вроде:
class MySingleton {
static var shared: MySingleton = {
let instance = MySingleton()
// настройка
return instance
}()
private init() {}
}
На первый взгляд это работает. Но это не dispatch_once. Это просто глобальная переменная, которая инициализируется лениво (lazy) при первом обращении. Swift гарантирует потокобезопасность для таких статических свойств, но это не то же самое, что dispatch_once.
Почему это плохо? Потому что вы теряете контроль над моментом инициализации. Если ваш синглтон зависит от внешних ресурсов (например, базы данных или сети), они могут быть ещё не готовы.
Ошибка 2: Использование dispatch_once через обёртку на Objective-C
Некоторые разработчики создают мост на Objective-C, чтобы вызвать dispatch_once из Swift:
// Objective-C файл
@interface OnceHelper : NSObject
+ (void)performOnce:(void(^)(void))block;
@end
@implementation OnceHelper
static dispatch_once_t onceToken;
+ (void)performOnce:(void(^)(void))block {
dispatch_once(&onceToken, block);
}
@end
Затем в Swift:
class MySingleton {
static var shared: MySingleton = {
var instance: MySingleton!
OnceHelper.performOnce {
instance = MySingleton()
}
return instance
}()
}
В чём проблема? Во-первых, это костыль. Во-вторых, dispatch_once_t — глобальная переменная, и если вы забудете сбросить токен (например, при тестировании), синглтон не пересоздастся. В-третьих, Apple явно рекомендует не использовать dispatch_once в Swift.
Ошибка 3: Забыть про потокобезопасность
Некоторые пишут синглтон так:
class MySingleton {
static var shared: MySingleton?
private init() {}
static func getInstance() -> MySingleton {
if shared == nil {
shared = MySingleton()
}
return shared!
}
}
Это классический не-потокобезопасный синглтон. Если два потока одновременно вызовут getInstance(), может создаться два экземпляра. В Swift это приведёт к race condition и неопределённому поведению.
Ошибка 4: Использование dispatch_once для не-синглтон задач
Некоторые разработчики пытаются использовать dispatch_once для инициализации глобальных ресурсов (например, настройка логгера или конфигурации). В Swift для этого есть lazy var или static let. Но если вы попытаетесь вызвать dispatch_once внутри функции, компилятор выдаст ошибку.
Пример неправильного кода:
func setupLogger() {
dispatch_once { // Ошибка: 'dispatch_once' is unavailable in Swift
// настройка логгера
}
}
В Swift нет прямого аналога dispatch_once для произвольных блоков кода. Но есть lazy var, который делает то же самое для свойств.
Ошибка 5: Игнорирование тестируемости
Синглтоны, построенные на dispatch_once, сложно тестировать. Если вы используете глобальный токен, вы не можете сбросить состояние между тестами. В Swift это решается через внедрение зависимостей (dependency injection) или использование протоколов.
Как правильно реализовать синглтон в Swift?
Apple официально рекомендует использовать статическую константу с ленивой инициализацией. Это потокобезопасно, просто и эффективно:
class MySingleton {
static let shared = MySingleton()
private init() {
// приватный инициализатор предотвращает создание других экземпляров
}
func doSomething() {
print("Singleton работает")
}
}
// Использование:
MySingleton.shared.doSomething()
Почему это работает?
- static let гарантирует, что свойство инициализируется только один раз.
- Ленивая инициализация — свойство создаётся при первом обращении.
- Потокобезопасность — Swift гарантирует, что инициализация статических свойств потокобезопасна (даже без dispatch_once).
- Приватный init предотвращает создание других экземпляров.
Альтернативы для сложных случаев
Если вам нужно больше контроля (например, передать параметры при инициализации), используйте фабричный метод с lazy var внутри класса:
class MySingleton {
private static var _shared: MySingleton?
static func shared(with parameter: String) -> MySingleton {
if _shared == nil {
_shared = MySingleton(parameter: parameter)
}
return _shared!
}
private let parameter: String
private init(parameter: String) {
self.parameter = parameter
}
}
// Использование:
let instance = MySingleton.shared(with: "test")
Обратите внимание: этот вариант не потокобезопасен. Для потокобезопасности используйте os_unfair_lock или DispatchQueue.
Потокобезопасный синглтон с кастомной инициализацией
class ThreadSafeSingleton {
private static var _shared: ThreadSafeSingleton?
private