Singleton initialization
我在代码中实现了单例设计模式。
假设是:
1 2 3 4 5 6 7 8 9 10 11
| class Singleton
{
Singleton () {}
static Singleton* s;
public:
static Singleton* Get () {
if (!s)
s = new Singleton ();
return s;
}
}; |
令我困惑的是这个模式的"初始化"。在.cpp中,我把:
1
| SingletonPointer* SingletonClass::s (0); |
但我不明白为什么可以accessdefine s,因为它是private。怎么可能?
蒂亚JIR
- 为什么从void函数返回值?;)
- 哎哟!我想最好是抄下来贴上去:谢谢你指点。我马上去拿。
型
使用单例模式的最好方法是根本不使用它。
简单总结为什么单件商品不好:
百万千克1他们是有特殊限制的全球性组织。由于自身原因,全球的情况已经够糟糕了;让他们成为单身者只会加剧这种糟糕。百万千克1百万千克1如果您真的只需要对象的一个实例,那么就创建一个。如果您需要一个特殊的设备来确保您不制作多个,那么您的代码的语义就有问题。把它变成单件并不能解决问题,它只是用新问题来解决问题。百万千克1百万千克1单件衣服不适合穿线。螺纹足够硬。别让他们更难相处。百万千克1
使用单例并不能解决任何问题。它只是对现有的代码应用错误的语义,使代码的未来扩展变得困难或不可能(如果明天需要两个呢?),并添加新问题。长话短说,不要用它们。
- 这是真的。我有篇文章指出了很多避免它的原因。如果我能找到它,我以后会把它添加到我的评论中。
- @雷德克斯:我会对这样一篇文章感兴趣的,我知道有很多缺点,但最好将它们形式化,以备将来参考。)
- 我想看看反对它的理由是什么!
- @吉尔:单身汉和他们带来的邪恶已经在网上和互联网上被讨论得很恶心。做一点搜索。
- @吉尔:我正在为你编一个链接列表。
- @吉尔:这里有一个链接可以把你送到许多方向。jalf.dk/blog/2010/03/…
- 但不要被误导。单件是一种模式,就像其他模式一样。它可能被误用,但也有一些情况下,它是最合适的解决方案。
- @詹姆斯坎兹:它引入问题却没有解决任何问题。在我看来,这是一种反模式。你有一个最合适的解决方案的例子吗?
- @詹姆斯:我同意在某些情况下他们是有吸引力的。在我看来,更清楚的是伐木。您不需要用"logger"参数来扰乱API,它只是一个纯粹的调试工具,而不是函数"逻辑"的一部分。你还有其他合适的例子吗?我已经听说过"联系"和"它是独立自主的典范",但他们从未真正说服过我。
- @Matthieu:您可以定义一个全局可访问的记录器,而不必任意阻止其他实例。
- @迈克:好吧,这不像是使用单件限制太多:)你还可以有其他的伐木工人(不同级别的)等……但我知道你的评论最关注的是强制的单一性,而我的评论更多的是关于全局变量:)令人惊讶的是,这样的"模式"可以积累这么多的流量:d
- @Matthieu:事实上,我反对单子模式,而不是使用全局模式(我认为这偶尔是一个很好的解决方案)。我从来没有听说过一个有效的案例来阻止一个类的多个实例,也没有见过单例被伪装成全局变量之外的任何东西;如果有人有一个例子,我会感兴趣的。
- @詹姆斯:请详细说明:在什么情况下,单例是设计良好的系统中最合适的解决方案?
- 任何时候,你所做的建模都最好表现为一个独特的对象。它们通常是管理器类型的对象:logmanager、configurationmanager等,它们确保公共资源的公共接口,由所有组件和子系统共享。(例如,如果有人修改了日志配置,您不需要跟踪100个实例来更新它们。)
- 另一个合理的例子是:临时文件。您希望确保在退出时删除它们(除非为了调试目的传递了不删除它们的特殊选项)。在这里,单例似乎也是最合理的解决方案。
- @詹姆斯:我不同意。在这个例子中,我的断言"如果你只需要一个,那么就创建一个"适用。logger对象通常归某个应用程序类型的对象所有,您只创建其中一个。你不需要去找它。就这么做吧。
- @詹姆斯:回复:临时文件。如果您有多个临时文件,那么根据定义,这不能是单例文件。除非您有某种类型的AllTempFilesDeleter类,它维护要删除的临时文件列表。但是,如果您这样做,为什么不让一堆文件智能指针(raii对象)超出范围并删除所拥有的文件呢?
- @Mike A Singleton不会比任何其他模式引入更多的问题。滥用一个模式,或者在不合适的地方使用它,它会带来问题。在适当的地方正确地使用它,它就能解决问题。
- @John Dibling如果您使用的是临时文件,则需要某种类型的临时文件管理器;您需要创建一个单独的目录来将其放入(出于安全原因,如果没有其他原因),并且在退出时需要清理该目录以及其中的任何文件。
- @詹姆斯:当然,但你不需要一个单身汉。
- @JohnDibling开发了长时间运行的程序,必须只有一个日志管理器实例,而且每个人都可以很容易地找到它。您的所有建议都是将其移动到一个单一的应用程序对象中(我从未发现有必要这样做)。
- @詹姆斯:Application对象不是单体的。它是一个普通对象,只创建过一次。
- @显然,约翰迪布林还有其他的解决办法。效率更低,更容易出错,但它们确实存在。为什么不为工作使用正确的工具?
- @约翰·迪布林·埃多克斯1〔1〕那么,当你需要它的时候,你怎么找到它呢?它在某个地方注册?在单人间?为什么所有的额外工作,加上错误的风险,当有一个简单,有效和众所周知的解决方案。
- @詹姆斯:我可能是错的,但你似乎把单件反模式等同于全球可访问对象的使用。如果您认为全局对象有它们(偶尔)的用途,那么我同意。如果你也有一个例子,其中一个单例(也就是说,奇怪地将一个全局可访问对象与一个防止其他同类型对象的机制结合起来的东西)是合适的,那么我会非常感兴趣地听到它;我从未遇到过这样的情况。
- @詹姆斯:仅仅说一些不太有效的解决办法并不能使之成为现实。我已经提供了很多单例不好的原因,并链接到更多更深入的解释。在这一点上,你没有反驳任何反单例积分,或提供任何支持单例积分除了只是说"单例是好的"。
- @詹姆斯:另一篇关于单打为什么不好的文章,比我的好:stackoverflow.com/questions/1020312/…
- 单身是坏的。如果你想要一个全球性的,就做一个全球性的。别让它成为单身汉。单例没有什么"正确"的地方,它比其他方法更不有效,更容易出错(尝试实现一个线程安全的单例)。最后,您将只剩下一个看起来是线程安全的单例,同时由于过度锁定而导致性能损失。
- @詹姆斯:很容易说"在适当的地方使用单例",但到目前为止,您只提供了一个全局适用的例子。我们的主张是,一个全球性的可能有时是适当的,但一个单一的不是。你能想出一个例子,一个单一的选择比一个全球性的更好吗?
- 只是为了反驳您的"只有一个日志管理器实例是必要的",不,不是这样的。您的所有代码都必须能够到达全局日志管理器,但是在代码中创建另一个地方不会有任何危害。它甚至可能是有益的(考虑单元测试)。每个测试可能希望创建自己的新日志管理器,而不是重用可能被以前的测试弄脏的日志管理器)。因此,在这种情况下,全局可能是合理的,但单例代码只会使代码更难测试。它不是一个合适的工具。
- 这不是争议,也不是可谈判的。这是要求的一部分。所有日志记录都必须遵守当前的配置,并且规范非常明确:当前只有一个日志记录配置。而且,由于初始化顺序的问题,您不能将全局变量用作日志管理器。
- @JAIF全局不能用作日志管理器。无法控制全局的初始化顺序。一旦您添加了代码以确保在第一次使用前进行初始化,并确保永远不会有多个代码,并且为了确保可以从任何地方访问它,您已经实现了一个singleton而不调用它。
- 回复:临时文件管理器;您确实需要一个单独的目录,因为每个实例都会创建自己的新目录,并且程序规格说明您只需要为临时文件使用一个目录。(更一般地说,如果程序要求说您永远不能拥有一个以上的x,那么单件是实现这一点的一种方便方法。程序需求确实是这样说的。)
- 关于约翰引用的那篇文章:第一句话用不同的语言几乎完全表达了我所说的话。95%显然不是一个准确的数字,他提到的两个问题也不是独立的,所以他使用的最终数字可能会有些偏离。但仍然有0.25%的课程应该是单门课程。在一个小项目中大约2或3个。听起来不错。(我列举了两三个例子。它们是有效的,但我认为没有更多的了。)
- @约翰·迪布林·埃多克斯1〔1〕在哪里?你在这里给出的唯一原因是你不喜欢他们,并且模糊地宣称他们容易出错(这不是我的经验)。如果我没有反驳你的任何观点,那是因为我还没有看到任何需要反驳的技术论据。
- @詹姆斯:我认为,对于为什么一个人突然想要一个以上的日志管理器,JALF给出了很好的理由,而你说的很少,但"不,从不"。在看到许多全局变量(不管是单件的还是非单件的)后,突然需要不止一个实例(令不得不清理全局混乱的开发人员感到沮丧),我觉得这不是很有说服力。此外,全局变量的初始化顺序失败也是不使用全局变量的一个很好的理由。另一个原因是它们隐藏了依赖关系。
- 因此,让我们暂时考虑一下这样一个想法:在所有情况中,一个独生子可能有0.1%的理由。尽管如此,这意味着在绝大多数目前使用它们的情况下,它们是不合理的。它们以这种方式所造成的伤害比它们所能带来的任何好处都要大得多,因此,即使假设确实有0.1%的案例,传播其他方法来处理这些案例也比处理单子传播给我们带来的混乱要容易得多。
- 我可以想到一个全局不能替换单例的情况:一个线程本地单例(它不是单例的,因为每个线程有一个实例)。当然,几乎所有现代编译器都有某种线程本地存储修饰符(这很快就会被标准化),但只有当您正在编写一个DLL,并且您的函数是从加载DLL之前创建的线程中调用时,这才有效。在这种情况下,线程局部全局变量是不可能的。
- @SBI这个问题只是项目需求之一。如果需求涉及日志记录,并且日志只有一个动态配置(在服务器软件中很常见),那么任何导致重复的、可能存在不同配置副本的内容都会违反需求。开发人员忽略需求可能更方便,这是不可接受的论点。
- @詹姆斯:我知道你有很多经验,也许这确实与我的经验相矛盾,或者我们只是对相同的数据进行了不同的解读,但在这个行业工作了近15年之后,我不相信任何人会说"我们只需要其中一个",不管他们是一个知道自己在做什么的程序员,还是一个传达回报的经理。酬劳。我经常看到这样的要求在一些经理的一时冲动下下降。IME只有一个高薪的客户把你认为在某个项目中固定的东西扔到船外,包括PM发誓永远不会改变的所有东西。
- 还有@jalf关于测试的很好的论据。使用globals和singleton的代码很难测试,对于测试,您可能希望在每次测试运行中都有不同的记录器实例。是的,如果你准备跳过一些束缚,这一切都可以用单件完成,但是为什么首先限制自己,只是为了在以后打破这些限制,而不限制自己会容易得多?
- @SBI有一些案例。其中两个经常出现的是日志配置(通常必须是全局的)和放置临时文件的目录。我在命令行解析的实现中也使用了单例——如果有一件事我百分之百肯定,那就是我永远不会有多个命令行:—)。(我完全同意这种情况很少见,而且您通常应该提供多个实例,即使规范规定现在只需要一个实例。)
- @詹姆斯:如果你再创建一个日志记录小发明,你的电脑会爆炸吗?如果没有,你就不需要单身。如果您只需要一个日志,并且可以很容易地访问它,那么只需创建一个日志并使其成为全局日志。
- @如果我创建第二个日志配置,代码将不符合要求。
- @詹姆斯:那就不要再创造第二个了。
- @但是我仍然需要单例逻辑来处理第一个构建顺序问题。当然,信任是很好的,但是验证是更好的---如果有不止一个中断了程序,并且有一个简单有效的方法来确保不会发生这种情况,我会愚蠢地不使用它。
- @詹姆斯:两个问题都解决了。如果你为App编写代码,class Logger {}; class App { public: Logger log_; } the_app; /*...*/ the_app.log_.DoIt();信任就不再是问题了。
- @john这并不能解决初始化顺序的问题(或者更确切地说,它延迟初始化App,并确保只有一个App实例)。它引入了一个不必要的复杂性,以及一个免费的附加类。
- @ James Kanze:您可以通过全局函数访问初始化顺序问题,其中它可以是静态变量(关于线程安全性的警告(将被固定在C++ 0x中,并且已经在大多数编译器中固定)和销毁顺序),或者在问题中创建(如果需要的话,可以解决线程安全问题)。Y)。对于防止多个实例来说,这是一个完全独立的问题,除了"因为某些疯子把它放在规范中"之外,您还没有提供一个为什么想要这样做的示例。
- @詹姆斯:好吧,我放弃了。我觉得你现在很顽固。
- @约翰认为我有一个行之有效的解决方案,而且在过去也一直在努力,而且没有人能够给出任何真正的理由来解释为什么它是坏的,或者提出更好的建议,是的,我会很顽固的。
- @杰姆斯。这也是C用户给出了他们为什么喜欢C++而不喜欢C++的理由。和C语言相比,程序员更喜欢使用汇编语言。对不起,我一点也不相信。
- 约翰提到了防止某个类没有第二个实例的最明显的方法:只创建一个实例。这是简单而优雅的,工作,允许精确地定义对象被实例化的时刻(因此没有初始化顺序问题),并使测试代码比使用单例容易得多。如果一个应用程序中只有两个单例,那么控制它们就不难了。
- 通过为一个类型引入一组附加规则,singleton增加了复杂性。这种复杂性必须得到保证,因此使用单例应该只出于好的理由。在我的书中,"这是我一直做的事情"还不够好。
- @SBI"类型的附加规则"出现在需求规范中;您不能忽略它们。约翰建议使用代码审查来强制它们,而不是让编译器为您执行(或者根本不强制它们——我不确定是哪一种)。
- @詹姆斯:你最大的问题是需求规范写得不好。其目的是定义生产系统的行为,而不是规定如何设计生产系统,也不是对如何测试生产系统进行任意限制。需求可以并且确实会发生变化,并且任何由系统的结构设计强制执行的需求都将是非常昂贵的。至于编译器的强制执行,没有人会意外地创建一个新对象,而不是使用提供的对象,试图自动防止故意的反常行为是一场失败的战斗。
- @詹姆斯:你已经养成了一个老习惯,那就是忽略大多数论点,只反驳很少的论点。你那样做的时候我从不喜欢。我把你对迈克的回答标记为冒犯。我不是土生土长的人,所以我可能是错的,但我认为"你不了解现实"是一种侮辱。
- @SBI与其说它是一种侮辱,不如说它说明了需求规范写得不好,而且它更接近事实。需求规范是由实际必须管理系统的人员编写的,一旦我交付了它,他们就有很好的理由不想处理多个不同的日志配置。假装你可以强迫客户接受任何你认为方便的东西,不管是什么原因,都是不现实的。
- @詹姆斯:看来,主持人是我对你对迈克的答复的看法,而不是你的。这甚至没有考虑到你在这里打倒了一个流浪汉。(也就是说,没有人说你应该强迫你的客户做任何事情。)
- @詹姆斯:我想是时候结束讨论了。单人间什么时候合适?当你得到一个比你使用的规范任意要求的规范时。什么时候不合适?当您想单独测试组件时。当未来迭代可能需要多个实例的可能性极小时。当您希望通过明确组件之间的所有依赖关系来简化维护时,避免不可见的耦合。当你想要一个结构化的、灵活的设计,它不会妨碍你适应未来的需求。
- @詹姆斯:在"假装你可以强迫客户接受任何事情…":这是一种双向关系,谈判总是有可能的。一个合理的论点可能会让他们相信,如果没有奇怪的设计限制,结果可能会更好;即使协商失败,这也只是为该项目使用一个糟糕的设计模式的原因,而不是认可它用于一般用途。
- @迈克什么时候是单身合适的:什么时候是解决问题的最好办法。什么时候不合适:什么时候解决不了问题。这很简单。单例测试不会在测试中产生任何特定的问题——我在包含单例的项目中做了大量的单元测试。对单例的依赖是明确的,单例不妨碍适应未来的需求。这通常不是最好的解决方案,但有时的确如此。
- @迈克,我想你误解了我所说的需求规范。这些要求不是"奇怪的设计限制"。该需求旨在使系统管理更容易。这是一个有效的要求,如果角色颠倒了,我会自己插入。我不赞成单件或其他任何一种模式,用于一般用途。只要模式合适。
- @James:"对singleton的依赖是明确的"-在使用点,而不是在使用它的对象的初始化点。
- @james:"singleton不妨碍适应未来的需求"——当您需要引入多个实例时,您需要遍历访问singleton的所有代码——可能是系统中的每个函数。对于结构化设计,您只需更改对象初始化代码以提供对正确实例的访问。
- @詹姆斯:"这通常不是最好的解决方案,但有时的确如此"——你给出的唯一例子是希望防止人们为了故意打破一个要求而跳过篮球圈。使用传统的结构化设计,创建一个实例,并提供对所有需要它的对象的引用,可以很容易地获得"所有生产代码使用相同的日志记录设施"的要求。要忽略这个实例并创建自己的实例,既需要忽略需求,又需要缺乏基本的常识;在设计级别执行的任何数量都不能帮助防止这种情况发生。
- @James:"单例测试不会在测试中产生任何特定的问题"-如何防止测试写入生产日志?根据环境的不同,您需要某种方法来配置您的单例。这是在运行时完成的吗?如果是这样,如何防止任意代码在生产中更改它?是否通过从环境中读取配置来完成?这带来了相当多的额外复杂性,以及另一个一切都依赖的功能。使用结构化设计,只需在生产中创建一个生产实例,在测试中创建一个测试实例。
- 迈克:SuntLon是一种防御措施(部分在C++中,它也解决了初始化问题的顺序)。这并不重要,就像让成员变量私有化而不是公共化一样,这并不重要。如果您不使用它,那么出错的风险相当小,但是由于您无论如何都需要99%的代码来确保初始化的顺序,为什么不保护您自己呢?(我有点同意"缺乏常识"的评论。大多数单例语言都有语义,这样程序员就不会自己创建一个了。)
- @Mike关于测试,至少关于日志,您只需提供一个不同的配置文件,singleton读取该文件。配置的处理方式类似。不需要单独的实例。
- @詹姆斯:"我认为你误解了我所说的需求规范"-当你说"需求规范中存在一种类型的附加规则"时,我可能误解了你,这意味着需求明确要求使用一个单例。如果是这样的话,那么需求就写得很糟糕;您将需要使用单例,但这并不能使它成为一个好的选择。如果这不是一个明确的需求,那么就有更多的传统设计可以在不将单个实例耦合到整个系统的情况下实现需求。
- @詹姆斯:静态初始化失败的最佳解决方案是完全避免静态对象,并在main启动后显式创建所有内容。我同意其他的解决方案是"99%的代码"对于一个单例(唯一剩下的坏处是使构造函数私有化);这仍然不能证明将一个实例耦合到所有东西的后果是为了防止任何人都不会犯的错误。
- @james:将变量设为私有可以防止意外破坏类不变量,并使系统处于可能很难调试的无效状态。一些类不变量可能很微妙,因此最好不要期望类的用户理解其实现细节的所有潜在缺陷,而是公开一个安全使用的接口。单例防止类的用户故意破坏一个高级需求(独立于类本身),代价是防止测试代码合法地创建多个对象。
- @James:"您只需提供一个不同的配置文件"——所以您还拥有一个与系统中其他所有内容耦合的配置文件阅读器。对于结构化设计,测试代码也不需要这样做;它可以简单地创建一个虚拟的日志工具,或者一个为测试环境配置的工具,而不依赖于被测试的类以外的任何东西。
型
静态字段在声明之外必须有定义。声明通常放在.h文件中的类声明中,而定义几乎总是放在.cpp文件中。静态变量的定义是必须的,因为它们必须初始化为某个对象。
但是,即使定义在类主体之外,甚至在完全不同的文件中,也并不意味着它不是类的一部分。SingletonClass::使它成为类定义的一部分(而不是类声明),因此它可以"访问"私有字段。
对于在类体外部定义的方法也一样,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| // A.h
class A
{
private:
int b;
public:
A(int x) : b(x)
{}
Do();
}
// A.cpp
A::Do()
{
return b;
} |
号
- 现在很清楚了。谢谢你的解释。我没有想到在定义类方法时我们总是这样做。
型
在初始化代码中,您没有访问Singleton::s,而是在定义它。
- 要点:这是关于定义,而不是访问。但是,如果它是私有的,那么如何在类外部定义它呢?
型
私有变量可以被类的所有方法访问。访问S变量的唯一位置是属于同一类的get()方法。
如果您想从外部访问s,则不能直接访问,但必须调用get()方法(它是公共的),而该方法实际上会为您返回s。
用法:
1
| Singleton * s = SingletonClass::Get(); |
型
它可以从外部通过Get访问(如果您提供了适当的类型)。事实上,它是private并不妨碍该方法返回指向它的指针。一个成员是private只是阻止通过名字访问它。