我来自C++背景,已经使用C#约一年。像许多人一样,我感到困惑的是为什么确定性资源管理不内置于语言中。
using
结构提供了"确定性"资源管理,并内置于C#语言中。请注意,所谓"确定性"是指在
using
块后执行代码之前保证已调用
Dispose
。请注意,这并不是"确定性"的实际含义,但每个人似乎都滥用它,在这种情况下非常糟糕。
在我这个偏向C++的大脑中,使用具有确定性析构函数的引用计数智能指针似乎比需要实现IDisposable并调用dispose清理非内存资源的垃圾收集器更加重要。
垃圾收集器不需要你实现
IDisposable
。事实上,GC完全无视它。
承认,我并不是很聪明...所以我纯粹是想更好地理解为什么事情会变成这样。
跟踪垃圾收集是一种快速可靠的方法,可以模拟无限内存机器,使程序员免于手动内存管理的负担。这消除了几类错误(悬空指针、过早释放、双重释放、忘记释放)。
如果修改C#,使得:
对象进行引用计数。当对象的引用计数降至零时,在对象上确定地调用资源清理方法,
考虑一个在两个线程之间共享的对象。线程竞争将引用计数减少到零。其中一个线程会赢得比赛,而另一个线程将负责清理。这是不确定的。认为引用计数本质上是确定性的是一种谬论。
另一个常见的谬论是,引用计数在程序中最早可能的点上释放对象。它并不是。减量总是被推迟,通常推迟到作用域结束。这使得对象的生存时间比必要的时间更长,留下所谓的“浮动垃圾”。请注意,特别是一些跟踪垃圾回收器可以并且确实比基于作用域的引用计数实现更早地回收对象。
然后标记对象进行垃圾收集。垃圾收集在未来的某个非确定性时间发生,此时回收内存。在这种情况下,您不必实现IDisposable或记住调用Dispose。
您不必为垃圾回收对象实现IDisposable
,因此这并没有什么好处。
如果有非内存资源需要释放,只需实现资源清理函数即可。
为什么这是个坏主意?
天真的引用计数非常缓慢且泄漏循环。例如,Boost在C++中的shared_ptr
比OCaml的跟踪GC慢了多达10倍。即使是天真的基于作用域的引用计数,在多线程程序存在的情况下也是不确定的(几乎所有现代程序都是如此)。
那样做会否背离垃圾回收器的初衷?
完全不会。实际上,这是一个在1960年代发明的坏主意,并在接下来的54年中受到了激烈的学术研究,得出结论:引用计数在一般情况下都很糟糕。
实现这样的东西可行吗?
绝对可以。早期的.NET和JVM原型使用了引用计数。他们也发现它很糟糕,并放弃了它,转而采用跟踪GC。
编辑:从迄今为止的评论来看,这是一个糟糕的想法,因为
垃圾回收不需要引用计数更快
是的。请注意,您可以通过延迟计数器的递增和递减使引用计数速度更快,但这会牺牲您非常渴望的确定性,并且仍然比具有当前堆大小的跟踪GC慢。但是,引用计数在渐近意义下更快,因此在将来当堆变得非常大时,也许我们会开始在生产自动内存管理解决方案中使用RC。
处理对象图中的循环的问题
试探删除是一种专门设计用于检测和收集引用计数系统中循环的算法。然而,它很慢且不确定。
我认为第一点是有效的,但是使用弱引用很容易处理第二点。
把弱引用称为“容易”是希望战胜现实的胜利。它们是一场噩梦。它们不仅不可预测且难以架构,而且还会污染API。
那么速度优化是否超过了您面临的缺点:
可能无法及时释放非内存资源
using
是否能够及时释放非内存资源?
可能会过早释放非内存资源
如果您的资源清理机制是确定性的并且内置于语言中,您可以消除这些可能性。
使用构造是确定性的并内置于语言中。
我认为您真正想问的问题是为什么IDisposable不使用引用计数。我的回答是轶事:我已经使用垃圾收集语言18年了,从来没有需要使用引用计数。因此,我更喜欢简单的API,不会被弱引用等偶然复杂性污染。