学c++时就知道一个对象用完之后要在析构函数里面Delete掉,然而在c++中强大的析构函数在c#里面已经风采不再,它已经功能退化。取而代之的是c#里面更先进的GC。
下面我将讲一下对于托管资源和非托管资源的垃圾回收。
.net应用程序是在一个托管的环境里运行的,GC为你控制着托管内存。对于托管资源的垃圾回收我们完全可以交给GC去处理,我们要做的就是去掉它所有的引用。垃圾回收器对内存管理表现的非常出色,并且它以非常高效的方法移除不再使用的对象。但是尽管如此,当我们创建大量的对象时还是会让GC超负荷工作,所以我们应当尽量避免创建大量的对象。为了程序的效率,你可以使用一些简单的技巧来减少垃圾回收器的工作。所有的引用类型,即使是局部变量,都是在堆上分配的。所有引用类型的局部变量在函数退出后马上成为垃圾,一个最常见的“垃圾”做法就是申请一个Windows的画图句柄:
protected override void OnPaint( PaintEventArgs e )
{
// Bad. Created the same font every paint event.
using ( Font MyFont = new Font( "Arial", 10.0f ))
{
e.Graphics.DrawString( DateTime.Now.ToString(),
MyFont, Brushes.Black, new PointF( 0,0 ));
}
base.OnPaint( e );
}
OnPaint()函数的调用很频繁的,每次调用它的时候,都会生成另一个Font对象,而实际上它是完全一样的内容。垃圾回收器每次都须要清理这些对象。这将是难以置信的低效。
取而代之的是,把Font对象从局部变量提供为对象成员,在每次绘制窗口时重用同样的对象:
private readonly Font _myFont =
new Font( "Arial", 10.0f );
protected override void OnPaint( PaintEventArgs e )
{
e.Graphics.DrawString( DateTime.Now.ToString( ),
_myFont, Brushes.Black, new PointF( 0,0 ));
base.OnPaint( e );
}
这样你的程序在每次paint事件发生时不会产生垃圾,垃圾回收器的工作减少了,你的程序运行会稍微快一点点。
然而对于非托管资源(如:数据库连接,文件句柄,网络连接,互斥体,COM对象,套接字,位图,GDI+)我们可不要把它交给GC去自动处理。非托管资源占用了系统资源比较大。如果交给GC不知道它会在什么时候被回收,这样会使得这些对象在内存中呆的时间更长,从而会使你的应用程序会因为系统资源的占用太多而速度下降。所以我们要即时的手动释放不再使用的非托管资源。非托管资源都实现了IDisposable接口,这样我们可以通过调用Dispose()方法来释放资源。最好的方法来保证Dispose()被调用的结构是使用using语句或者使用try/finally块。其实当你把对象分配到using语句内时,C#的编译器就把这些对象放到一个try/finally块内,所以下面的两段代码编译成的IL是一样的:
SqlConnection myConnection = null;
// Example Using clause:
using ( myConnection = new SqlConnection( connString ))
{
myConnection.Open();
}
// example Try / Catch block:
try {
myConnection = new SqlConnection( connString );
myConnection.Open();
}
finally {
myConnection.Dispose( );
}
而当我们要处理两个对象时怎么办呢?首先使用using来做:
using ( SqlConnection myConnection = newSqlConnection( connString ))
{
using ( SqlCommand mySqlCommand = newSqlCommand( commandString,myConnection ))
{
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
}
当编译时,会被分配到try/finally块中,等效于如下代码:
SqlConnection myConnection = null;
SqlCommand mySqlCommand = null;
try
{
myConnection = new SqlConnection( connString );
try
{
mySqlCommand = new SqlCommand( commandString,
myConnection );
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
finally
{
if ( mySqlCommand != null )
mySqlCommand.Dispose( );
}
}
finally
{
if ( myConnection != null )
myConnection.Dispose( );
}
这样会很糟糕,因为生成了嵌套的try/finally块,这样会降低效率。所以,如果是遇到多个实现了IDisposable接口的对象时,我更愿意写自己的try/finally块:
SqlConnection myConnection = null;
SqlCommand mySqlCommand = null;
try {
myConnection = new SqlConnection( connString );
mySqlCommand = new SqlCommand( commandString,
myConnection );
myConnection.Open();
mySqlCommand.ExecuteNonQuery();
}
finally
{
if ( mySqlCommand != null )
mySqlCommand.Dispose();
if ( myConnection != null )
myConnection.Dispose();
}
综上所述:无论何时在一个方法内处理一个对象时,使用using语句是最好的方法来确保申请的资源在各种情况下都得到释放。当你在一个方法里分配了多个(实现了IDisposable接口的)对象时,创建多个using块或者使用你自己的try/finally块。
接下来我们将讨论如何让自己写的类优雅的而高效的释放内存。
当我们写的类中出现局部的非托管资源时我们可以使用using或者try/finally来进行显示的回收。但是当出现全局的非托管资源时我们又该怎么办呢?
我们可以通过实现IDisposable接口来达到释放资源的目的,但是你不能指望你的用户总是记得去调用Dispose方法,一旦他们忘记调用时你会损失一些资源。唯一可以确保非内存资源可以恰当释放的方法就是创建一个析构函数。当垃圾回收器运行时,它会直接从内存中移除不用析构的垃圾对象。而其它有析构函数的对象还保留在内存中。这些对象被添加到一个析构队列中,垃圾回收器会起动一个线程专门来析构这些对象。当析构线程完成它的工作后,这些垃圾对象就可以从内存中移除了。就是说,须要析构的对象比不须要析构的对象在内存中待的时间要长。但你没得选择。如果你是采用的这种被动模式,当你的类型占用非托管资源时,你就必须写一个析构函数。下面我将给出MSDN中给的一个经典模式:
using System;
using System.ComponentModel;
public class DisposeExample
{
public class MyResource: IDisposable
{
// 非托管资源
private IntPtr handle;
// 托管资源
private Component component = new Component();
private bool disposed = false;
public MyResource(IntPtr handle)
{
this.handle = handle;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); //告诉GC不要再调用它的析构函数(防止重复调用)
}
private void Dispose(bool disposing)
{
// 检查是否已经被调用
if(!this.disposed)
{
if(disposing)
{
component.Dispose();//处理托管资源
}
//调用适当的方法来清理非托管资源
CloseHandle(handle);
handle = IntPtr.Zero;
}
disposed = true;
}
// Use interop to call the method necessary
// to clean up the unmanaged resource.
[System.Runtime.InteropServices.DllImport("Kernel32")]
private extern static Boolean CloseHandle(IntPtr handle);
//当用户忘记调用Dispose方法时,析构函数将给最后一次机会帮他完成清理非托管资源的任务
~MyResource()
{
Dispose(false);
}
}
public static void Main()
{
// Insert code here to create
// and use the MyResource object.
}
}
如果派生类有另外的清理任务,就让它实现Dispose方法:
public class DerivedResourceHog : MyResourceHog
{
// Have its own disposed flag.
private bool _disposed = false;
protected override void Dispose( bool isDisposing )
{
// Don't dispose more than once.
if ( _disposed )
return;
if ( isDisposing )
{
// TODO: free managed resources here.
}
// TODO: free unmanaged resources here.
// Let the base class free its resources.
// Base class is responsible for calling
// GC.SuppressFinalize( )
base.Dispose( isDisposing );
// Set derived class disposed flag:
_disposed = true;
}
}
(引入用了一个非常重要的忠告:对于任何与处理和资源清理相关的方法,你必须只释放资源! 不要在处理过程中添加其它任何的任务。)
在托管环境里,你不用为每一个创建的类写析构函数;只有须要释放一些使用的非托管资源时才添加,或者你的类所包含的成员有实现了IDisposable接口的时候也要添加。
今日c#性能优化明星:Dispose
优化指数:99