Singleton with finalizer but not IDisposable
这就是我从"clr via c"、"effective c_"和其他资源了解IDisposable和Finalizer的原因:
- IDisposable用于确定地清除托管和非托管资源。
- 负责非托管资源(如文件句柄)的类应实现IDisposable,并提供终结器以确保清理这些资源,即使客户端代码没有对实例调用Dispose()。
- 只负责托管资源的类不应实现终结器。
- 如果您有一个终结器,那么您必须实现IDisposable(这允许客户端代码做正确的事情并调用Dispose(),而终结器可以防止资源丢失)。
虽然我理解以上所有的理由并同意以上所有的观点,但有一种情况下我认为打破这些规则是有意义的:一个负责非托管资源的单例类(例如提供对特定文件的单点访问)。
我认为在单例上使用dispose()方法总是错误的,因为单例实例应该在应用程序的生命周期中存在,如果任何客户端代码调用dispose(),那么您就被填满了。但是,您需要一个终结器,这样当卸载应用程序时,终结器可以清除非托管资源。
因此,在我看来,使用带有不实现IDisposable的终结器的单例类是一件合理的事情,但是这种类型的设计与我所理解的最佳实践相反。
这是一个合理的方法吗?如果没有,为什么不呢?最好的选择是什么?
我首先提到面向对象的设计模式及其结果并不总是影响每种语言的决策,即使是在面向对象的语言中。当然,你可以找到一个更容易用一种语言实现的经典设计模式(Simultalk),而不是另一种语言(C++)。
尽管如此,我不确定我是否同意这样一个前提,即一个单例实例只应在应用程序的末尾进行处理。在我为singleton(或设计模式:可重用面向对象软件的元素)阅读的设计模式描述中,没有任何内容将此作为该模式的属性。单例应该确保在任何时候只有一个类实例存在;这并不意味着只要应用程序存在,它就必须存在。
我有一种感觉,在实践中,应用程序的大部分生命周期中确实存在许多单例。但是,考虑使用TCP连接与服务器通信,但也可以以断开连接模式存在的应用程序。当连接时,您需要一个singleton来维护连接信息和连接状态。一旦断开连接,您可能希望保持同一个单例—或者您可以丢弃该单例。虽然有些人可能会认为保留单例(我甚至可能在其中),但在设计模式本身中,没有任何东西阻止您处理它——如果重新构建了连接,则单例可以再次实例化,因为此时不存在单例。
换言之,您可以创建场景,其中单例对象具有IDisposable是合乎逻辑的。
如果非托管资源仅在应用程序退出时释放,那么您甚至不需要担心终结器,因为卸载进程应该为您处理这个问题。
如果您有多个应用程序域,并且想要处理一个应用程序域卸载,这是一个可能的问题,但可能是一个您不需要关心的问题。
第二种说法是,这种设计可能不是正确的做法(并且会使修复变得更困难,因为随后您发现实际上需要两个实例)在您的入口点中创建对象(或延迟加载包装器对象),并将其通过代码传递到需要它的地方,以明确谁负责向谁提供它,然后您可以自由地更改仅使用一个对象的决定,而对其余的代码(使用它得到的内容)几乎没有影响。
只要终结器不在任何其他托管对象上调用方法(如Dispose),就可以了。请记住,终结顺序不是确定性的。也就是说,如果您的singleton对象foo包含对需要处置的对象栏的引用,则无法可靠地写入:
1 2 3 4 | ~Foo() { Bar.Dispose(); } |
垃圾收集器可能已经收集了条。
冒着踏入一堆OO-goo(即发动战争)的风险,使用单例的一个替代方法是使用静态类。
虽然它可能会让您收到代码审查gripes和fxcop警告,但是实现一个没有IDisposable的终结器并没有本质上的错误。然而,在单例上这样做并不是捕获进程或AppDomain崩溃的可靠方法。
冒着提供主观设计建议的风险:如果对象确实是无状态的,则将其设为静态类。如果它是有状态的,那么问为什么它是单例的:您正在创建一个可变的全局变量。如果您试图捕获关闭的应用程序,请在主循环退出时处理它。
如果您想用终结器创建一个singleton,那么您可能应该将对它的静态引用作为weakreference。这将需要一些额外的工作来确保访问器中的线程安全,但它将允许在无人使用时垃圾收集singleton(如果有人随后调用getInstance()方法,他们将获得一个新实例)。如果使用了静态强引用,那么即使没有其他引用,它也会保持singleton实例的活动状态。
单件适用于除此之外的任何特定情况,
我认为处理单件物品没有什么问题。与懒惰的实例化相结合,它仅仅意味着如果您暂时不需要资源,就释放它,然后根据需要重新获取它。