单例模式或类方法

6

在阅读有关Objective C中单例的问题的回答后,似乎每个解决方案在实例访问器的线程方面都做出了一些权衡。即:

@synchronized(self)
{
    if (sharedInstance == nil)
            sharedInstance = [[MySingleton alloc] init];
}
return sharedInstance;

这实际上是将对单例的访问单线程化,如果在操作中频繁使用它,似乎会导致线程不必要地竞争。

直接使用类对象作为单例实例,并通过类方法公开功能,有什么缺点呢?

@interface MySingleton : NSObject {
}

+ (void)doSomething;
@end

@implementation MySingleton    
+ (void)initialize {
   //do some setup if necessary
}

+ (void)doSomething {
    //do something
}
@end

这样我们就避免了每次引用单例对象时进行锁定+检查的操作,同时也消除了将其存储在本地或方法实例变量中的需要。这种方法还可以让运行时保证系统中任何给定时间只存在一个实例(Class对象)。 编辑 这里不仅涉及到线程处理,对于传统的单例模式,通常会编写以下代码:
MySingleton *instance = [MySingleton getSharedInstance];
NSObject *someResult = [instance getResult];
//or
if (instance.someProperty) {
  //do something
}

然而,如果你的单例是类实例,你基本上可以消除一直调用 getSharedInstance 的需要。请考虑以下代码:

NSObject *someResult = [MySingleton getResult];
//or
if ([MySingleton someProperty]) {
  //do something
}

我听到的观点是你必须将数据存储在文件本地静态变量或全局变量中(呕吐),但它与传统的单例并没有什么不同,除了失去了Objective-C 2.0属性(相反,您必须使用传统的访问器方法)。
对我来说,这里有一个关键的权衡,似乎是一种胜利。在传统的单例中,如果您真的想要做得正确,最终会覆盖-copyWithZone、+allocWithZone、-retain、-retainCount、-release和-autorelease。
这似乎是每次想编写简单的Singleton对象时都需要做很多工作。那为什么不简单地用这个替换它呢:
@implementation MySingleton
+ (void)initialize {
    //do your setup
}

- (id)init {
    NSAssert(NO, @"You should read the documentation on singletons.");
}
@end

代码轻量,除非用户非常机智,否则不会创建两个实例。

言归正传 我的问题是:

使用类对象作为单例的实例是否有任何缺点?

似乎可以采取同样的线程安全、内存效率等措施,而不必记住覆盖那么多方法和访问器或在代码中添加实例检查。


我使用顶级方法。如果我知道在一个方法中我将频繁使用单例,我会先获取它并将其分配给一个本地变量。 - JeremyP
我认为你的理论并没有什么问题,但你需要意识到,阅读你的代码的人会有些困惑 -- 你在调用类方法,但却像操作对象一样。 - slycrel
@slycrel 同意。我通常会尽量写很多注释,所以我认为这会有所帮助。当然,我假设有人会阅读这些注释,但实际上可能是错误的。 - ImHuntingWabbits
4个回答

5

如果您使用iOS 4.0或更高版本,则最好的解决方案是仅使用dispatch_once,如下所示:

+ (id)sharedInstance {
    static dispatch_once_t predicate;
    dispatch_once(&predicate, ^{
        sharedInstance = [[MyClass alloc] init];
    });
    return sharedInstance;
}

你可能还想考虑使用一个单独的dispatch_queue来序列化对类内部的访问。如果所有公共方法都在同一个dispatch_queue上运行一个块,那么你就不必担心并发问题了。


1
我认为这绝对解决了线程安全的问题,但我不认为这是我的问题的重点,我已经相应地修改了它。谢谢! - ImHuntingWabbits
我认为我不想为每个公共方法运行一个块。我认为@synchronized更清晰。 - JeremyP
2
@JeremyP:该代码块仅运行一次。(因此在dispatch_once中有“once”)。对于只需要发生一次的事情,这个提议比@synchronized更有效率。 - BJ Homer
3
dispatch_once 在单线程测试中比 synchronized 快 3 倍,在多线程测试中可快达 20 倍。参见 http://bjhomer.blogspot.com/2011/09/synchronized-vs-dispatchonce.html。 - BJ Homer
1
@JeremyP 在实践中,你是正确的,它可能不会成为瓶颈。我仍然认为dispatch_once@synchronized(self) { if (!sharedInstance) { ... } }更能表达“只运行一次这段代码”的含义。 - BJ Homer
显示剩余3条评论

3

这是我在Stack Overflow上的第一篇帖子...(所以请准备好接受愚蠢的问题)

我认为有一种混合解决方案可能会很有用。

我想要从单例类中设置和获取(全局)值,而不必调用“getSharedInstance”。我希望代码看起来像这样...

frameRate = Singleton.frameRate;
Singleton.frameRate = 42;

为了达到这个目的,我们需要在单例模式中为每个需要存储的变量设置一个getter和setter类方法。然后,该类方法会访问实例并将数据存储在ivar中。主程序不会直接访问该实例。
getter方法如下所示:
+ (int) frameRate
{
    return [[Singleton instance] ivarFrameRate];
}

(丑陋的)实例调用被隐藏在类代码中。

在此调用实例方法时,当首次使用该类方法时,它将自动实例化对象。一旦单例模式被实例化,实例将按照传统方式存储ivars。在这里,我使用“ivar”作为前缀是为了使ivars更明确。

@property  int ivarFrameRate;

并且

@synthesize ivarFrameRate;

这会自动创建传统的getter(和setter)方法来访问ivar。
(编辑 - 这是一个完整的示例)
//  Singleton.h
#import <Foundation/Foundation.h>
@interface Singleton : NSObject
{
float ivarFrameRate
}

@property  float ivarFrameRate;

- (id) init;
+ (Singleton *) instance;
+ (float) frameRate;
+ (void) setFrameRate:(float)fr;
@end

并且

//  Singleton.m
#import "Singleton.h"
@implementation Singleton
@synthesize ivarFrameRate;

static Singleton* gInstance = NULL;

+ (Singleton*)instance
{
    @synchronized(self)
    {
        if (gInstance == NULL)
        gInstance = [[self alloc] init];
    }
    return(gInstance);
}


- (id)init
{
    self = [super init];
    return self;
}

+ (float) frameRate
{
    return [[Singleton instance] ivarFrameRate];
}

+ (void) setFrameRate:(float)fr;
{
    [[Singleton instance] setIvarFrameRate:fr];
}

无法编译。在Objective-C中,类不能拥有属性,编译器也不知道将Singleton.frameRate更改为[Singleton frameRate]。 - ImHuntingWabbits
我可以向您保证它可以编译,并且我正在使用单例模式将一些变量设置为全局。这可能有点冗长,因为每个getter和setter实际上都要写两次。单个对象实例将变量存储为ivar。属性与实例相关联。然后,(+)类方法从单个实例中设置和获取变量。 - Glyn Williams
请完整地发布代码,我仍然无法编译Singleton.frameRate。 - ImHuntingWabbits
我已经在我的帖子中添加了完整的代码。(上面) - Glyn Williams
1
经过长时间的拖延,我将您的代码复制到一个测试项目中,并且正确地运行了,它非常顺畅。我认为这个解决方案将为我困扰已久的问题提供更加优雅的解决方式。格兰芬多分院得到10分! - ImHuntingWabbits

0

这样做没问题,但只是改变了你的情况而不是解决了你的问题。除非你的单例没有任何实际数据与之关联,否则这种方法就无法正常工作。每当访问中央数据时,都需要正确地使其线程安全。

此外,如果没有某种iVar,我不知道如何直接在类中存储数据(即预期的方式)。

在上面的示例中,我会以这种方式编写代码,从而获得您提出的相同结果,并且仅在创建/重新创建单例时才会受到性能影响:

if (sharedInstance)
     return sharedInstance;

@synchronized(self)
{
    if (sharedInstance == nil)
            sharedInstance = [[MySingleton alloc] init];
}
return sharedInstance;

请记住,无论哪种方式,如果您正在访问在不同线程上可能发生变化的数据,那么您都必须使该代码线程安全,无论是通过非常谨慎的规划还是使用代码来确保没有问题。我建议您混合使用,但如果有疑问,请尽可能使用后者。=)

中央数据可以作为文件本地静态变量存储,如果适当的话,可以用锁定方案进行包装。根据我的经验,不同的数据可能需要不同的锁定方案才能使访问更加高效。 - ImHuntingWabbits
1
这种模式(在进入@synchronized块之前检查标志)被称为双重检查锁定,并且是有问题的。具体来说,sharedInstance指针可能在对象完全初始化之前就被设置。请参阅http://www.mikeash.com/pyblog/friday-qa-2009-10-02-care-and-feeding-of-singletons.html - BJ Homer
谢谢你指出这个问题,我以前从来没有遇到过或见过这种做法有问题。了解到在某些特定情况下它不起作用,并知道了解决办法是很好的。一年过去了,我想我更倾向于使用上面提到的dispatch_once调用来处理这个问题,正如Mike Ash所提到的。 - slycrel
如果你在谷歌上搜索各种语言的“双重检查锁定”,会有一些关于这种方案缺陷的好的白皮书。大多数会导致内存泄漏,而其他一些情况下可能会出现死锁。虽然失败很少见,但代价高昂。 - ImHuntingWabbits

0

如果您将类用作单例,则存储数据的唯一方法是使用静态文件变量和全局变量。如果您要走得这么远,使一个您不打算实例化的类,那么您最好使用标准的C函数:

void doSomething(void);

void doSomething() {
    //do something
}

啊,但是那样我就失去了我的漂亮的Objective-C语法和消息传递。 - ImHuntingWabbits

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接