理解和实现IDisposable接口 - 入门教程






4.78/5 (56投票s)
从初学者的角度理解和实现IDisposable接口。
引言
在本文中,我们将讨论经常被混淆的实现IDisposable
接口的话题。我们将尝试了解何时需要实现IDisposable
接口,以及如何正确地实现它。
背景
在我们的应用程序开发中,我们经常需要获取一些资源。这些资源可能是我们要使用的集群存储驱动器上的文件、数据库连接等。在使用这些资源时,重要的是要记住在完成后释放这些资源。
C++程序员非常熟悉资源获取即初始化(RAII)
的概念。这种编程习惯表明,如果一个对象想要使用某些资源,那么它有责任获取并释放这些资源。它应该在正常情况和异常/错误情况下都这样做。这种习惯使用构造函数、析构函数和异常处理机制得到了很好的实现。
让我们不要跑题,看看我们如何使用IDisposable
模式在C#中实现类似的习惯。
使用代码
在深入了解IDisposable
接口的细节之前,让我们先看看我们通常如何使用资源,或者使用资源的最佳实践是什么。我们应该始终假设两点来获取资源。一是我们能够成功获取并使用该资源。二是在获取资源时可能出现某些错误/异常。因此,资源获取和使用应该始终发生在try块中。资源释放应该始终在finally块中。因为finally块总是会被执行。让我们看一个小代码片段,以便更好地理解。
使用Try-finally来管理资源
//1. Let us first see how we should use a resource typically
TextReader tr = null;
try
{
//lets aquire the resources here
tr = new StreamReader(@"Files\test.txt");
//do some operations using resources
string s = tr.ReadToEnd();
Console.WriteLine(s);
}
catch (Exception ex)
{
//Handle the exception here
}
finally
{
//lets release the aquired resources here
if (tr != null)
{
tr.Dispose();
}
}
使用'using'来管理资源
做同样事情的另一种方法是使用using
,这样我们只需要考虑资源获取和异常处理。资源释放将自动完成,因为资源获取被封装在using
块内。让我们做上面同样的事情,使用using
块。
//2. Let us now look at the other way we should do the same thing (Recommended version)
TextReader tr2 = null;
//lets aquire the resources here
try
{
using (tr2 = new StreamReader(@"Files\test.txt"))
{
//do some operations using resources
string s = tr2.ReadToEnd();
Console.WriteLine(s);
}
}
catch (Exception ex)
{
//Handle the exception here
}
上述两种方法本质上是相同的,唯一的区别是第一种方法需要我们显式释放资源,而在第二种方法中,资源释放是自动完成的。using
块是做这类事情的推荐方式,因为它即使程序员忘记了,也会进行清理。
之所以可以使用'using
'块,是因为上面示例中的TextReader
类实现了 IDisposable
模式。
关于Finalizers的说明
Finalizers
类似于析构函数。当对象超出作用域时,它们会被调用。我们通常不需要实现Finalizers
,但如果我们计划实现IDisposable
模式,同时又希望在本地对象超出作用域时发生资源清理,那么我们必须在类中实现Finalizer
。
class SampleClass
{
~SampleClass()
{
//This is a Finalizer
}
}
何时以及为何需要实现IDisposable
现在我们知道了资源获取和释放的理想方式。我们也知道推荐的方式是using
语句。现在是时候看看为什么我们可能需要了解更多关于实现IDisposable
模式的知识了。
假设我们正在编写一个将在整个项目中重用的类。这个类将获取一些资源。为了获取这些资源,我们的类将需要一些托管对象(如上面的示例)和一些非托管的东西(如使用COM组件或使用指针进行不安全的代码)。
既然我们的类获取了资源,那么释放这些资源的责任也落在类身上。让我们创建一个包含所有资源获取和释放逻辑的类。
class MyResources
{
//The managed resource handle
TextReader tr = null;
public MyResources(string path)
{
//Lets emulate the managed resource aquisition
Console.WriteLine("Aquiring Managed Resources");
tr = new StreamReader(path);
//Lets emulate the unmabaged resource aquisition
Console.WriteLine("Aquiring Unmanaged Resources");
}
void ReleaseManagedResources()
{
Console.WriteLine("Releasing Managed Resources");
if (tr != null)
{
tr.Dispose();
}
}
void ReleaseUnmangedResources()
{
Console.WriteLine("Releasing Unmanaged Resources");
}
}
我们已经准备好了包含资源分配和释放代码的类。我们还有使用该类的方法。现在我们想按照本文前面的指导方针来使用这个类。即。
- 在
try-finally
块中使用该对象,在try块中获取资源,并在finally块中释放它们。 - 在
using
块中使用该对象,资源释放将自动完成。 - 当对象超出作用域时,它应该被自动释放,因为它是一个局部变量。
实现IDisposable
现在,如果我们希望在使用我们的对象时通过执行清理来支持所有上述功能,我们需要实现IDisposable
模式。实现IDisposable
模式将强制我们拥有一个Dispose
函数。
其次,如果用户想使用try-finally
方法,他也可以调用这个Dispose函数,对象应该释放所有资源。
最后也是最重要的一点是,让我们有一个Finalizer
,它将在对象超出作用域时释放非托管资源。这里重要的一点是,只有当程序员没有使用'using
'块并且没有在finally块中显式调用Dispose
时,才进行此Finalize。
现在,让我们为这些函数创建存根。
class MyResources : IDisposable
{
// ...
#region IDisposable Members
public void Dispose()
{
}
#endregion
~MyResources()
{
}
}
为了理解这个类可能需要处理的可能场景,让我们看看我们类的以下可能用例。我们还将看看在每种可能的情况下应该怎么做。
- 用户不会做任何事情来释放资源。我们必须在Finalizer中负责释放资源。
- 用户将使用try-finally块。我们需要进行清理,并确保Finalizer不会再次执行。
- 用户将放置一个using '块。我们需要进行清理,并确保Finalizer不会再次执行。
所以,这是处理Dispose业务的标准方法。它也被称为Dispose模式。让我们看看如何做到这一点,以实现我们想要的一切。
public void Dispose()
{
// If this function is being called the user wants to release the
// resources. lets call the Dispose which will do this for us.
Dispose(true);
// Now since we have done the cleanup already there is nothing left
// for the Finalizer to do. So lets tell the GC not to call it later.
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing == true)
{
//someone want the deterministic release of all resources
//Let us release all the managed resources
ReleaseManagedResources();
}
else
{
// Do nothing, no one asked a dispose, the object went out of
// scope and finalized is called so lets next round of GC
// release these resources
}
// Release the unmanaged resource in any case as they will not be
// released by GC
ReleaseUnmangedResources();
}
~MyResources()
{
// The object went out of scope and finalized is called
// Lets call dispose in to release unmanaged resources
// the managed resources will anyways be released when GC
// runs the next time.
Dispose(false);
}
所以,现在我们类的完整定义看起来是这样的。这次我为方法添加了一些打印消息,以便我们可以看到当使用不同方法时事情是如何工作的。
class MyResources : IDisposable
{
//The managed resource handle
TextReader tr = null;
public MyResources(string path)
{
//Lets emulate the managed resource aquisition
Console.WriteLine("Aquiring Managed Resources");
tr = new StreamReader(path);
//Lets emulate the unmabaged resource aquisition
Console.WriteLine("Aquiring Unmanaged Resources");
}
void ReleaseManagedResources()
{
Console.WriteLine("Releasing Managed Resources");
if (tr != null)
{
tr.Dispose();
}
}
void ReleaseUnmangedResources()
{
Console.WriteLine("Releasing Unmanaged Resources");
}
public void ShowData()
{
//Emulate class usage
if (tr != null)
{
Console.WriteLine(tr.ReadToEnd() + " /some unmanaged data ");
}
}
public void Dispose()
{
Console.WriteLine("Dispose called from outside");
// If this function is being called the user wants to release the
// resources. lets call the Dispose which will do this for us.
Dispose(true);
// Now since we have done the cleanup already there is nothing left
// for the Finalizer to do. So lets tell the GC not to call it later.
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
Console.WriteLine("Actual Dispose called with a " + disposing.ToString());
if (disposing == true)
{
//someone want the deterministic release of all resources
//Let us release all the managed resources
ReleaseManagedResources();
}
else
{
// Do nothing, no one asked a dispose, the object went out of
// scope and finalized is called so lets next round of GC
// release these resources
}
// Release the unmanaged resource in any case as they will not be
// released by GC
ReleaseUnmangedResources();
}
~MyResources()
{
Console.WriteLine("Finalizer called");
// The object went out of scope and finalized is called
// Lets call dispose in to release unmanaged resources
// the managed resources will anyways be released when GC
// runs the next time.
Dispose(false);
}
}
让我们首先使用try-finally
块来使用这个类
//3. Lets call out class using try-finally block
MyResources r = null;
try
{
r = new MyResources(@"Files\test.txt");
r.ShowData();
}
finally
{
r.Dispose();
}
操作的顺序可以通过这些输出消息来理解。
Aquiring Managed Resources
Aquiring Unmanaged Resources
This is a test data. / Some unmanaged data.
Dispose called from outside
Actual Dispose called with a True
Releasing Managed Resources
Releasing Unmanaged Resources
让我们使用using
块来使用这个类
//4. The using block in place
MyResources r2 = null;
using (r2 = new MyResources(@"Files\test.txt"))
{
r2.ShowData();
}
操作的顺序可以通过这些输出消息来理解。
Aquiring Managed Resources
Aquiring Unmanaged Resources
This is a test data. / Some unmanaged data.
Dispose called from outside
Actual Dispose called with a True
Releasing Managed Resources
Releasing Unmanaged Resources
让我们使用它,并将资源释放留给GC。当Finalizer被调用时,非托管资源仍应被清理。
//5. Lets not do anything and the GC and take care of managed data
// we will let our finalizer to clean the unmanaged data
MyResources r3 = new MyResources(@"Files\test.txt");
r3.ShowData();
操作的顺序可以通过这些输出消息来理解。
Aquiring Managed Resources
Aquiring Unmanaged Resources
This is a test data. / Some unmanaged data.
Finalizer called
Actual Dispose called with a False
Releasing Unmanaged Resources
关注点
实现IDisposable
对初学者来说一直有些令人困惑。我试图写下我对何时实现此接口以及如何正确实现的理解。虽然本文是从初学者的角度编写的,但我希望这篇文章对某人有所启发和帮助。
历史
- 2012年7月2日: 初版。