实现IDisposable的正确方式

173

在我的课堂上,我按照以下方式实现IDisposable:

public class User : IDisposable
{
    public int id { get; protected set; }
    public string name { get; protected set; }
    public string pass { get; protected set; }

    public User(int UserID)
    {
        id = UserID;
    }
    public User(string Username, string Password)
    {
        name = Username;
        pass = Password;
    }

    // Other functions go here...

    public void Dispose()
    {
        // Clear all property values that maybe have been set
        // when the class was instantiated
        id = 0;
        name = String.Empty;
        pass = String.Empty;
    }
}
在VS2012中,我的代码分析指出要正确实现IDisposable,但我不确定我在这里做错了什么。
确切的文本如下: CA1063 实现 IDisposable 时应正确 提供 'User' 上可重写的 Dispose(bool) 实现或将类型标记为 sealed。调用 Dispose(false) 应只清理托管资源。调用 Dispose(true) 应同时清理托管和非托管资源。stman User.cs 10 参考:CA1063:正确实现 IDisposable 我已经阅读了这个页面,但恐怕我真的不明白需要在这里做什么。 如果有人能更通俗易懂地解释问题和/或如何实现IDisposable,那将真正有帮助!

1
Dispose 方法里面就只有这些代码吗? - Claudio Redi
45
你应该实现你的Dispose()方法来调用你类中任何成员的Dispose()方法。这些成员中没有一个有Dispose()方法。所以你不应该实现IDisposable。重置属性值是无意义的。 - Hans Passant
16
只有在需要释放非托管资源时(包括被封装的非托管资源,如 SqlConnectionFileStream 等),才需要实现 IDisposable 接口。如果只有托管资源,就不需要也不应该实现 IDisposable 接口。我认为这是代码分析中的一个主要问题。它非常擅长检查愚蠢的小规则,但并擅长检查概念上的错误。 - jason
2
@Ortund 在 Stack Overflow 上已经有大量关于可处理模式的资料。即使在这个问题的回答中,也有一些微妙的误解该模式的例子。更好的方法是将未来的提问者指向第一个相关的 SO 问题(该问题有309个赞)。 - Keith Payne
4
请勿投反对票或赞成票,让帖子保持零分,并提供有用的提示关闭问题。 - tjmoore
显示剩余2条评论
8个回答

141
这将是正确的实现方式,尽管我没有看到您在发布的代码中需要处理的任何内容。只有在以下情况下才需要实现IDisposable
  1. 您具有非托管资源
  2. 您持有可以被处理的对象的引用。
您发布的代码中没有需要处理的内容。
public class User : IDisposable
{
    public int id { get; protected set; }
    public string name { get; protected set; }
    public string pass { get; protected set; }

    public User(int userID)
    {
        id = userID;
    }
    public User(string Username, string Password)
    {
        name = Username;
        pass = Password;
    }

    // Other functions go here...

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing) 
        {
            // free managed resources
        }
        // free native resources if there are any.
    }
}

5
开始学写C#时,有人告诉我最好尽可能多地利用using(){ }结构,但为此需要实现IDisposable接口。因此,一般情况下,我更喜欢通过usings来访问类,特别是在只需要在一个或两个函数中使用该类时。 - Ortund
72
@Ortund 你误解了。最好在类实现了IDisposable接口时使用using代码块。如果你不需要一个类是可处理的,就不要去实现它。这没有任何意义。 - Daniel Mann
9
@DanielMann using语句块的语义往往不仅适用于IDisposable接口本身,而且非常吸引人。我想可能有很多滥用IDisposable仅用于作用域目的的情况。 - Thomas
1
只是顺便提一下,如果您有任何未受管理的资源需要释放,您应该包括调用Dispose(false)的Finalizer,这将允许GC在进行垃圾回收时调用Finalizer(如果尚未调用Dispose),并正确释放未受管控的资源。 - mariozski
4
在您的实现中没有终结器调用GC.SuppressFinalize(this);是毫无意义的。正如@mariozski所指出的,如果该类未在using块内使用,则终结器将有助于确保调用Dispose - Haymo Kutschbach
我认为你会想要使用Dispose方法来分离对象内部连接的任何事件。 - John Kurtz

70
首先,您不需要“清理”字符串和整数-垃圾回收器会自动处理它们。在Dispose中需要清理的是非托管资源或实现IDisposable的托管资源。
然而,假设这只是一个学习练习,推荐实现IDisposable的方法是添加“安全保护措施”,以确保任何资源不会被二次处理:
public void Dispose()
{
    Dispose(true);

    // Use SupressFinalize in case a subclass 
    // of this type implements a finalizer.
    GC.SuppressFinalize(this);   
}
protected virtual void Dispose(bool disposing)
{
    if (!_disposed)
    {
        if (disposing) 
        {
            // Clear all property values that maybe have been set
            // when the class was instantiated
            id = 0;
            name = String.Empty;
            pass = String.Empty;
        }

        // Indicate that the instance has been disposed.
        _disposed = true;   
    }
}

3
+1,拥有一个标志来确保清理代码只执行一次比将属性设置为 null 或其他内容要好得多(特别是因为这会干扰 readonly 语义)。 - Thomas
5
使用用户的代码(即使它将自动清理)来明确放置内容,这是一个好的做法。同时,不像其他人那样对他在学习过程中犯的小错误耿耿于怀,这也是一个优秀之处。请给他点赞。 - Murphybro2
IDisposable 的契约规定,调用 Dispose 多次是无害的(它不应破坏对象或抛出异常)。记住对象已被处理主要是为了使那些不能在已处理对象上使用的方法能够及早地抛出 ObjectDisposedException(而不是在调用包含对象的方法时,甚至导致意外的不同异常)。 - Mike Rosoft

53
下面的示例展示了实现IDisposable接口的通用最佳实践。参考 请记住,仅当您的类中有非托管资源时,您才需要一个析构函数(finalizer)。如果添加析构函数,您应该在Dispose中禁止Finalization,否则它会导致您的对象存储在内存中的时间比应该更长(注:阅读Finalization如何工作)。下面的示例详细说明了以上所有内容。
public class DisposeExample
{
    // A base class that implements IDisposable. 
    // By implementing IDisposable, you are announcing that 
    // instances of this type allocate scarce resources. 
    public class MyResource: IDisposable
    {
        // Pointer to an external unmanaged resource. 
        private IntPtr handle;
        // Other managed resource this class uses. 
        private Component component = new Component();
        // Track whether Dispose has been called. 
        private bool disposed = false;

        // The class constructor. 
        public MyResource(IntPtr handle)
        {
            this.handle = handle;
        }

        // Implement IDisposable. 
        // Do not make this method virtual. 
        // A derived class should not be able to override this method. 
        public void Dispose()
        {
            Dispose(true);
            // This object will be cleaned up by the Dispose method. 
            // Therefore, you should call GC.SupressFinalize to 
            // take this object off the finalization queue 
            // and prevent finalization code for this object 
            // from executing a second time.
            GC.SuppressFinalize(this);
        }

        // Dispose(bool disposing) executes in two distinct scenarios. 
        // If disposing equals true, the method has been called directly 
        // or indirectly by a user's code. Managed and unmanaged resources 
        // can be disposed. 
        // If disposing equals false, the method has been called by the 
        // runtime from inside the finalizer and you should not reference 
        // other objects. Only unmanaged resources can be disposed. 
        protected virtual void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called. 
            if(!this.disposed)
            {
                // If disposing equals true, dispose all managed 
                // and unmanaged resources. 
                if(disposing)
                {
                    // Dispose managed resources.
                    component.Dispose();
                }

                // Call the appropriate methods to clean up 
                // unmanaged resources here. 
                // If disposing is false, 
                // only the following code is executed.
                CloseHandle(handle);
                handle = IntPtr.Zero;

                // Note disposing has been done.
                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);

        // Use C# destructor syntax for finalization code. 
        // This destructor will run only if the Dispose method 
        // does not get called. 
        // It gives your base class the opportunity to finalize. 
        // Do not provide destructors in types derived from this class.
        ~MyResource()
        {
            // Do not re-create Dispose clean-up code here. 
            // Calling Dispose(false) is optimal in terms of 
            // readability and maintainability.
            Dispose(false);
        }
    }
    public static void Main()
    {
        // Insert code here to create 
        // and use the MyResource object.
    }
}

15

IDisposable的存在是为了提供一种方式让您清理由垃圾回收器无法自动清理的非托管资源。

您正在“清理”的所有资源都是托管资源,因此您的Dispose方法没有起到任何作用。您的类根本不应该实现IDisposable。垃圾回收器会自己很好地处理所有这些字段。


1
同意这一点 - 有一种将所有东西处理掉的概念,而实际上并不需要这样做。Dispose 应该仅在您需要清理非托管资源时使用!! - Chandramouleswaran Ravichandra
4
Dispose方法不是严格意义上的,它还允许你处理“实现IDisposable接口的托管资源”。 - Matt Wilko
1
@MattWilko 这使得它成为处理非托管资源的一种间接方式,因为它允许另一个资源来处理非托管资源。在这里,甚至没有通过另一个托管资源对非托管资源进行间接引用。 - Servy
@MattWilko 在任何实现了IDisposable接口的托管资源中,Dispose方法都会自动调用。 - panky sharma
@pankysharma 不会自动调用,需要手动调用。这就是它的全部意义。你不能假设它会自动被调用,你只知道人们应该手动调用它,但人们会犯错误和忘记。 - Servy

14

你需要像这样使用可处置模式

private bool _disposed = false;

protected virtual void Dispose(bool disposing)
{
    if (!_disposed)
    {
        if (disposing)
        {
            // Dispose any managed objects
            // ...
        }

        // Now disposed of any unmanaged objects
        // ...

        _disposed = true;
    }
}

public void Dispose()
{
    Dispose(true);
    GC.SuppressFinalize(this);  
}

// Destructor
~YourClassName()
{
    Dispose(false);
}

1
在析构函数中加入一次 GC.SuppressFinalize(this) 的调用,这样不是更明智吗?否则对象本身会在下一次 GC 中被回收。 - Sudhanshu Mishra
2
@dotnetguy:当垃圾回收运行时,对象的析构函数会被调用。因此,无法调用两次。请参见此处:https://msdn.microsoft.com/zh-cn/library/ms244737.aspx - schoetbi
1
那么现在我们把任何样板代码都称为“模式”了吗? - Chel
4
不,我们不是。MSDN在这里明确表示它是一个模式"Dispose Pattern" - https://msdn.microsoft.com/zh-cn/library/b1yfkh5e(v=vs.110).aspx,所以在给出负评前,也许您可以稍微Google一下? - Belogix
2
微软和你的帖子都没有明确说明为什么模式应该是这样的。通常,它甚至不是样板文件,只是多余的 - 被 SafeHandle(及其子类型)所取代。对于实现适当处理的托管资源,处理变得更加简单;您可以将代码简化为 void Dispose() 方法的简单实现。 - BatteryBackupUnit
显示剩余2条评论

12

如果类(如User)没有获取非托管资源(例如文件、数据库连接等),则无需将其标记为IDisposable。通常,我们仅在类具有至少一个IDisposable字段或/和属性时才将其标记为IDisposable。 实现IDisposable时,最好按照微软的典型方案进行:

public class User: IDisposable {
  ...
  protected virtual void Dispose(Boolean disposing) {
    if (disposing) {
      // There's no need to set zero empty values to fields 
      // id = 0;
      // name = String.Empty;
      // pass = String.Empty;

      //TODO: free your true resources here (usually IDisposable fields)
    }
  }

  public void Dispose() {
    Dispose(true);

    GC.SuppressFinalize(this);
  } 
}

通常情况下是这样的。但另一方面,使用构造函数打开了编写类似于C++智能指针的东西的可能性,即无论如何退出using块,都将恢复先前的状态的对象。我发现唯一的方法是使这样的对象实现IDisposable。在这种边缘用例中,似乎可以忽略编译器的警告。 - Papa Smurf

4
我看到了许多微软Dispose模式的例子,实际上这是一种反模式。正如许多人指出的那样,问题中的代码根本不需要IDisposable。但是,如果您要实现它,请不要使用Microsoft模式。更好的答案是遵循这篇文章中的建议:

https://www.codeproject.com/Articles/29534/IDisposable-What-Your-Mother-Never-Told-You-About

唯一可能有帮助的另一件事是抑制代码分析警告... https://learn.microsoft.com/en-us/visualstudio/code-quality/in-source-suppression-overview?view=vs-2017

4

当您需要确定性(确认)垃圾回收时,可以实现IDisposable。

class Users : IDisposable
    {
        ~Users()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
            // This method will remove current object from garbage collector's queue 
            // and stop calling finilize method twice 
        }    

        public void Dispose(bool disposer)
        {
            if (disposer)
            {
                // dispose the managed objects
            }
            // dispose the unmanaged objects
        }
    }

在创建和使用Users类时,请使用“using”块来避免显式调用dispose方法:

using (Users _user = new Users())
            {
                // do user related work
            }

使用块结束后,通过隐式调用dispose方法来处理创建的Users对象。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接