编辑:从另一个问题我提供了一个答案,链接到很多关于单身人士的问题/答案:More info about singletons here:
所以我看了帖子Singletons: good design or a crutch?
争论仍然激烈 .
我认为单身人士是一种设计模式(好的和坏的) .
Singleton的问题不是模式而是用户(对不起所有人) . 每个人和他们的父亲都认为他们可以正确地实施一个(而且从我做过的许多采访中,大多数人都做不到) . 此外,因为每个人都认为他们可以实现正确的Singleton,他们滥用模式并在不合适的情况下使用它(用Singletons替换全局变量!) .
所以需要回答的主要问题是:
-
什么时候应该使用Singleton
-
如何正确实现Singleton
我对这篇文章的希望是,我们可以在一个地方收集(而不是谷歌和搜索多个网站)一个权威的来源,了解何时(以及如何)正确使用单身人士 . 同样合适的还有一份反用法和常见的不良实施清单,解释了为什么他们无法工作以及为了实现他们的弱点 .
所以让球滚动:
我会举起手来说这是我用的东西,但可能有问题 .
我喜欢"Scott Myers"在他的书中处理这个主题"Effective C++"
使用单身人士的好情况(并不多):记录框架线程回收池
/*
* C++ Singleton
* Limitation: Single Threaded Design
* See: http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
* For problems associated with locking in multi threaded applications
*
* Limitation:
* If you use this Singleton (A) within a destructor of another Singleton (B)
* This Singleton (A) must be fully constructed before the constructor of (B)
* is called.
*/
class MySingleton
{
private:
// Private Constructor
MySingleton();
// Stop the compiler generating methods of copy the object
MySingleton(MySingleton const& copy); // Not Implemented
MySingleton& operator=(MySingleton const& copy); // Not Implemented
public:
static MySingleton& getInstance()
{
// The only instance
// Guaranteed to be lazy initialized
// Guaranteed that it will be destroyed correctly
static MySingleton instance;
return instance;
}
};
好 . 让我们一起批评和其他实施 .
:-)
24 回答
大多数人在试图让自己对使用全局变量感觉良好时会使用单身人士 . 有合法的用途,但大多数时候人们使用它们,事实上只有一个实例只是一个微不足道的事实,相比之下它是全球可访问的 .
你们所有人都错了 . 阅读问题 . 回答:
在下列情况下使用单身人士
如果出现以下情况,请勿使用Singleton:
您想节省内存
你想尝试新的东西
你想炫耀你知道多少
因为其他人都在这样做(参见维基百科中的cargo cult programmer)
在用户界面小部件中
它应该是一个缓存
在字符串中
在会话中
我可以整天走
如何创建最好的单身人士:
越小越好 . 我是一个极简主义者
确保它是线程安全的
确保它永远不为空
确保仅创建一次
懒惰还是系统初始化?符合您的要求
有时OS或JVM会为您创建单例(例如,在Java中,每个类定义都是单例)
提供析构函数或以某种方式弄清楚如何处置资源
使用小内存
单身人士可以让你在一个 class 中结合两个不良特质 . 几乎在各方面都是错的 .
单身人士给你:
对对象的全局访问,和
保证永远不会创建多于一个此类型的对象
第一是很简单 . 全局通常都很糟糕 . 除非我们确实需要,否则我们不应该让对象全局可访问 .
第二个听起来似乎有道理,但让我们考虑一下 . 你最后一次**意外*创建了一个新对象而不是引用一个现有对象是什么时候?由于这是标记C,让我们使用该语言的示例 . 你经常不小心写
当你打算写
当然不是 . 我们不需要针对此错误的保护,因为这种错误不会发生 . 如果确实如此,正确的反应是回家睡12-20个小时,希望你感觉好些 .
如果只需要一个对象,只需创建一个实例 . 如果一个对象应该可以全局访问,那么将其设为全局对象 . 但这并不意味着创建它的其他实例应该是不可能的 .
"only one instance is possible"约束并没有真正保护我们免受可能的错误 . 但它确实使我们的代码很难重构和维护 . 因为我们经常发现我们确实需要不止一个实例 . 我们确实有多个数据库,我们确实有多个配置对象,我们确实需要几个 Logger . 我们的单元测试可能希望能够在每次测试时创建和重新创建这些对象,以便采用一个常见的示例 .
因此,当且仅当我们需要它们提供的两个特征时才应该使用单例:如果我们需要全局访问(这是罕见的,因为通常不鼓励全局变量) and 我们需要阻止任何人创建多个实例a1291976_ class(听起来像是一个设计问题) . 我能看到的唯一原因是如果创建两个实例会破坏我们的应用程序状态 - 可能是因为该类包含许多静态成员或类似的愚蠢 . 在这种情况下,明显的答案是修复该课程 . 它不应该依赖于唯一的实例 .
如果您需要对对象进行全局访问,请将其设置为全局,如
std::cout
. 但是不要限制可以创建的实例数量 .如果你绝对需要将类的实例数限制为一个,并且无法安全地处理创建第二个实例,那么就强制执行 . 但是也不要让它在全球范围内可访问 .
如果你确实需要两个特征,那么1)使它成为单身,2)让我知道你需要什么,因为我很难想象这样的情况 .
单身人士的问题不在于他们的实施 . 它们将两个不同的概念混为一谈,这两个概念都不是明显可取的 .
1)单身人士为对象提供全局访问机制 . 虽然在没有明确定义的初始化顺序的语言中它们可能稍微更线程安全或稍微更可靠,但这种用法仍然是全局变量的道德等价物 . 它是一个全局变量,装在一些笨拙的语法中(foo :: get_instance()而不是g_foo,比如说),但它服务于完全相同的目的(在整个程序中可访问的单个对象)并具有完全相同的缺点 .
2)单身人士阻止一个类的多个实例化 . 很少见,IME,这种功能应该融入一个类 . 这通常是一个更具背景性的事情;许多被认为是独一无二的东西真的只是恰好只有一个 . IMO更合适的解决方案是只创建一个实例 - 直到您意识到需要多个实例 .
模式有一点: don't generalize . 当它们有用时,以及当它们失败时,它们都有所有的情况 .
当你需要 test 代码时,单身人士会很讨厌 . 您通常会遇到类的一个实例,并且可以选择在构造函数中打开一个门,还是在重置状态等方法之间进行选择 .
其他问题是,辛格尔顿事实上只不过是伪装的 global variable . 当你的程序中有太多的全局共享状态时,事情往往会回归,我们都知道 .
这可能会让人更加努力 . 当一切都取决于你的单身人士时,它通常会坚持下去 . 这也妨碍了灵活性 . 调查一些 Dependency Injection 框架以尝试缓解此问题 .
单身人士基本上会让你在语言中拥有复杂的全局状态,否则很难或不可能拥有复杂的全局变量 .
Java特别使用单例作为全局变量的替代,因为所有内容都必须包含在类中 . 它与全局变量最接近的是公共静态变量,它们可以像使用
import static
一样全局使用C确实有全局变量,但是未定义调用全局类变量的构造函数的顺序 . 因此,单例允许您推迟创建全局变量,直到第一次需要该变量 .
Python和Ruby等语言很少使用单例,因为您可以在模块中使用全局变量 .
那么什么时候使用单身人物好/坏?几乎就是使用全局变量时好/坏的时候 .
Alexandrescu的Modern C Design具有线程安全,可继承的通用单例 .
对于我的2p值,我认为使用它们是绝对必要的 . )我通常不会让静态
get()
函数实例化任何东西,并将设置和销毁留给主应用程序的某个专用部分 . 这有助于突出单身人士之间的依赖关系 - 但正如上面所强调的那样,如果可能的话,最好避免使用它们 .有一个我从未见过的问题,这是我以前的工作遇到的问题 . 我们有在DLL之间共享的C单例,并且确保单个类实例的常用机制不起作用 . 问题是每个DLL都有自己的一组静态变量以及EXE . 如果你的get_instance函数是内联函数或者是静态库的一部分,那么每个DLL都将使用它自己的“singleton”副本 .
解决方案是确保单例代码仅在一个DLL或EXE中定义,或者使用创建单例管理器那些要包裹实例的属性 .
第一个例子不是线程安全的 - 如果两个线程同时调用getInstance,那个静态就是PITA . 某种形式的互斥体会有所帮助 .
正如其他人所指出的那样,单例的主要缺点包括无法扩展它们,以及失去实例化多个实例的能力,例如:用于测试目的 .
单身人士的一些有用方面:
懒惰或前期实例化
方便需要设置和/或状态的对象
但是,您不必使用单例来获得这些好处 . 您可以编写一个执行该工作的普通对象,然后让人们通过工厂(单独的对象)访问它 . 如果需要,工厂可以担心只实例化一个,重新使用它等 . 此外,如果您编程到接口而不是具体类,工厂可以使用策略,即您可以切换进出接口的各种实现 .
最后,工厂适用于依赖注入技术,如Spring等 .
当你初始化和反对时运行很多代码时,单身人士会很方便 . 例如,当您在设置持久性对象时使用iBatis时,它必须读取所有配置,解析映射,确保其全部正确等等 . 然后才能获取代码 .
如果你每次都这样做,性能会大大降低 . 在单例中使用它,您可以使用该命中一次,然后所有后续调用都不必执行此操作 .
单身人士的真正垮台是他们打破了继承权 . 除非您可以访问引用Singleton的代码,否则无法派生新类来为您提供扩展功能 . 因此,除了Singleton将使您的代码紧密耦合(可通过策略模式修复...也称为依赖注入)之外,它还将阻止您从修订(共享库)关闭代码部分 .
因此,即使 Logger 或线程池的示例也是无效的,应该由Strategies替换 .
因为单例只允许创建一个实例,所以它有效地控制了实例复制 . 例如,您不需要多个查找实例 - 例如莫尔斯查找映射,因此将其包装在单例类中是apt . 只是因为你有一个类的实例并不意味着你也受限于对该实例的引用数量 . 您可以将调用(以避免线程问题)排队到实例并进行必要的更改 . 是的,单身人士的一般形式是全球公共形式,您当然可以修改设计以创建更多访问受限制的单身人士 . 我以前没有累过这个,但我确定知道这是可能的 . 对于那些评论说单身模式完全是邪恶的人,你应该知道这一点:是的,如果你没有正确地使用它,或者在有限的功能和可预测的行为范围内它是邪恶的:不要通用 .
但是当我需要像Singleton这样的东西时,我经常最终使用Schwarz Counter来实例化它 .
我使用Singletons作为面试测试 .
当我要求开发人员命名一些设计模式时,如果他们只能命名为Singleton,那么他们就不会被雇用 .
下面是实现线程安全单例模式的更好方法,在析构函数本身中释放内存 . 但我认为析构函数应该是可选的,因为单例实例将在程序终止时自动销毁:
关于我们需要使用单例类的情况可以是 - 如果我们想要在整个程序执行期间维护实例的状态如果我们参与写入应用程序的执行日志,其中只有一个文件实例需要使用....等等 . 如果任何人可以在我的上述代码中建议优化,那将是值得注意的 .
反用法:
单独使用过多的一个主要问题是该模式阻止了替代实现的轻松扩展和交换 . 在使用单例的任何地方,类名都是硬编码的 .
我认为这是C#的 most robust version :
这是 .NET-optimised version :
您可以在dotfactory.com找到此模式 .
迈耶斯单身人士模式在大多数情况下运作良好,并且在它出现的情况下,并不一定要花更多钱寻找更好的东西 . 只要构造函数永远不会抛出并且单例之间没有依赖关系 .
单例是 globally-accessible object (从现在开始GAO)的实现,尽管并非所有GAO都是单例 .
Logger 本身不应该是单例,但记录方法理想情况下应该是全局可访问的,以便从生成日志消息的位置和记录的位置进行分离 .
延迟加载/延迟评估是一个不同的概念,单例通常也实现它 . 它带来了许多自己的问题,特别是线程安全和问题,如果它失败了,那么当时看起来好主意的事实证明并不是那么好 . (有点像字符串中的COW实现) .
考虑到这一点,GOAs可以像这样初始化:
它不需要粗暴地完成,显然在包含对象的加载库中,您可能需要一些其他机制来管理它们的生命周期 . (将它们放在加载库时获得的对象中) .
至于什么时候我使用单身人士?我将它们用于两件事 - 一个表示已用dlopen加载的库的单例表 - Logger 可以订阅的消息处理程序以及可以发送消息的消息处理程序 . 特别适用于信号处理程序 .
我仍然不明白为什么单身人士必须是全球性的 .
我打算生成一个单例,我将类中的数据库作为私有常量静态变量隐藏,并使得使用数据库的类函数不会将数据库暴露给用户 .
我不明白为什么这个功能会很糟糕 .
当我有一个封装了大量内存的类时,我觉得它们很有用 . 例如,在我最近一直在研究的游戏中,我有一个影响力 Map 类,其中包含一系列非常大的连续内存数组 . 我希望所有在启动时分配,所有在关机时释放,我绝对只需要它的一个副本 . 我也必须从很多地方访问它 . 我发现单例模式在这种情况下非常有用 .
我确信还有其他解决方案,但我发现这个解决方案非常有用且易于实现 .
如果你是创建单身人士并且使用它的人,不要把它作为单身人士(它没有意义,因为你可以控制对象的单一性而不使它成为单身人士)但是当你是一个开发者时它是有意义的库并且您只想为用户提供一个对象(在这种情况下,您是创建单例的人,但您不是用户) .
单身人士是对象所以将它们用作对象,许多人通过调用返回它的方法直接访问单身人士,但这是有害的,因为你让你的代码知道对象是单身,我更喜欢使用单身作为对象,我传递它们通过构造函数,我将它们用作普通对象,通过这种方式,您的代码不知道这些对象是否是单例,这使得依赖关系更加清晰,并且有助于重构...
在桌面应用程序中(我知道,只有我们恐龙才能再写这些了!)它们对于获得相对不变的全局应用程序设置至关重要 - 用户语言,帮助文件的路径,用户首选项等,否则必须传播到每个类和每个对话框中 .
编辑 - 当然这些应该是只读的!
另一个实现