Помогите понять метод класса, возвращающий singleton

Может кто-нибудь помочь мне понять, что делает следующий метод?

+ (Game *) shared
{
    static Game *sharedSingleton;

    @synchronized(self)
    {
        if (!sharedSingleton)
        {
            sharedSingleton = [[Game alloc] init];
        }
    }

    return sharedSingleton;
}

Ответ 1

Очевидно, что идея одиночного элемента состоит в создании только одного экземпляра. Первым шагом в достижении этого является объявление статического экземпляра класса через строку static Game *sharedSingleton;.

Второй шаг - проверить, был ли уже создан один экземпляр, а если нет, создать его или вернуть его, чтобы вернуть существующий отдельный экземпляр. Однако этот второй шаг открывает потенциальные проблемы в случае, если два отдельных потока пытаются вызвать метод +shared в тот же самый момент. Вы бы не хотели, чтобы один поток изменял единственную переменную sharedSingleton, в то время как другой поток пытается ее изучить, поскольку это может привести к неожиданным результатам.

Решение этой проблемы заключается в использовании директивы компилятора @synchronized() для синхронизации доступа к объекту, указанному в скобках. Например, скажем, что этот единственный общий экземпляр класса Game имеет переменную экземпляра с именем players, которая представляет собой NSMutableArray экземпляров класса Player. Скажем, класс Game имел метод -addPlayer:, который модифицировал бы переменную экземпляра players путем добавления указанного игрока. Важно, что если бы этот метод был вызван из нескольких потоков, то только одному потоку разрешалось изменять массив players за раз. Таким образом, реализация этого метода может выглядеть примерно так:

- (void)addPlayer:(Player *)player {
   if (player == nil) return;
   @synchronized(players) {
      [players addObject:player];
   }
}

Использование директивы @synchronized() гарантирует, что только один поток может получить доступ к переменной players за раз. Если один поток пытается использовать другой поток в настоящий момент, первый поток должен ждать завершения другого потока.

Хотя это более прямолинейно, когда вы говорите об переменной экземпляра, возможно, менее понятно, как достичь одного и того же результата в одном методе создания самого класса. Строка self в строке @synchronized(self) в следующем коде в основном приравнивается к классу Game. Синхронизируя по классу Game, он гарантирует, что строка sharedSingleton = [[Game alloc] init]; вызывается только один раз.

+ (Game *) shared
{
    static Game *sharedSingleton;

    @synchronized(self) // assures only one thread can call [Game shared] at a time
    {
        if (!sharedSingleton)
        {
            sharedSingleton = [[Game alloc] init];
        }
    }

    return sharedSingleton;
}

[EDIT]: обновлено. На основании моих тестов некоторое время назад (и я только что повторно протестировал его сейчас), все они выглядят эквивалентно:

Внешний @implementation:

Game *sharedInstance;

@implementation Game
+ (Game *)sharedGame {
    @synchronized(self) {
        if (sharedInstance == nil) {
            sharedInstance = [[[self class] alloc] init];
        }
    }
    return sharedInstance;
}
@end

Вне @implementation, static:

static Game *sharedInstance;

@implementation Game
+ (Game *)sharedGame {
    @synchronized(self) {
        if (sharedInstance == nil) {
            sharedInstance = [[[self class] alloc] init];
        }
    }
    return sharedInstance;
}
@end

Внутри @implementation:

@implementation Game

static Game *sharedInstance;

+ (Game *)sharedGame {
    @synchronized(self) {
        if (sharedInstance == nil) {
            sharedInstance = [[[self class] alloc] init];
        }
    }
    return sharedInstance;
}
@end

Внутри +sharedGame:

@implementation Game
+ (Game *)sharedGame {
    static Game *sharedInstance;
    @synchronized(self) {
        if (sharedInstance == nil) {
            sharedInstance = [[[self class] alloc] init];
        }
    }
    return sharedInstance;
}
@end

Единственное отличие состоит в том, что в первом варианте, без ключевого слова static, sharedInstance не отображается в разделе "Статистика файлов" в gdb. И, очевидно, в последнем варианте sharedInstance не отображается вне метода +sharedGame. Но практически все они уверяют, что когда вы вызываете [Game sharedInstance], вы вернете sharedInstance и что sharedInstance создается только один раз. (Обратите внимание, однако, что необходимы дальнейшие меры предосторожности, чтобы кто-то не мог создать экземпляр, отличный от одиночного, используя что-то вроде Game *game = [[Game alloc] init];).

Ответ 2

Пошаговое объяснение...

// A static variable guarantees there only 1 instance of it ever, 
// even accross multiple instances of the same class, this particular
// variable will store the class instance, so it can be returned whenever
// a client-class requests an instance of this class.
static Game *sharedSingleton;

// create a method that can always be called, even if there no instance yet
// this method should create a new instance if there isn't one yet, otherwise
// return the existing instance
+ (Game *) shared  
{
    // synchronized makes sure only 1 client class can enter this method at any time, 
    // e.g. to prevent creating 2 instances whenever 2 client-classes try to 
    // access the following code from different threads.
    @synchronized(self)
    {
        // if the static variable is called for the first time, 
        // create an instance and return it, otherwise return the existing instance ...
        if (!sharedSingleton)
        {
            sharedSingleton = [[Game alloc] init];
        }
    }

    return sharedSingleton;
}

Ответ 3

Я мало знаю Objective-C, но, похоже, это метод получения singleton копии типа Game * и должен быть потокобезопасным.

По существу, вызов этого метода будет возвращать ту же самую копию игры * каждый раз (независимо от того, какой поток), и не будет выделять новый экземпляр игры до тех пор, пока он не будет вызван (известный как lazy-loading)

Ответ 4

Я бы также переопределил mutableCopyWithZone: и copyWithZone: методы, чтобы избежать копирования singleton:

// copy cannot be done
- (Game *)copyWithZone:(NSZone *)zone {
    return self;
}
// mutablecopy cannot be done
- (Game *)mutableCopyWithZone:(NSZone *)zone {
    return [self copyWithZone:zone];
}

Так как copy и mutableCopy (при условии, что класс singleton не наследуется от другого класса, который имеет переопределенные реализации NSObject по умолчанию), просто вызовите copyWithZone: и mutableCopyWithZone: не нужно также переопределять их.

Чтобы другие разработчики не нарушали одноэлементный шаблон с помощью init или new, два метода могут быть объявлены недоступными:

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;

Таким образом, компилятор не позволит использовать два метода, а другие разработчики будут вынуждены использовать документированный метод для получения общего экземпляра. Другой - более строгий метод - переопределить init и new и создать исключение.