Dispatch_once в Swift: главные ошибки и правильные альтернативы

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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                

Блоги

Рекомендаций по книгам