百科问答小站 logo
百科问答小站 font logo



如何评价王垠的 《讨厌的 C# IDisposable 接口》? 第1页

  

user avatar   jeffz 网友的相关建议: 
      

有一半道理。

其实IDisposable接口现在已经和非托管结构没必然联系了,其实就是一个释放资源的意思,例如需要unsubscribe一些事件等等。打个比方IObservable<T>.Subscribe()事件就返回一个IDisposable接口。你说它可以返回另一个接口吗?当然可以,甚至就一个Action也行,但因为有IDisposable于是就直接用了。

真要说起来,我认为其实IDisposable说到底唯一的作用就是配合using,否则方法叫任何名字都行。但是话说回来,叫任何名字也都解决不了所谓的“传染”问题,因为它真心需要释放资源啊。不管你是把资源用Unload还是Close还是Unsubscribe方法来释放,终究还是会传染到另一个“释放资源”的方法上的,即便方法名不叫Dispose。

所以,我现在写的代码,假如不是要配合using使用的话,我会选择不实现IDisposable而是自己定义一个方法,例如叫做CleanUp,或是Unsubscribe,或是Close,甚至还是Dispose但不实现接口。这么做的好处就是find usage的时候不会把所有对IDisposable或using的地方都找出来,工具可以更容易理解代码。

最后,IDisposable和Dispose方法其实包含语义上的约定,也就是说调用后对象就销毁了,不能继续使用了。假如你叫做Close方法,可能就可以设计成允许重新Open。但假如是Dispose掉,那么再Open时就可以抛出ObjectDisposedException,告诉你不许再用了。


user avatar   feng-dong 网友的相关建议: 
      

原则上来说,任何非托管资源,都可以被封装成托管资源。所以非要对「非托管资源」暴露一个接口,只能是因为这个资源太稀少,不能等待 GC 的启动时机。

所以这里就涉及几个问题:

  1. GC 优化的好不好。GC 应该是尽量频繁的短时间运行。典型的「分代 generational」GC 就是为了这个设计的。所以没道理 GC 不能为稀缺资源进行优化。
  2. 系统本身设计有没有缺陷。像 HWND 用光这个问题,简直就是羞耻。系统 handle pool 预定过小不说,a button is a window 本來就是非常傻的设计。你要用这个来反驳改变现状困难还行,反驳王垠嘲笑你那就正撞枪口了。
  3. 即使对稀缺资源,disposable 和 GC-manage 应该是共同作用。Disposable 属于给程序员一个手动优化的选项,而不是不掉用就出错的东西。

其实我也奉劝各种用 GC 的平台,要是对付必须及时释放的资源,你们能不能就 fallback to reference counting ?自动 ref-counting 语言不支持,就算暴漏一个手动 counting 接口也是给程序员一条活路啊。

你们觉得 root-tracing GC 是香饽饽,也不能一遇到稀缺资源就退回到吃手动管理这泡屎吧?


user avatar   ling-jian-94 网友的相关建议: 
      

因为文章中途修改过,这个回答主要是针对原文中质疑“

ManualResetEvent

,

Semaphore

,

ReaderWriterLockSlim

”这些对象为什么要实现IDisposable的部分。这些对象内部都使用了Windows句柄,指向Windows的Event对象,因此占用了内核资源,从原理上看它跟不再使用但没有关闭的文件、socket、僵尸进程的性质差不多,如果有太多句柄没有被回收,也会造成服务甚至系统内核崩溃。IDisposable不应该在没有非托管资源的类上实现,这个说的很对,但是前面举的几个例子,偏偏都是有托管资源的情况,在这些情况下,GC是帮不上忙的。这是整个答案的总提纲。

=================================================================

惊讶,就算是写Java的程序员,难道会把文件打开着等着gc去关闭吗……

说IDisposable的设计有缺陷这个可以理解,因为的确一层一层嵌套去做析构挺麻烦的(虽然C++程序员会表示少见多怪)。如果不能理解他举得几个例子为什么需要IDisposable,那就有点奇怪了。

即使只做Linux开发,也会明白及时关闭文件和socket的重要性:文件号数量是有限的,还经常严格受到ulimit的限制,而gc通常的实现是在一个低优先级的线程中进行的,这意味着在系统压力大的时候,gc是跟不上的,而系统压力大的时候也同时是文件号吃紧的时候,如果把这个任务扔给gc,分分钟就是重要服务崩溃甚至系统崩溃的节奏。

Windows下也是一样,轮子哥说不知道为什么Windows不解决这个问题,我觉得这压根就是个无解的问题。句柄指向的是Windows的内核对象,包括File,包括Socket,包括Event、Mutex——是的,如果使用线程同步的对象基本全部都有内核部分的实现,不仅是Windows,Linux也一样。句柄通过引用计数保存了一个对象在内核中,这个对象可能有多个句柄指向它,甚至可能被多个进程共享(想一下GetHWnd),内核一旦内存溢出,那可就出大事了。所以任何时候对于使用了系统句柄的对象,C#都会希望它能被及时释放掉。涉及密码学的则是另一件事,密码学中使用的所有临时对象都必须被立即清零,这在C++当中也是一样的,以至于有时候要专门定义不允许被编译器优化的SafeZeroMemory之类的接口。

另外一些吧,我觉得完全得让Java背锅——Java怎么躺着也中枪了呢?

因为C#的接口设计是模仿Java的,而Java的多态相当不灵活,比如说接口继承这个问题。我们规定所有使用using语法的对象都要实现IDisposable,然后我们又希望所有的Stream都可以使用using,那么我们必须让Stream接口继承IDisposable。这有的时候就坑了,比如说MemoryStream也是Stream,结果MemoryStream也必须实现IDisposable了。

总体上来看,我觉得这篇文章当中说的问题,属于没有理解到使用句柄对象的危险性——如果某个设计在一个到处被引用的临时对象里保存了一个打开的文件,估计会被人狠狠敲脑袋;但是许多人却认识不到在一个到处被引用的对象里保存一个Event对象或者ReadWriteLock之类的对象也是同样危险的。但是后者却是多线程需要同步时最简洁的设计,这也是一种悲哀吧……

针对这种有很多对象需要同步的需求,我觉得也许可以考虑采用一种“锁池”的设计:

设计一个叫做LockService的类,用于提供全局锁服务,可以设计成Singleton的,也可以按不同namespace提供多个实例。里面提前创建很多很多个同步对象,放进一个空闲表里。同时有一个HashMap保存当前使用的锁。当请求锁服务的时候,需要提供一个字符串(或者其他对象类型,需要可以hash)的key,所有key相同的请求会互锁;LockService类收到这个请求之后,首先判断这个key是否在HashMap中,如果在,则取出HashMap中保存的锁进行Lock操作,同时将这个key的引用计数加一;否则从空闲表中取出一个空闲的锁放进HashMap中。Unlock的时候,将引用计数减一,如果引用计数减到了0,则从HashMap中归还这个锁。如果空闲表中锁的数量不足则扩大锁的规模,空闲表中空闲数量过多则释放多余的资源并缩小规模。HashMap自己可以用一个ReadWriteLock来保护。

这样设计的好处是只有实际正在实用的锁才会占用资源,而大部分可能被锁但暂时没有被使用的锁只是逻辑上存在这个“锁”而不占用句柄,总的句柄数可以被控制,也绕开了IDisposable的问题,在这个基础上再包装一个返回一个IDisposable对象的接口用来在using中使用,再包装一个LockObject类封装一下,就可以得到一个没有IDisposable接口的锁对象了。LockObject加锁的时候使用自己作为key,就可以不用想怎么设计一个不重复的key的问题了。

嗯,不过看上去蛮蠢的。

=================================================================

今天又看了一遍修改完的最新版本,比起最早的版本来说还是清晰很多了,不过对于后面文件和Event对象的讨论还是有一些不能认同的地方。

作者说,File只所以要关闭,是因为跟其他程序的访问是互斥的,这个有一定道理,但显然不是全部的理由,如果我这个文件只有我自己用呢,是不是就可以不关闭?如果是个临时文件呢?如果不是文件,而是socket呢?事实上我们都知道,不管是Linux还是Windows,文件号数量都是有限的,如果一直打开不关闭,很快就会报too many files然后崩溃了。之所以要及时回收,是因为我们知道这是一种有限的资源,如果不回收可能就会崩溃。

那么为什么内存可以让GC来回收呢?这跟GC的机制是有关的,我们知道GC一般需要进行全对象的标记扫描,这是非常昂贵的操作,不能在每次修改引用的时候立即进行,所以GC会延迟到之后进行,其中的一个条件就是:分配内存时,发现内存不足。这是因为托管程序的内存分配完全是由.NET Framework(或者JVM)进行的,所以托管框架对于内存使用量心知肚明,可以在合适的时候进行GC。对文件和句柄就不是这样了,托管框架并不知道什么时候打开文件或者句柄已经接近上限,也没法在这个时候触发GC——这些对象占用的资源和内存并不在托管框架范围内。所以,尽情占用内存等待GC是安全的,而尽情占用句柄、文件号等待GC是不安全的。

Event再小,那也是个句柄,作者大概还没有太多Windows下的开发经验,不知道“句柄泄露”这件事情多么可怕,而且观察程序占用内存这件事情本身就是不太正确的,泄露的句柄占用的是内核内存。另外,许多时候交给GC来回收句柄并不会表现出有大风险,这一般都是因为压力没上去,不管怎么说,把身家性命交在一个有随机性的、不知道会不会触发的程序手里,还是说不过去的。

===================================================================

话说,第一次看到的版本里面说后面会出一个“终极解决方案”的,怎么吃了……




  

相关话题

  如何评价《王垠:C 编译器优化过程中的 Bug》? 
  SqlConnection的close必须吗? 
  为什么现在招聘程序员大多要求 Java / C / C++ 技能,而 C# / .NET 不受青睐? 
  如何对 Expression 进行计算? 
  .net开发都有哪些容易入手,轻量级的框架? 
  王垠的《谈谈Parser》是在回应 winter 吗? 
  C#(csharp)这门语言的优势在哪? 
  C#调用C++DLL函数,一般怎么封装这个DLL? 
  C# 中如何在不使用 async和await 关键字的情况下构建一个按照顺序执行的 Task 集合? 
  C#里的析构方法什么时候才会调用? 

前一个讨论
中国现行的教育制度是不是浪费和埋没了很多人才?
下一个讨论
为什么不能断点编译,或者说几乎没见过断点编译?





© 2024-06-28 - tinynew.org. All Rights Reserved.
© 2024-06-28 - tinynew.org. 保留所有权利