前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[C#] 利用using与try/finally来清理资源

[C#] 利用using与try/finally来清理资源

作者头像
科控物联
发布2022-03-29 16:36:04
6920
发布2022-03-29 16:36:04
举报
文章被收录于专栏:科控自动化

如果某个类型用到了非托管型的系统资源,那么就需要通过IDisposable接口的Dispose()方法来明确地释放。.NET环境规定,这种资源并不需要由包含该资源的类型或系统来释放,而是应该由使用此类型的代码释放。也就是说,如果你使用了带有Dispose()方法的类型,那么就应该调用它的Dispose()方法以释放其中的资源,而要想确保该方法总是能够得到调用,最好的办法就是利用using语句或try/finally代码块。

拥有非托管资源的那些类型都实现了IDisposable接口,此外,还提供了finalizer(终结器/终止化器),以防用户忘记释放该资源。使用资源的人如果没有记得及时释放,那么这些非内存型的资源就要等到将来执行finalizer的时候才能得以释放。这意味着这些对象在内存中要多待很长的时间,从而令应用程序因占用资源过多而变得缓慢。

所幸,C#语言的设计者明白释放非托管型资源是个很常见的任务,因此,他们提供了一些关键字,使得开发者更容易处理这些资源。

假如你是这么写代码的:

那么这种写法就会导致SqlConnection及SqlCommand这两个disposable(可释放的/可处置的)对象不能够正确地清理。它们会一直留在内存中,直至其finalizer得到调用。(这两个对象所属的类都继承了System.ComponentModel.Component中的那个finalizer。)

你可以在用完这两个对象之后自己去调用它们的Dispose方法,以修复此问题:

这么写在一般情况下是没有问题的,但如果SQL命令在执行过程中抛出了异常,那么Dispose()就不会得到调用。using语句能够确保Dispose()总是可以得到调用。如果在该语句中分配对象,那么C#编译器会把这样的对象包裹在try/finally结构里面:

如果函数里面只用到了一个IDisposable对象,那么要想确保它总是能够适当地得到清理,最简单的办法就是使用using语句,该语句会把这个对象放在try/finally结构里面去分配。下面这两种写法所产生的IL是相同的:

如果using语句中的变量其类型并不支持IDisposable接口,那么C#编译器就会报错。比方说:

对象的编译期类型必须支持IDisposable接口才能够用在using语句中,而不是说任何一种对象都可以放在using里面:

如果你不清楚某个对象是否实现了IDisposable接口,那么可以通过as子句来安全地处置它:

在obj实现了IDisposable的情况下,using语句会生成对应的清理代码,而在没有实现的情况下则会退化成using(null),这样的using语句不会有任何效果,但它可以令程序正常运行下去。如果你拿不准某个对象是否应该放在using里面,那么可以采用稳妥一些的写法,也就是假设该对象有可能会实现IDisposable接口,并将其包裹在刚才演示的那种using结构中。

以上内容讲的是最为简单的一种情况,也就是说,如果方法里面只有一个IDisposable对象,那就把该对象包裹在using语句里面。接下来要讲解稍微复杂一些的用法。本条最开头的那个例子涉及两个不同的IDisposable对象,一个是表示数据库连接的SqlConnection,另一个是表示数据库命令的SqlCommand。笔者当时是用两条不同的using语句来处理这两个对象的,每一条using语句都会生成对应的一层try/finally结构。这种写法的实际效果与下面这段代码相似:

每多写这样的一条using语句,就相当于多嵌套了一层try/finally结构。所幸这种情况并不是特别常见,因为很少有哪个方法需要分配两个不同类型的IDisposable对象。万一真的遇到了这种情况,那么确实可以像早前那样,分别用一条using语句来处理它们,因为那样写是能够正常运作的。不过笔者觉得那种写法看起来有些别扭,如果碰到了需要分配多个IDisposable对象的情况,那我宁可自己去编写try/finally块(而不想采用系统所生成的那种多层结构):

如果你打算采用早前那种写法,那么就请保持原样,而不要过于取巧去采用带有as的using来处理IDisposable对象:

这样写看起来似乎清晰了一些,但其中有个微妙的bug。如果SqlCommand()构造函数抛出了异常,那么SqlConnection就得不到清理了,这是因为在构造SqlCommand的时候,SqlConnection所引用的那个对象已经创建出来了,但程序还没来得及进入using块。由于此时的SqlConnection并未处在using的范围内,因此,它的Dispose不会得到调用。由此可见,凡是实现了IDisposable的对象都应该放在using或try块中去构建,否则就有可能泄漏资源。

现在讲到的这两种情况都是较为直白的。如果方法里面只有一个IDisposable对象,那么把它放在using语句里面去分配就可以了,这样做能够确保该资源无论如何都会得到释放。若有多个IDisposable对象,则可以分别用对应的using语句来分配,也可以自己编写try/finally结构,将其全都纳入同一个代码块中。

清理IDisposable对象时,还有一个小问题要考虑,那就是有些类型同时提供了Dispose方法与Close方法。例如SqlConnection就是这样的类。除了Dispose之外,你还可以通过Close方法来清理它:

这样写虽然也能断开连接,但是其效果与Dispose并不完全相同,因为后者不仅会释放资源,而且还会告诉垃圾回收器该对象不需要执行finalizer了。Dispose方法会调用GC.SuppressFinalize()以屏蔽finalizer,但Close方法通常不会调用,从而导致那些根本就不需要执行finalizer的对象依然排在等待执行的队列中。所以说,应该尽量选用Dispose(),而不是Close()。更多细节请参见本书第17条。

Dispose()方法并不会把对象从内存中移除,它只是提供了一次机会,令其能够释放非托管型的资源。这意味着,如果调用了对象的Dispose()方法之后程序里面还有一些地方要使用该对象,那么就会出现问题。比方说早前那个例子就用到了SQLConnection对象。调用该对象的Dispose()方法可以断开程序与数据库之间的连接,但是这个SQLConnection对象却依然位于内存中,只是不再与数据库相连。于是就形成了一种已经无用但仍然占据着内存的对象,如果程序中的其他地方还需引用该对象,那就不要过早地将其释放。

从某种意义上说,C#程序的资源管理起来要比C++困难,因为并没有一套确切的finalization(终结/终止化)流程供开发者释放程序中的每一份资源。但由于C#提供了垃圾回收机制,因此,涉及资源的代码写起来还是比较简单的。你所能用到的绝大部分类型都不是那种实现了IDisposable接口的类型,.NET Framework里面只有一小部分类实现了该接口。如果要使用这些资源,那么必须确保它们在各种情况下都能得以释放。最好是把这样的对象包裹在using语句或try/finally结构里面,总之,无论采用什么样的写法,你都要保证这些资源能够正确地释放。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 科控物联 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档