zoukankan      html  css  js  c++  java
  • 深入理解C#中的IDisposable接口

    写在前面

    在开始之前,我们需要明确什么是C#(或者说.NET)中的资源,打码的时候我们经常说释放资源,那么到底什么是资源,简单来讲,C#中的每一种类型都是一种资源,而资源又分为托管资源和非托管资源,那这又是什么?!

    托管资源:由CLR管理分配和释放的资源,也就是我们直接new出来的对象;

    非托管资源:不受CLR控制的资源,也就是不属于.NET本身的功能,往往是通过调用跨平台程序集(如C++)或者操作系统提供的一些接口,比如Windows内核对象、文件操作、数据库连接、socket、Win32API、网络等。

    我们下文讨论的,主要也就是非托管资源的释放,而托管资源.NET的垃圾回收已经帮我们完成了。其实非托管资源有部分.NET的垃圾回收也帮我们实现了,那么如果要让.NET垃圾回收帮我们释放非托管资源,该如何去实现。

    如何正确的显式释放资源

    假设我们要使用FileStream,我们通常的做法是将其using起来,或者是更老式的try…catch…finally…这种做法,因为它的实现调用了非托管资源,所以我们必须用完之后要去显式释放它,如果不去释放它,那么可能就会造成内存泄漏。

    这听上去貌似很简单,但我们编码的时候可能很多时候会忽略掉释放资源这个问题,.NET的垃圾回收又如何帮我们释放非托管资源,接下来我们一探究竟吧,一个标准的释放非托管资源的类应该去实现IDisposable接口:

    复制代码
    public class MyClass:IDisposable
    {
        /// <summary>执行与释放或重置非托管资源关联的应用程序定义的任务。</summary>
        public void Dispose()
        {
        }
    }
    复制代码

    我们实例化的时候就可以将这个类using起来:

    using(var mc = new MyClass())
    {
    }

    看上去很简单嘛,但是,要是就这么简单的话,也没有这篇文章的必要了。如果要实现IDisposable接口,我们其实应该这样做:

    1. 实现Dispose方法;

    2. 提取一个受保护的Dispose虚方法,在该方法中实现具体的释放资源的逻辑;

    3. 添加析构函数;

    4. 添加一个私有的bool类型的字段,作为释放资源的标记

    接下来,我们来实现这样的一个Dispose模式:

    复制代码
    public class MyClass : IDisposable
    {
        /// <summary>
        /// 模拟一个非托管资源
        /// </summary>
        private IntPtr NativeResource { get; set; } = Marshal.AllocHGlobal(100);
        /// <summary>
        /// 模拟一个托管资源
        /// </summary>
        public Random ManagedResource { get; set; } = new Random();
        /// <summary>
        /// 释放标记
        /// </summary>
        private bool disposed;
        /// <summary>
        /// 为了防止忘记显式的调用Dispose方法
        /// </summary>
        ~MyClass()
        {
            //必须为false
            Dispose(false);
        }
        /// <summary>执行与释放或重置非托管资源关联的应用程序定义的任务。</summary>
        public void Dispose()
        {
            //必须为true
            Dispose(true);
            //通知垃圾回收器不再调用终结器
            GC.SuppressFinalize(this);
        }
        /// <summary>
        /// 非必需的,只是为了更符合其他语言的规范,如C++、java
        /// </summary>
        public void Close()
        {
            Dispose();
        }
        /// <summary>
        /// 非密封类可重写的Dispose方法,方便子类继承时可重写
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposed)
            {
                return;
            }
            //清理托管资源
            if (disposing)
            {
                if (ManagedResource != null)
                {
                    ManagedResource = null;
                }
            }
            //清理非托管资源
            if (NativeResource != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(NativeResource);
                NativeResource = IntPtr.Zero;
            }
            //告诉自己已经被释放
            disposed = true;
        }
    }
    复制代码

    如果不是虚方法,那么就很有可能让开发者在子类继承的时候忽略掉父类的清理工作,所以,基于继承体系的原因,我们要提供这样的一个虚方法。

    其次,提供的这个虚方法是一个带bool参数的,带这个参数的目的,是为了释放资源时区分对待托管资源和非托管资源,而实现自IDisposable的Dispose方法调用时,传入的是true,而终结器调用的时候,传入的是false,当传入true时代表要同时处理托管资源和非托管资源;而传入false则只需要处理非托管资源即可。

    那为什么要区别对待托管资源和非托管资源?在这个问题之前,其实我们应该先弄明白:托管资源需要手动清理吗?不妨将C#的类型分为两类:一类实现了IDisposable,另一类则没有。前者我们定义为非普通类型,后者为普通类型。非普通类型包含了非托管资源,实现了IDisposable,但又包含有自身是托管资源,所以不普通,对于我们刚才的问题,答案就是:普通类型不需要手动清理,而非普通类型需要手动清理。

    而我们的Dispose模式设计思路在于:如果显式调用Dispose,那么类型就该按部就班的将自己的资源全部释放,如果忘记了调用Dispose,那就假定自己的所有资源(哪怕是非普通类型)都交给GC了,所以不需要手动清理,所以这就理解为什么实现自IDisposable的Dispose中调用虚方法是传true,终结器中传false了。

    同时我们还注意到了,虚方法首先判断了disposed字段,这个字段用于判断对象的释放状态,这意味着多次调用Dispose时,如果对象已经被清理过了,那么清理工作就不用再继续。

    但Dispose并不代表把对象置为了null,且已经被回收彻底不存在了。但事实上,对象的引用还可能存在的,只是不再是正常的状态了,所以我们明白有时候我们调用数据库上下文有时候为什么会报“数据库连接已被释放”之类的异常了。

    所以,disposed字段的存在,用来表示对象是否被释放过。

    如果对象包含非托管类型的字段或属性的类型应该是可释放的

    这句话读起来可能有点绕啊,也就是说,如果对象的某些字段或属性是IDisposable的子类,比如FileStream,那么这个类也应该实现IDisposable。

    之前我们说过C#的类型分为普通类型和非普通类型,非普通类型包含普通的自身和非托管资源。那么,如果类的某个字段或属性的类型是非普通类型,那么这个类型也应该是非普通类型,应该也要实现IDisposable接口。

    举个栗子,如果一个类型,组合了FileStream,那么它应该实现IDisposable接口,代码如下:

    复制代码
    public class MyClass2 : IDisposable
    {
        ~MyClass2()
        {
            Dispose(false);
        }
        public FileStream FileStream { get; set; }
        /// <summary>
        /// 释放标记
        /// </summary>
        private bool disposed;
        /// <summary>执行与释放或重置非托管资源关联的应用程序定义的任务。</summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        /// <summary>
        /// 非密封类可重写的Dispose方法,方便子类继承时可重写
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposed)
            {
                return;
            }
            //清理托管资源
            if (disposing)
            {
                //todo
            }
            //清理非托管资源
            if (FileStream != null)
            {
                FileStream.Dispose();
                FileStream = null;
            }
            //告诉自己已经被释放
            disposed = true;
        }
    }
    复制代码

    因为类型包含了FileStream类型的字段,所以它包含了非普通类型,我们仍旧需要为这个类型实现IDisposable接口。

    及时释放资源

    可能很多人会问啊,GC已经帮我们隐式的释放了资源,为什么还要主动地释放资源,我们先来看一个例子:

    复制代码
    private void button6_Click(object sender, EventArgs e)
    {
        var fs = new FileStream(@"C:1.txt",FileMode.OpenOrCreate,FileAccess.ReadWrite);
    }
    private void button7_Click(object sender, EventArgs e)
    {
        GC.Collect();
    }
    复制代码

    上面的代码在WinForm程序中,单击按钮6,打开一个文件流,单击按钮7执行GC回收所有“代”(下文将指出代的概念)的垃圾,如果连续单击两次按钮6,将会抛异常:

    懒得勤快的博客_全栈开发者_互联网分享精神

    如果单击按钮6再单击按钮7,然后再单击按钮6则不会出现这个问题。

    我们来分析一下:在单击按钮6的时候打开一个文件,方法已经执行完毕,fs已经没有被任何地方引用了,所以被标记为了垃圾,那么什么时候被回收呢,或者GC什么时候开始工作?微软官方的解释是,当满足以下条件之一时,GC才会工作:

    1. 系统具有较低的物理内存;

    2. 由托管堆上已分配的对象使用的内存超出了可接受的范围;

    3. 手动调用GC.Collect方法,但几乎所有的情况下,我们都不必调用,因为垃圾回收器会自动调用它,但在上面的例子中,为了体验一下不及时回收垃圾带来的危害,所以手动调用了GC.Collect,大家也可以仔细体会一下运行这个方法带来的不同。

    GC还有个“代”的概念,一共分3代:0代、1代、2代。而这三代,相当于是三个队列容器,第0代包含的是一些短期生存的对象,上面的例子fs就是个短期对象,当方法执行完后,fs就被丢到了GC的第0代,但不进行垃圾回收,只有当第0代满了的时候,系统认为此时满足了低内存的条件,才会触发垃圾回收事件。所以我们永远不知道fs什么时候被回收掉,在回收之前,它实际上已经没有用处了,但始终占着系统资源不放(占着茅坑不拉屎),这对系统来说是种极大的浪费,而且这种浪费还会干扰整个系统的运行,比如我们的例子,由于它始终占着资源,就导致了我们不能再对文件进行访问了。

    不及时释放资源还会带来另外的一个问题,虽然之前我们说实现IDisposable接口的类,GC可以自动帮我们释放,但这个过程被延长了,因为它不是在一次回收中完成所有的清理工作,即使GC自动帮我们释放了,那也是先调用FileStream的终结器,在下一次的垃圾回收时才会真正的被释放。

    了解到危害后,我们在打码过程中,如果我们明知道它应该被using起来时,一定要using起来:

    using (var fs = new FileStream(@"C:1.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite))
    {
    }

    需不需要将不再使用的对象置为null

    在上文的内容中,我们都提到要释放资源,但并没有说明需不需要将不再使用的对象置为null,而这个问题也是一直以来争议很大的问题,有人认为将对象置为null能让GC更早地发现垃圾,也有人认为这并没有什么卵用。其实这个问题首先是从方法的内部被提起的,为了更好的说明这个问题,我们先来段代码来检验一下:

    复制代码
    private void button6_Click(object sender, EventArgs e)
    {
        var mc1 = new MyClass() { Name = "mc1" };
        var mc2 = new MyClass() { Name = "mc2" };
        mc1 = null;
    }
    private void button7_Click(object sender, EventArgs e)
    {
        GC.Collect();
    }
    public class MyClass
    {
        public string Name { get; set; }
        ~MyClass()
        {
            MessageBox.Show(Name + "被销毁了");
        }
    }
    复制代码

    单击按钮6,再单击按钮7,我们发现:

    没有置为null的mc2会先被释放,虽然它在mc1被置为null之后;

    在CLR托管的应用程序中,有一个“根”的概念,类型的静态字段、方法参数以及局部变量都可以被作为“根”存在(值类型不能作为“根”,只有引用类型才能作为“根”)。

    上面的代码中,mc1和mc2在代码运行过程中分别会在内存中创建一个“根”。在垃圾回收的过程中,GC会沿着线程栈扫描“根”(栈的特点先进后出,也就是mc2在mc1之后进栈,但mc2比mc1先出栈),检查完毕后还会检查所有引用类型的静态字段的集合,当检查到方法内存在“根”时,如果发现没有任何一个地方引用这个局部变量的时候,不管你是否已经显式的置为null这都意味着“根”已经被停止,然后GC就会发现该根的引用为空,就会被标记为可被释放,这也代表着mc1和mc2的内存空间可以被释放,所以上面的代码mc1=null没有任何意义(方法的参数变量也是如此)。

    其实.NET的JIT编译器是一个优化过的编译器,所以如果我们代码里面将局部变量置为null,这样的语句会被忽略掉:

    s=null;

    如果我们的项目是在Release配置下的,上面的代码压根就不会被编译到dll,正是由于我们上面的分析,所以很多人都会认为将对象赋值为null完全没有必要,但是,在另一种情况下,就完全有必要将对象赋值为null,那就是静态字段或属性,但这斌不意味着将对象赋值为null就是将它的静态字段赋值为null:

    复制代码
    private void button6_Click(object sender, EventArgs e)
    {
        var mc = new MyClass() { Name = "mc" };
    }
    private void button7_Click(object sender, EventArgs e)
    {
        GC.Collect();
    }
    public class MyClass
    {
        public string Name { get; set; }
        public static MyClass2 MyClass2 { get; set; } = new MyClass2();
        ~MyClass()
        {
            //MyClass2 = null;
            MessageBox.Show(Name + "被销毁了");
        }
    }
    public class MyClass2
    {
        ~MyClass2()
        {
            MessageBox.Show("MyClass2被释放");
        }
    }
    复制代码

    上面的代码运行我们会发现,当mc被回收时,它的静态属性并没有被GC回收,而我们将MyClass终结器中的MyClass2=null的注释取消,再运行,当我们两次点击按钮7的时候,属性MyClass2才被真正的释放,因为第一次GC的时候只是在终结器里面将MyClass属性置为null,在第二次GC的时候才当作垃圾回收了,之所以静态变量不被释放(即使赋值为null也不会被编译器优化),是因为类型的静态字段一旦被创建,就被作为“根”存在,基本上不参与GC,所以GC始终不会认为它是个垃圾,而非静态字段则不会有这样的问题。

    所以在实际工作当中,一旦我们感觉静态变量所占用的内存空间较大的时候,并且不会再使用,便可以将其置为null,最典型的案例就是缓存的过期策略的实现了,将静态变量置为null这或许不是很有必要,但这绝对是一个好的习惯,试想一个项目中,如果将某个静态变量作为全局的缓存,如果没有做过期策略,一旦项目运行,那么它所占的内存空间只增不减,最终顶爆机器内存,所以,有个建议就是:尽量地少用静态变量。

  • 相关阅读:
    A Simple Problem with Integers poj 3468 多树状数组解决区间修改问题。
    Fliptile 开关问题 poj 3279
    Face The Right Way 一道不错的尺取法和标记法题目。 poj 3276
    Aggressive cows 二分不仅仅是查找
    Cable master(二分题 注意精度)
    B. Pasha and String
    Intervals poj 1201 差分约束系统
    UITextField的快速基本使用代码块
    将UIImage转换成圆形图片image
    color转成image对象
  • 原文地址:https://www.cnblogs.com/bruce1992/p/14075627.html
Copyright © 2011-2022 走看看