寻找Zihuatanejo

IDisposable接口(多转整理)

    本人最近接触一个项目,在这个项目里面看到很多类实现了IDisposable接口.在我以前的项目中都很少用过这个接口,只知道它是用来手动释放资源的.这么多地方用应该有它的好处,为此自己想对它有进一步的了解,但这个过程远没有我想象中的简单.

    IDisposable接口定义:定义一种释放分配的资源的方法。

    .NET 平台在内存管理方面提供了GC(Garbage Collection),负责自动释放托管资源和内存回收的工作,但它无法对非托管资源进行释放这时我们必须自己提供方法来释放对象内分配的非托管资源,比如你在对象的实现代码中使用了一个COM对象 最简单的办法可以通过实现Finalize()来释放非托管资源,因为GC在释放对象时会检查该对象是否实现了 Finalize() 方法。 有一种更好的,那就是通过实现一个接口显式的提供给客户调用端手工释放对象的方法,而不是傻傻的等着GC来释放我们的对象.这种实现并不一定要使用了非托管资源后才用,如果你设计的类会在运行时有非常大的实例(象 GIS 中的Geometry),为了优化程序性能,你也可以通过实现该接口让客户调用端在确认不需要这些对象时手工释放它们 .

    在定义一个类时,可以使用两种机制来自动释放未托管的资源.这些机制通常放在一起实现.因为每个机制都为问题提供了略为不同的解决方法.这两种机制是:
     第一:声明一个析构函数,作为类的一个成员.在GC回收资源时会调用.
     第二:在类中实现IDisposable接口


   析构函数的问题:
      执行的不确定性:析构函数是由GC调用的,而GC的调用是不确定的.如果对象占用了比较重要的资源,应尽可以早的释放资源.

   IDisposable接口定义了一个模式,为释未托管资源提供了确定的机制,并避免产生析构函数固有的与GC相关的问题.

   在实际应用了,常常是结合两种方法来取长补短.之所以要加上析构函数,是防止客户端没有调用Dispose方法.    

    本人对IDisposable接口的理解是这样的:

    这种手动释放资源的方式肯定要比等待GC来回收要效率高啊,于是出现了下面的示例类代码:
    
    这个Foo类实现了IDisposable接口,里面有一个简单的方法:增加一个用户.


 public class Foo : IDisposable
    
{
        
/// <summary>
        
/// 实现IDisposable接口
        
/// </summary>

        public void Dispose()
        
{
            Dispose(
true);
            
//.NET Framework 类库
            
// GC..::.SuppressFinalize 方法 
            
//请求系统不要调用指定对象的终结器。
            GC.SuppressFinalize(this);
        }

        
/// <summary>
        
/// 虚方法,可供子类重写
        
/// </summary>
        
/// <param name="disposing"></param>

        protected virtual void Dispose(bool disposing)
        
{
            
if (!m_disposed)
            
{
                
if (disposing)
                
{
                    
// Release managed resources
                }


                
// Release unmanaged resources

                m_disposed 
= true;
            }

        }

        
/// <summary>
        
/// 析构函数
        
/// 当客户端没有显示调用Dispose()时由GC完成资源回收功能
        
/// </summary>

        ~Foo()
        
{
            Dispose(
false);
        }

        
/// <summary>
        
/// 增加一个用户
        
/// </summary>

        public bool  AddUser()
        
{
            
//代码省略
            return true;
        
        }

        
/// <summary>
        
/// 是否已经被释放过,默认是false
        
/// </summary>

        public  bool m_disposed;
        
//private IntPtr handle; 

    }


      客户端是这样调用的:先实例化对象,然后增加一个用户,此时销毁对象.

      

Foo _foo = null;
            _foo 
= new Foo();
            
//资源是否已经被释放
            
//第一次默认为false;
            bool isRelease3 = _foo.m_disposed;
            
//增加用户
            bool isAdded= _foo.AddUser();
            
//不再用了,释放资源
            _foo.Dispose();

   

     C#编程的一个优点是程序员不需要担心具体的内存管理,尤其是垃圾收集器会处理所有的内存清理工作。用户可以得到像C++语言那样的效率,而不需要考虑像在C++中那样内存管理工作的复杂性。虽然不必手工管理内存,但如果要编写高效的代码,就仍需理解后台发生的事情

    一面运行没有错误,可总想知道这个dispose方法到底做了些什么.既然是释放资源,那么类被释放后应该就被销毁,它的引用应该是不存在的,于是本人的测试代码如下:

   

try
            
{
                
if (_foo == null)
                
{
                    
//对象调用Dispose()后应该运行到此外
                    Response.Write("资源已经释放啦!");

                }

                
else
                
{
                    Response.Write(_foo.GetType().ToString());
                    
//资源是否已经被释放 此时为true
                    bool isRelease4 = _foo.m_disposed;
                    
bool isAdded2 = _foo.AddUser();

                }


            }

            
catch (Exception ex)
            
{
                Response.Write(
"ERR");

            }

     本想应该会运行Response.Write("资源已经释放啦!"),可是结果相反,它的引用依然存在.这让我不解,后来得到园友jyk的指点,他让我试下,.net下实现了dispose方法的类,我就用Stream试了下,测试结果好下:

    

 Stream _s = this.FileUpload1.PostedFile.InputStream;
            
//客户端文件大小 为了判断对象是否被销毁
            long orgLength = _s.Length;
            _s.Dispose();
            
try
            
{
                
if (_s == null)
                
{
                    Response.Write(
"资源已经释放啦!");

                }

                
else
                
{
                    Response.Write(_s.GetType().ToString());
                    
//客户端文件大小 此处为释放资源后
                    
//运行结果表明,此时的文件流的大小是0
                    
//说明资源已经成功释放
                    long _length= _s.Length;

                }


            }

            
catch (Exception ex)
            
{
                Response.Write(
"ERR");

            }

     

      运行结果我们可以非常清楚的看出,Stream资源已经被释放,因为两次访问Stream的大小,发现在dispose后的大小为零.这就好像是第一次初始化的结果.但Stream属于非托管资源,如果是托管资源呢?在Foo的测试代码中发现,释放前后的变量(m_disposed,调用Dispose前为false,调用后为true,而且还可以调用类的方法)发生了变化,并不是我想象当中的初始化.这是让我一直不解的地方.

     后来在资料书上看,发现IDisposable接口是专门针对未托管资源而设计的.它在托管资源上没有特别大的帮助.最终的资源回收工作还得要GC.我们看下托管资源和非托管资源在内存上的分配情况.

    /*非托管资源一般都是放在堆栈上,而托管资源都是存储在堆上.*/  非常感谢 Angel Lucifer的指教,本人见笑了 特此删除:
     

     
值类型与引用类型在内存分配上的分别:

     值类型存储在堆栈中,堆栈的工作原理就是先进后出.它在释放资源的顺序上与定义变量时分配内存的顺序相反.值变量一旦出了作用域就会从堆栈中删除对象.
 
    引用类型则存储在堆中.,当new一个类时,此时就会为对象分配内存存入托管堆中,它可以在方法退出很长的时间后仍然可以使用.我以一句常用的实例类的语句来说明下.
     
    classA a=new  classA();
 
    这句非常平常的语句其实可以分成两部分来看:

    第一:classA a;声明一个classA的引用a,在堆栈上给这个引用分配存储空间.它只是个引用,并不是真正的对象.它包含存储对象的地址.
    第二:a=new classA();分配堆上的内存,以存储真正的对象.然后修改a的值为新对象的内存地址.


    当引用出了作用域后,就会从堆栈上删除引用,但引用对象的数据仍然存储在托管堆中,一直到程序停止,或者是GC删除.

    所在这点就可以解释我上面写的Foo类在调用Dispose方法后,程序仍然可以访问对象的原因了.

    /*我认为堆是否就有像asp.net中的缓存功能,它可以将对象缓存起来,对象只要创建一次就可以在一定的有限时间内存在.*/
    非常感谢 Angel Lucifer的指教 特此更正如下:
    这种情况完全是因为GC回收操作的不可预测性导致的。GC Heap上的对象生存期完全看GC是否要回收它而决定。此外,值类型完全没必要实现 IDisposable 接口。
 
    总结:
          如果你的类中没有用非托管资源,或者是非常大的实例(象 GIS 中的Geometry), 就没有太大的必要实现这个接口. 并不是实现了这样的接口就说明你写的类有多大的不同或者会带来多大的性能优势.  

    本人对IDisposable接口就限于此,如果园友们有更多更好的理解都可以指教.

注:本人引用:http://hi.baidu.com/hygrom/blog/item/cc7f7f8da2e5f917b21bba70.html 
                 MSDN
    还是其它的一时没有记,望谅解.

正确实现 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语句。两者皆大欢喜!

通过使用Dispose模式可以适当地释放非内存资源,比如数据库连接、Win32 interop组件和操作系统的句柄。你不要指望垃圾收集器能够立即将资源释放掉,因为垃圾收集器是由于管制堆(Managed Heap)的内存紧张时才触发的。你可以快速消耗掉例如数据库连接等少量资源,但会给程序的扩展性造成副面影响。在不必要的时候不能实现Dispose模式,因为它可能会增加系统开销,而这在很多情况下是可以避免的。

在.NET当中Dispose 模式是由一个IDisposable接口来实现的,它包括一个简单的方法--Dispose: interface IDisposable
{
 void Dispose();
}

最明显的例子是在一个类里当类的实例抢占住一个非管制资源(unmanaged resource)时必须实现IDisposable,比如一个本地数据连接或是操作系统的句柄。

另外,记下一个经常被忽略的应该实现IDisposable接口的例子。当一个类实现IDisposable时,实例的正确用法是当对象不在需要时调用Dispose方法删除它,因此,在你实现一个类,而该类又包含其他实现IDisposable的类时,必须调用Dispose方法。这通常意味着在该类中你必须实现IDisposable,即使它无法直接处理非管制资源。

以下是一个实现IDisposable接口的典型模式: public class SlalomRacer: IDisposable
{
    bool _disposed = false;

    public bool IsDisposed
    {
        get { return _disposed; }
        set { _disposed = value; }
    }

    ~SlalomRacer()
    {
        InternalDispose(false);
    }

    public void Dispose()
    {
        InternalDispose(true);
    }

    protected void InternalDispose(bool disposing)
    {
        if(disposing)
        {
            GC.SuppressFinalize(this);
            _managedThing.Dispose();
        }
        _unmanagedThing.Dispose();
    }
    [...]
}

在前面的代码片断中,当IDisposable被实现时,可以通过两种方法调用disposal代码。首先,如果你直接调用Dispose方法,所有管制和非管制对象均会被列为被清除目标。可以看到终止操作会执行一个阻止对象被清除掉的优化的步骤。还注意到可以安全地多次调用Dispose方法。调用dispose方法之后,会使用一个标志来确保这个对象上的任何一个方法都不能被调用,示例代码如下: public void SeekHotTub()
{
    if(IsDisposed)
        throw new ObjectDisposedException("BT");
}

ObjectDisposedException会提醒你前面已经使用了一个disposed对象。在一个使用过disposed对象上调用其他方法时引发异常是完全有必要的--毕竟,你不能再次使用这些disposed对象。

其次,如果你不调用这个Dispose方法,终止操作会自己调用Dispose(false),它会采用一个和前段代码稍有不同的代码路径。第一,不清除那些管制对象,即使他们也实现了IDisposable接口。你无法确定对象引用是有效的--这些对象可能在等待操作的终止,或者已经被终止了。第二,也没有必要去调用GC.SuppressFinalization,因为这些对象已被终止使用了。

最后,如果你在使用C#,你应该利用其语言固有的对IDisposable接口的支持来实现对象清除,你可以使用以下声明: using(SlalomRacer mickey = new SlalomRacer())
{
    // use mickey here
    mickey.RunGates();
    mickey.GetStitches();
}
// mickey disposed automatically here

C#编辑器会适当地发出调用Dispose方法的IL代码,即使会引发异常。

posted on 2009-02-15 11:49  Zihuatanejo  阅读(527)  评论(0编辑  收藏  举报

导航