《Effective C#》读书笔记——条目17:实现标准的销毁模式<.NET资源管理>

简介:

 如何为我们自己的包含非托管资源的类型编写资源管理代码呢?在 .NET 中为我们提供了一种标准的销毁非托管资源的模式,这个标准的模式能够使使用者通过调用IDisposable接口正常释放掉非托管资源,也能够保证使用者在忘记释放资源时使用终结器释放。这个标准模式可以和GC配合,保证仅在最糟糕的情况下才调用终结器,尽可能的降低其带来的性能影响。

 

阅读目录:

    1.实现IDisposable接口

      1.1 资源释放的标准模式

    2.提供终结器

      2.1 析构函数

    小节

    参考阅读&进一步阅读

 

1.实现IDisposable接口

  实现IDisposable接口是一种标准的做法,用来通知使用者和运行时系统该对象包含的资源需要及时释放。IDisposable.Dispose()方法仅仅定义了一个方法:

1 public interface IDisposable
2 {
3     void Dispose();
4 }

  

实现IDisposable.Dispose()方法需要完成以下目标:

  • 释放掉所有非托管资源
  • 释放掉所有托管资源,包括释放事件监听程序
  • 设定一个状态标志,表示该对象已经被销毁
  • 跳过终结操作,调用GC.SuppressFinalize(this)即可。

 

实现IDisposable接口应该完成两件事:

  • 提供一种机制,让使用者可以在垃圾收集的时候及时释放掉所有的托管资源
  • 提供一种标准做法,让使用者可以释放掉所有的非托管资源(避免终结过程带来的开销)

 

 1.1 资源释放的标准模式

  不过这里存在着问题:如何让派生类清理自己的资源,同样也能让基类进行清理呢?如果派生类覆写了终结器,或是实现了IDisposable接口,那么这些方法必须调用基类。否则,基类将不能够被正确清理。在这里我们有一种标准的做法就是:编写一个受保护的虚辅助方法,将销毁和析构共同的工作提取出来,并让派生类也可以释放其自己的资源。基类包含了核心接口的代码,而虚方法则为派生类提供了根据Dispose()或终结器的需要进行资源清理的入口:

1         //Dispose 虚方法
2         //将销毁和析构共同的工作提取出来,并让派生类也可以释放其自己的资源
3         protected virtual void Dispose(bool isDisposing)

 

该重载方法需要同时支持终结器和Dispose方法,同时因为它是个虚方法所以所有得派生类都可以讲其作为释放资源的入口点。派生类可覆写该方法,并在其中清理自身的资源,然后调用基类的版本。我们来看这一个标准模式的示例代码:

复制代码
 1     public class MyResourceHog : IDisposable
 2     {
 3         //标记为已销毁
 4         private bool alreadyDisposed = false;
 5 
 6         //实现IDisposable
 7         //调用定义的Dispose()虚方法
 8         //跳过终结器
 9         public void Dispose()
10         {
11             Dispose(true);
12             GC.SuppressFinalize(this);
13         }
14 
15         //Dispose 虚方法
16         //将销毁和析构共同的工作提取出来,并让派生类也可以释放其自己的资源
17         //isDisposing == true 时,同时清理托管资源;
18         protected virtual void Dispose(bool isDisposing)
19         {
20             //不需要处理多次
21             if (alreadyDisposed)
22                 return;
23             if (isDisposing)
24             {
25                 //省略:在这里释放托管资源
26             }
27             //省略:在这里释放非托管资源
28             //设置已处理标志
29             alreadyDisposed = true;
30         }
31 
32         public void ExampleMethod()
33         {
34             if (alreadyDisposed)
35                 throw new ObjectDisposedException("MyResourceHog", "调用了已经被释放的对象");
36             //省略
37         }
38     }
复制代码

 

派生类在执行自己分配的资源清理工作时,可以覆写基类中受保护的Dispose(bool)方法,且无论isDisposing取值如何,都要调用基类的Dispose(isDisposing)方法,以便让基类完成自身资源的释放:

复制代码
 1     public class DerivedResourceHog : MyResourceHog
 2     {
 3         private bool disposed = false;
 4 
 5         protected override void Dispose(bool isDisposing)
 6         {
 7             if (disposed)
 8                 return;
 9             if (isDisposing)
10             {
11                 //这里释放托管资源
12             }
13             //释放非托管资源
14 
15             //这里释放基类资源
16             //基类负责调用
17             // GC.SuppressFinalize(this);
18             base.Dispose(isDisposing);
19 
20             //设置已经被销毁的标志
21             disposed = true;
22         }
23     }
复制代码

 

  我们可以观察到前面的示例中基类和派生类都包含了一个标志,表示对象当前的销毁状态。这是种防御性手段,各个对象维持自身的状态可以把销毁过程中可能出现的错误限制在了一个类型中,而不会影响到组成对象的所有类型。Dispose()方法可以被调用多次,即使对象已经被销毁,终结器也有类似的规则。

  同时我们应该看到,示例程序中的两个类并没有提供终结器,这是由于这里没有使用非托管资源——因此不需要终结器(也就是说,上面的代码一直会调用Dispose(true))。除非你的类中包含非托管资源,否则不应该实现终结器,因为这个会对性能造成很大的影响(即使终结器用于也不会被调用)。不过这个标准模式确实不可改变的,因为派生类中可能会使用非托管资源,所以添加终结器,进而实现Dispose(bool),以便正确处理非托管资源。

 

关于销毁/清理方法最重要的建议:

  • Dispose()方法只能释放资源,不能再方法内执行任何别的操作
  • 终结器除了清理非托管资源之外不应该有任何别的操作

 

2.提供终结器

  如果你的类使用了非托管资源,那么你必须提供一个终结器。因为类的使用者可能会忘记调用Dispose()方法。如果没有提供终结器,而使用者又忘记调用Dispose()的话,那么就会发生资源泄露。终结器是唯一可以保证能够释放掉非托管资源的方式,没有之一

 我们通过调用Object.Finalize 方法来使用终结器,默认情况下,Finalize方法不会执行任何操作,如果我们想要让GC在回收对象内存前执行清理非托管资源的操作,我们必须先在类中重写该方法(添加析构函数)。

 

2.1 析构函数

在C#中不能够直接重写或调用Finalize(),只能通过析构函数语法来间接调用终结器。

  析构函数是C#调用终结器的操作机制,析构函数是由GC来负责调用的。程序退出时也会调用析构函数。析构函数具有下面的几个特点:

  • 只能对类使用析构函数(结构不可以)。
  • 一个类只能有一个析构函数。
  • 无法继承或重载析构函数。
  • 无法调用析构函数。 它们是被自动调用的。
  • 析构函数既没有修饰符,也没有参数。

 

我们看下面的使用析构函数的示例:

复制代码
1 public class Employee
2 {    
3     //析构函数
4     ~Employee()
5     {
6         //清理操作
7     }
8 }
复制代码

 

经过编译器编译后会生成和下面类似的代码:

复制代码
 1 public class Employee
 2 {    
 3     protected override void Finalize()
 4     {
 5         try
 6         {
 7             //清理操作...
 8         }
 9         finally
10         {
11             base.Finalize();
12         }
13     }
14 }
复制代码

 

从上面的代码我们知道:通过自动调用基类型的析构函数可以保证继承链上的对象所使用的非托管资源得到有效的释放或者我们可以直接查看IL代码:

  

  在GC运行时,它会立即清理掉那些没有提供终结器的垃圾对象,而提供了终结器的垃圾对象会停留在内存中,被添加到一个叫做“终结队列“(finalization queue)的地方。GC会使用另一个线程来执行队列中对象的终结。终结器完成工作之后,这些垃圾对象才能够从内存中清理出去。从这里我们可以看出使用终结器会在很大程度上影响程序的性能。

 

小节

   对于包含了非托管资源或者某个成员实现了IDisposable接口的类型必须为其提供一个终结器,即使需要的只是IDisposable接口,而不是终结器也需要实现完整的模式——同时提供终结器和实现IDisposable接口。否则派生类(可能包含非托管资源)就不得不在标志的Dispose模式之外自成体系,增加其复杂性,请遵守前面实现的标准Dispose模式,会节省你、你的类的使用者以及基于你的类型的派生类作者的大量时间。

 

参考&进一步阅读

清理非托管资源

析构函数(C#编程指南)

 

本文转自gyzhao博客园博客,原文链接:http://www.cnblogs.com/IPrograming/archive/2012/11/30/EffectiveCSharp_17.html ,如需转载请自行联系原作者
相关文章
|
6月前
|
SQL 开发框架 数据可视化
企业应用开发中.NET EF常用哪种模式?
企业应用开发中.NET EF常用哪种模式?
108 5
|
6月前
|
SQL 开发框架 数据可视化
企业应用开发中.NET EF常用哪种模式?
企业应用开发中.NET EF常用哪种模式?
|
3月前
|
数据库 开发者
.NET 异步编程之谜:async/await 模式究竟隐藏着怎样的神奇力量?
【8月更文挑战第28天】在当今注重效率和响应性的软件开发领域,.NET 的 async/await 模式如同得力助手,简化异步代码编写,使代码更易理解和维护。通过后台执行耗时操作,如网络请求和数据库查询,避免阻塞主线程,显著提升系统响应性。此模式不仅适用于网络请求,还广泛应用于数据库操作和文件读写。合理使用 async/await 可大幅优化性能,但需注意避免过度使用、正确处理调用链及异常,以确保系统稳定性和高效性。深入探索 async/await,助您构建更出色的应用程序。
53 0
|
1月前
|
网络协议 大数据 网络架构
桥接模式和NET模式的区别
桥接模式和NET模式的区别
40 0
|
3月前
|
敏捷开发 设计模式 开发者
【揭秘终极利器】AgileEAS.NET:服务定位器模式的魔法,如何让企业级软件开发瞬间提速?揭秘背后的技术奥秘与实战指南!
【8月更文挑战第16天】AgileEAS.NET是基于DotNet的企业级敏捷开发平台,其服务定位器模式助力构建高度解耦系统。通过全局服务目录动态查找服务,避免硬编码依赖。在AgileEAS.NET中,服务定位器以静态类形式封装服务注册与检索功能。示例展示了如何注册与获取服务实例,如在`UserController`中通过服务定位器使用`IUserService`。此模式整合到框架生命周期管理,便于各处获取服务实例,提升开发效率。然而,应适度使用并考虑依赖注入容器以增强代码可维护性和可测试性。
74 4
|
3月前
|
开发框架 监控 .NET
|
5月前
|
设计模式 存储 编译器
【.NET Core】异步编程模式
【.NET Core】异步编程模式
46 2
|
6月前
|
敏捷开发 缓存 中间件
【专栏】解读.NET 技术的高效开发模式
【4月更文挑战第29天】本文探讨了.NET技术的高效开发模式,关键要素包括面向对象编程、良好架构设计和高效代码管理。通过实例展示了在企业级应用和Web应用开发中如何运用这些原则,强调了分层架构、微服务架构以及遵循编程规范和使用版本控制的重要性。文章旨在为开发者提供.NET开发的实用指南和灵感,促进软件开发效率与质量的提升。
41 0
《More Effective C# 》读书笔记 第一章
《More Effective C# 》读书笔记 第一章
|
存储 程序员 编译器
【Effective C++详细总结】第三章 资源管理
【Effective C++详细总结】第三章 资源管理
254 0