找回密码
 立即注册
首页 业界区 业界 从零开始:C#回收魔法—深入浅出揭开Dispose与释放模式的 ...

从零开始:C#回收魔法—深入浅出揭开Dispose与释放模式的神秘面纱

茅断卉 2025-10-6 12:49:58
一、什么是Dispose?

我们先来看一个简单例子(Net 8)。定义一个实现了Dispose方法的简单对象Defer。然后在控制台中我们执行以下代码。
  1. // 定义Defer类型
  2. ref struct Defer(Action action) { public void Dispose() => action?.Invoke();}
  3. // Main入口
  4. static void Main(string[] args)
  5. {
  6.     using var df = new Defer(() => Console.WriteLine("Run"));
  7.     Console.WriteLine("Hello, World!");
  8. }
  9. // 控制台输出:
  10. // Hello, World!
  11. // Run
复制代码
可以看到,hello,world和Run的输出顺序反过来了。
这个Defer结构体可以近似模拟Golang中Defer关键词起到的延迟执行功能。using本身是一个语法糖,帮助我们更好把握Dispose()方法的调用时机。
对于ref struct,上述代码等效于:
  1. {
  2.   Defer df = new Defer(() => Console.WriteLine("Run"));
  3.   try
  4.   {
  5.       Console.WriteLine("Hello, World!");
  6.   }
  7.   finally
  8.   {
  9.       df.Dispose();
  10.   }
  11. }
复制代码
在这里,try内部将要保住的代码为df对象生命周期以内的代码。
对于异步DisposeAsync(), using等效于:
  1. {
  2.     ResourceType resource = «expression»;
  3.     try
  4.     {
  5.         «statement»;
  6.     }
  7.     finally
  8.     {
  9.         IAsyncDisposable d = (IAsyncDisposable)resource;
  10.         if (d != null)
  11.         {
  12.             await d.DisposeAsync();
  13.         }
  14.     }
  15. }
复制代码
二、为什么要设计Dispose?

C#采用垃圾回收机制来自动管理内存,这使得程序员不需要手动管理内存分配和释放,大大减少了内存泄漏和野指针等问题。然而,垃圾回收器只负责托管内存的回收,对于非托管资源,它无法自动管理。而且,垃圾回收器的运行时间是不确定的,它可能在资源已经不再需要很久之后才运行。因此,需要一种机制来主动释放非托管资源,这就是Dispose出现的原因之一。
在C#开发中,我们经常使用各种资源,比如文件、数据库连接等。这些资源用完后需要及时释放,否则会占用系统资源,影响程序性能。Dispose方法就是用来释放这些资源的。当我们不再需要某个对象时,需要主动/被动的调用Dispose方法,就能把资源归还给系统,避免资源泄露。
简单来说,Dispose就是约定号的一个“用完就收拾”的方法。可以方便的配合using关键词来使用。我们可以再看看几个例子。
案例1 通过using在指定代码完成后触发Dispose:
  1. // Main入口
  2.     using (Defer df1 = new(() => Console.WriteLine("Run")))
  3.     Console.WriteLine("Hello, World!1");  // 或 通过 { ... } 包住代码
  4.    
  5.     Console.WriteLine("Hello, World!2");
  6. // 控制台输出:
  7. // Hello, World!1
  8. // Run
  9. // Hello, World!2
复制代码
案例2 通过using多重触发,最终按变量定义的顺序反着执行(出栈顺序):
  1. // Main入口
  2. using Defer df1 = new(() => Console.WriteLine("Run1")),
  3.             df2 = new(() => Console.WriteLine("Run2")),
  4.             df3 = new(() => Console.WriteLine("Run3"));
  5. Console.WriteLine("Hello, World!");
  6. // 控制台输出:
  7. // Hello, World!
  8. // Run3
  9. // Run2
  10. // Run1
复制代码
案例3 异步IAsyncDisposable,调用await using:
  1. public class A_Async:IAsyncDisposable {async ValueTask IAsyncDisposable.DisposeAsync() => await Task.CompletedTask;}
  2. static async void Main(string[] args)
  3. {
  4.    await using A_Async a = new();
  5. }
复制代码
三、为什么要用释放模式(Dispose Pattern)?

在C#实现接口时,Visual Studio的提示中经常会弹出通过释放模式实现接口,那么什么是释放模式?
释放模式是Dispose模式和析构函数(finalizer)的结合使用,目的是为了确保资源能够被正确释放,无论是通过显式的调用Dispose方法,还是在对象被垃圾回收器(GC)回收时触发析构函数。这种模式被称为“Dispose模式”,它是一种资源管理的最佳实践,用于处理托管资源和非托管资源。
1.png

举个例子,我们有一个对象,里面有一些非托管资源,也有一些托管资源。示例代码如下:
  1. class SampleObject:IDisposable
  2. {
  3.     private ManagedObject _mo;  //托管
  4.     private UnmanagedObject _umo; //非托管
  5.     public void Dispose()   //资源释放
  6.     {
  7.       _mo.Dispose(); //释放托管
  8.       _umo.Dispose(); //释放非托管
  9.     }
  10. }
复制代码
3.1 防止重复调用Dispose()

正常情况下我们的代码问题不大。但假设ManagedObject和UnmanagedObject不是我们写的,所以要考虑重复Dispose可能会出现问题。为此,我们需要在SampleObject内部加上一个标志位来避免重复释放,此时代码变成了:
  1. class SampleObject:IDisposable
  2. {
  3.   private ManagedObject _mo;
  4.   private UnmanagedObject _umo;
  5.   private bool disposedValue = false; // 新增: flag变量
  6.   public void Dispose()
  7.   {
  8.     if (!disposedValue) // 新增: 判断flag值,避免重复调用
  9.     {
  10.       _mo.Dispose();
  11.       _umo.Dispose();
  12.       disposedValue = true;
  13.     }
  14.   }
  15. }
复制代码
3.2 避免遗漏调用Dispose()

对于含非托管资源的对象,如果忘了调用Dispose(),轻点就是内存泄漏,严重的话可能是灾难。为了确保我们的对象能够调用Dispose(),我们考虑增加析构函数。期望在程序被GC回收的时候自动释放资源,示例代码如下:
  1. class SampleObject:IDisposable
  2. {
  3.   private ManagedObject _mo;
  4.   private UnmanagedObject _umo;
  5.   private bool disposedValue = false;
  6.   public void Dispose()
  7.   {
  8.    
  9.     DisposeFinal(); // 执行资源释放
  10.     // 新增: 如果手动调用了Dispose(),告诉终结器不要再执行析构函数
  11.     // 即不要重复调用DisposeFinal()方法
  12.     GC.SuppressFinalize(this);
  13.   }
  14.   public void DisposeFinal()  //重命名,从Dispose方法中分离出来
  15.   {
  16.     if (!disposedValue)
  17.     {      
  18.       _mo.Dispose();
  19.       _umo.Dispose();
  20.       disposedValue = true;
  21.     }
  22.   }
  23.   // 新增: 析构函数,在忘记调用Dispose()时由终结器执行Dispose()
  24.   ~SampleObject()
  25.   {
  26.       DisposeFinal();
  27.   }
  28. }
复制代码
3.3 托管资源的提前回收

如果3.2中的对象忘了调用Dispose(),此时触发了析构函数,仍然可以执行Dispose()。
尽管看着好像一切都完美了。但这里还是有潜在的重复调用Dispose()隐患。因为终结器的执行顺序是不固定的,当SampleObject对象被终结器触发析构函数时,其他对象(比如_mo)可能也触发了析构函数。造纸在SampleObject执行Dispose时,有可能_mo的Dispose()方法被执行了2次(自身一次,外部调用一次),从而造成意外后果。
我们可以看一个例子。
3.3.1 定义一个有缺陷的托管资源类

这个类未对重复释放进行拦截。
  1. // 我们定义一个有缺陷的托管资源的类
  2. class ManagedData:IDisposable
  3. {
  4.     // 模拟托管资源,大数组尽量让GC多保留一会,增加测试结果多样性
  5.     private MemoryStream data= new MemoryStream(new byte[100_000000]);  
  6.     private bool _finalized = false;
  7.     int id;
  8.     public ManagedData(int id)  //记录当前对象id
  9.     {
  10.         this.id = id;
  11.     }
  12.     ~ManagedData()
  13.     {
  14.         _finalized = true;  // 由析构函数释放
  15.         Console.WriteLine($"{id}:ManagedData 已终结.");
  16.     }
  17.     public void Dispose()
  18.     {
  19.         if (_finalized)
  20.             throw new ObjectDisposedException($"{id}:无法访问已终结的ManagedData.");
  21.         data.Dispose();
  22.         Console.WriteLine($"{id}:ManagedData 正常释放.");
  23.         _finalized = true;  // 由dispose释放
  24.     }
  25. }
复制代码
3.3.2 定义一个继承IDisposable接口的类

再定义一个实现IDisposabled接口的SampleObject来使用。在这里我们用标准的释放模式(Dispose Pattern)来写,但故意把托管资源放到disposing判定的外面来来执行。
  1. class SampleObject:IDisposable
  2. {
  3.   private ManagedData _mo;
  4.   int id;
  5.   public SampleObject(int id) //记录当前对象id
  6.   {
  7.       this.id = id;
  8.       _mo = new ManagedData(id);
  9.   }
  10.   private bool disposedValue;
  11.   // 标准的释放模式写法
  12.   protected virtual void Dispose(bool disposing)
  13.   {
  14.       if (!disposedValue) //如果已执行dispose,则以下代码跳过
  15.       {
  16.           // 判定来源
  17.           // 如果是手动Dispose()调用的,disposing为true释放托管资源
  18.           // 如果是被动由终结器在析构函数调用的,disposing为false此时不应该释放托管资源
  19.           if (disposing)  
  20.           {
  21.               // 本来应该写托管资源的地方
  22.           }           
  23.           try
  24.           {
  25.               _mo.Dispose();  // 为了测试,这里将托管资源的释放和操作放外面
  26.           }
  27.           catch (Exception ex)
  28.           {
  29.               Console.WriteLine($"{id}:异常: {ex.GetType().Name} - {ex.Message}");
  30.           }
  31.           disposedValue = true;
  32.       }
  33.   }
  34.   ~SampleObject()
  35.   {
  36.       Dispose(disposing: false);
  37.   }
  38.   public void Dispose()
  39.   {
  40.       Dispose(disposing: true);
  41.       GC.SuppressFinalize(this);
  42.   }
  43. }
复制代码
3.3.3 我们创建一些对象进行测试

尝试在一个循环中创建这个对象,然后调用GC,等待GC释放
[code]for (int i = 0; i

相关推荐

您需要登录后才可以回帖 登录 | 立即注册