posts - 5, comments - 71, trackbacks - 4, articles - 0
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

正确实现 IDisposable 接口

Posted on 2007-01-16 12:44 Ling Xu 阅读(5458) 评论(27)  编辑 收藏

正确实现 IDisposable

 

.NET中用于释放对象资源的接口是IDisposable,但是这个接口的实现还是比较有讲究的,此外还有FinalizeClose两个函数。

MSDN建议按照下面的模式实现IDisposable接口:

 1 public class Foo: IDisposable
 2 {
 3     public void Dispose()
 4     {
 5        Dispose(true);
 6        GC.SuppressFinalize(this);
 7     }
 8 
 9     protected virtual void Dispose(bool disposing)
10     {
11        if (!m_disposed)
12        {
13            if (disposing)
14            {
15               // Release managed resources
16            }
17  
18            // Release unmanaged resources
19  
20            m_disposed = true;
21        }
22     }
23  
24     ~Foo()
25     {
26        Dispose(false);
27     }
28  
29     private bool m_disposed;
30 }
31  
32 

 

.NET的对象中实际上有两个用于释放资源的函数:DisposeFinalizeFinalize的目的是用于释放非托管的资源,而Dispose是用于释放所有资源,包括托管的和非托管的。

 

在这个模式中,void Dispose(bool disposing)函数通过一个disposing参数来区别当前是否是被Dispose()调用。如果是被Dispose()调用,那么需要同时释放托管和非托管的资源。如果是被~Foo()(也就是C#Finalize())调用了,那么只需要释放非托管的资源即可。

 

这是因为,Dispose()函数是被其它代码显式调用并要求释放资源的,而Finalize是被GC调用的。在GC调用的时候Foo所引用的其它托管对象可能还不需要被销毁,并且即使要销毁,也会由GC来调用。因此在Finalize中只需要释放非托管资源即可。另外一方面,由于在Dispose()中已经释放了托管和非托管的资源,因此在对象被GC回收时再次调用Finalize是没有必要的,所以在Dispose()中调用GC.SuppressFinalize(this)避免重复调用Finalize

 

然而,即使重复调用FinalizeDispose也是不存在问题的,因为有变量m_disposed的存在,资源只会被释放一次,多余的调用会被忽略过去。

 

因此,上面的模式保证了:

 

1、 Finalize只释放非托管资源;

2、 Dispose释放托管和非托管资源;

3、 重复调用FinalizeDispose是没有问题的;

4、 FinalizeDispose共享相同的资源释放策略,因此他们之间也是没有冲突的。

 

C#中,这个模式需要显式地实现,其中C#~Foo()函数代表了Finalize()。而在C++/CLI中,这个模式是自动实现的,C++的类析构函数则是不一样的。

 

按照C++语义,析构函数在超出作用域,或者delete的时候被调用。在Managed C++(即.NET 1.1中的托管C++)中,析构函数相当于CLR中的Finalize()方法,在垃圾收集的时候由GC调用,因此,调用的时机是不明确的。在.NET 2.0C++/CLI中,析构函数的语义被修改为等价与Dispose()方法,这就隐含了两件事情:

 

1、 所有的C++/CLI中的CLR类都实现了接口IDisposable,因此在C#中可以用using关键字来访问这个类的实例。

2、 析构函数不再等价于Finalize()了。

 

对于第一点,这是一件好事,我认为在语义上Dispose()更加接近于C++析构函数。对于第二点,Microsoft进行了一次扩展,做法是引入了“!”函数,如下所示: 

1 public ref class Foo
2 {
3 public:
4        Foo();
5        ~Foo();       // destructor
6        !Foo();       // finalizer
7 };
8 

 

“!”函数(我实在不知道应该怎么称呼它)取代原来Managed C++中的Finalize()GC调用。MSDN建议,为了减少代码的重复,可以写这样的代码: 

 1 ~Foo()
 2 {
 3     //释放托管的资源
 4     this->!Foo();
 5 }
 6  
 7 !Foo()
 8 {
 9     //释放非托管的资源
10 }
11 

 

对于上面这个类,实际上C++/CLI生成对应的C#代码是这样的:

 

 1 public class Foo
 2 {
 3     private void !Foo()
 4     {
 5        // 释放非托管的资源
 6     }
 7  
 8     private void ~Foo()
 9     {
10        // 释放托管的资源
11        !Foo();
12     }
13  
14     public Foo() 
15     {
16     }
17  
18     public void Dispose()
19     {
20        Dispose(true);
21        GC.SuppressFinalize(this);
22     }
23  
24     protected virtual void Dispose(bool disposing)
25     {
26        if (disposing)
27        {
28            ~Foo();
29        }
30        else
31        {
32            try
33            {
34               !Foo();
35            }
36            finally
37            {
38               base.Finalize();
39            }
40        }
41     }
42  
43     protected void Finalize()
44     {
45        Dispose(false);
46     }
47 }
48 

 

由于~Foo()!Foo()不会被重复调用(至少MS这样认为),因此在这段代码中没有和前面m_disposed相同的变量,但是基本的结构是一样的。

 

并且,可以看到实际上并不是~Foo()!Foo()就是DisposeFinalize,而是C++/CLI编译器生成了两个DisposeFinalize函数,并在合适的时候调用它们。C++/CLI其实已经做了很多工作,但是唯一的一个问题就是依赖于用户在~Foo()中调用!Foo()

 

关于资源释放,最后一点需要提的是Close函数。在语义上它和Dispose很类似,按照MSDN的说法,提供这个函数是为了让用户感觉舒服一点,因为对于某些对象,例如文件,用户更加习惯调用Close()

 

然而,毕竟这两个函数做的是同一件事情,因此MSDN建议的代码就是: 


1 public void Close()
2 {
3     Dispose(();
4 }
5 
6 

这里直接调用不带参数的Dispose函数以获得和Dispose相同的语义。这样似乎就圆满了,但是从另外一方面说,如果同时提供了DisposeClose,会给用户带来一些困惑。没有看到代码细节的前提下,很难知道这两个函数到底有什么区别。因此在.NET的代码设计规范中说,这两个函数实际上只能让用户用一个。因此建议的模式是: 

 1 public class Foo: IDisposable
 2 {
 3     public void Close()
 4     {
 5        Dispose();
 6     }
 7  
 8     void IDisposable.Dispose()
 9     {
10        Dispose(true);
11        GC.SuppressFinalize(this);
12     }
13  
14     protected virtual void Dispose(bool disposing)
15     {
16        // 同前
17     }
18 }
19 

 

这里使用了一个所谓的接口显式实现:void IDisposable.Dispose()。这个显式实现只能通过接口来访问,但是不能通过实现类来访问。因此:

 

1 Foo foo = new Foo();
2 
3 foo.Dispose(); // 错误
4 (foo as IDisposable).Dispose(); // 正确
5 

 

这样做到了兼顾两者。对于喜欢使用Close的人,可以直接用 foo.Close(),并且他看不到 Dispose()。对于喜欢Dispose的,他可以把类型转换为 IDisposable 来调用,或者使用using语句。两者皆大欢喜!

      

Feedback

#1楼   回复  引用    

2007-01-16 13:29 by Terry[未注册用户]
好文章,这次真的是弄清楚了,不会忘了

#2楼   回复  引用  查看    

2007-01-16 13:38 by charleschen      
很好的普及文章,顶

#3楼   回复  引用  查看    

2007-01-16 15:47 by LIVE      
Good

#4楼   回复  引用  查看    

2007-01-16 16:35 by Anders Cui      
不错,蛮清楚的 :)

#5楼   回复  引用    

2007-01-16 17:01 by star[未注册用户]
好文章 ding

#6楼   回复  引用    

2007-01-16 21:35 by 零度的火[未注册用户]
好好弥补了一下基础知识,谢谢!

#7楼   回复  引用  查看    

2007-01-17 11:11 by 文炽城      
终于完全的了解了他们这两兄弟了``````感谢楼主``

#8楼   回复  引用    

2007-01-18 10:14 by maple[未注册用户]
9 protected virtual void Dispose(bool disposing)
10 {
11 if (!m_disposed)
12 {
13 if (disposing)
14 {
15 // Release managed resources
16 }
17
18 // Release unmanaged resources
19
20 m_disposed = true;
21 }
22 }

建议再加上lock
9 protected virtual void Dispose(bool disposing)
10 {
lock(this)
{
11 if (!m_disposed)
12 {
13 if (disposing)
14 {
15 // Release managed resources
16 }
17
18 // Release unmanaged resources
19
20 m_disposed = true;
21 }
}
22 }

#9楼[楼主]   回复  引用  查看    

2007-01-19 11:27 by Andrew Xu      
是否可以这样:

protected virtual void Disposing(bool disposing)
{
if (!m_disposed)
{
lock (this)
{
if (!m_disposed)
{
if (disposing)
{
// Release managed resource
}
// Release unmanaged resource
m_disposed = true;
}
}
}

#10楼   回复  引用    

2007-02-03 18:50 by hqfhy[未注册用户]
无意中看到这个,提一下自己的看法
ms的模式虽然在多数情况下比较好用,但在有些时候会带来些问题
问题出在当对象中使用了其他的非托管资源对象时,如果某些非托管资源的释放依赖于那些非托管资源对象中的非托管资源,那么,当对象离开作用域时,那些对象的Finalize会先于自身的Finalize被调用,这样该模式就会造成错误,如下代码

class foo()
{
UnmangedResourceWraper resource=new resource();
~foo()
{
this.Dispose(false);
}
}

请考虑UnmangedResourceWraper类的dispose和Finalize如何写

#11楼   回复  引用    

2007-02-04 22:38 by Andrew Xu[未注册用户]
实际上对于这种情况,首先 UnmanagedResourceWraper应当正确实现IDisposable,其次foo也应当实现IDisposable,然后在 foo.Dispose 中调用 resource.Dispose()。

这是一个附加的规则,在MS的编码规范中指出,如果一个类内有实现了IDisposable接口的成员,那么这个类本身也应当实现 IDisposable 并且需要正确调用成员的 Dispose 函数。

#12楼   回复  引用    

2007-02-05 11:08 by hqfhy[未注册用户]
问题是 你考虑一下~UnmangedResourceWraper()会在什么时候被调用呢???实际上这个问题本质上是ms的模式未能解决非托管资源释放时候
的次序问题,也就是说,你不能在最顶层制定释放次序

#13楼   回复  引用    

2007-02-05 15:51 by Andrew Xu[未注册用户]
你有没有注意到标准的 Dispose 中有

GC.SuppressFinalize(this);

这句话的意思是通知 GC 不要再去调用这个类的 Finalize,这也就避免了调用 ~UnmanagedResourceWraper(),也就是说,MS认为你调用了 Dispose 以后就已经把所有的资源都释放了,因此不再需要进一步的资源释放操作,这样也避免了资源释放次序的不确定性。

#14楼   回复  引用    

2007-02-07 16:25 by hqfhy[未注册用户]
:),做不到,你可以测试一下,实际上~UnmanagedResourceWraper()会先于~foo被调用的,:)也就是说,还没运行到GC.SuppressFinalize(this); 呵呵呵,具体请参考c#对象的finalize的调用次序

#15楼   回复  引用    

2007-02-07 16:55 by Andrew Xu[未注册用户]
可能你忽略了一个问题:

UnmanagedResourceWrapper() 实际上是一个包含有非托管资源的托管类,因此对于 foo 来说,这个类是托管资源,而不是非托管资源。

如果用户代码直接调用 foo.Dispose(),那么:

1、按照之前我说的规则,foo.Dispose() 应当调用在释放托管资源的阶段调用 UnmanagedResourceWrapper.Dispose();
2、UnmanagedResourceWrapper.Dispose()在释放非托管资源的阶段释放所包含的非托管资源,随后调用 GC.SuppressFinalize,因此 ~UnmanagedResourceWrapper() 是不会被调用到的。
3、此外,foo.Dispose() 本身也调用了 GC.SuppressFinalize,因此 ~foo() 也不会被调用到。

如果用户代码没有直接调用 foo.Dispose(),那么事情会在GC中进行:

1、GC在调用~foo之前调用它内部所有的托管资源的析构函数,这里就是指~UnmanagedResourceWrapper();
2、托管资源的析构函数负责释放它所包含的所有非托管资源;
3、这个过程是递归的,如果这个托管资源内部还有其它的托管资源,那么在调用析构函数之前GC会递归地调用它们的析构函数;

需要注意的是,实际上只有非托管资源是真正需要释放的,而托管的资源应当由GC来管理。鉴于GC工作的不确定性,托管资源的释放的顺序也是不确定的。

#16楼   回复  引用    

2007-02-08 09:59 by hqfhy[未注册用户]
好吧,我们继续来看看。
1、你说的不错,当用户主动调用dispose的时候是没有问题的
2、关于finalize调用的时间我看你是不是再看看文档,我跟你的理解有出入,也就是说c#的析构函数是在对象不可访问时就被调用了
2、对于这样的情况,比如,你有3个c++写的静态 1.dll,2.dll,3.dll,由于这3个dll会在你的程序的很多地方使用,于是你将他们各自封装,其中2和3都需要使用1。这个时候你有1wraper,2wraper,3wraper,你会怎么写这3个wraper的dispose和finalize呢,我有一个前提,就是释放2,3的资源时1必须未被释放。最后,事实上用户在使用1wraper时应该遵守什么样的模式呢

#17楼   回复  引用    

2007-02-08 11:18 by Andrew Xu[未注册用户]
对你说的2,当UnmanagedResourceWrapper是不可访问的时候,此时foo必定也是不可访问的,否则你就可以藉由 foo.resource 去访问它。按照我所说的模式,在这种情况下调用析构函数的先后顺序是没有关系的,因为析构函数只释放非托管资源。没有托管资源之间的依赖性。

然而我理解你的假设可能是非托管资源之间存在依赖性,显然,这种情况是存在的,但是作为包装器的开发者必须认识到这一点,而不能将这种依赖性引入到托管的代码中去。换句话说,要能使标准的Dispose模式正常工作,对于任何非托管的资源,需要从托管平台上看上去都是互相独立的。

我不认为这是一个很过分的要求,毕竟托管环境和非托管环境的区别太大,为了能够使用非托管的资源,有必要在这上面做一些特定的工作。以你的例子来说,如果2和3都需要使用1,那么你要么有一个新的非托管的4来处理这种依赖,要么在你的wrapper中需要建立起一种联系以保证释放的次序。

#18楼   回复  引用    

2007-02-09 13:23 by hqfhy[未注册用户]
这次,意见一致,呵呵,我本来一直讲的就是非托管资源
对于封装的非托管资源的释放问题会导致封装类在使用上必须仔细考虑。

我刚开了个blogs:还没有东西,有空来坐坐
www.cnblogs.com/hqfhy

#19楼   回复  引用  查看    

2007-03-20 22:02 by reonlyrun      
好东西,先回复,再看!

#20楼   回复  引用  查看    

2007-05-30 16:54 by 大豆男生      
正需要

#21楼   回复  引用    

2007-07-03 19:44 by 孤剑[未注册用户]
嘿嘿,学习一下。最近就是用IDispoable来释放资源,通过今天的测试,还不错。

不过还得继续理解一下,好象和楼主理解有一定的出入。

#22楼   回复  引用  查看    

2007-11-21 18:58 by Enzo      
谢谢 对Dispose Finalize和Close两个函数有了更进一步的认识

#23楼   回复  引用    

2008-04-10 13:50 by jnywxxb[未注册用户]
dddddddddddddddddddddddddd



发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 621587


相关文章:

相关链接: