使用IDisposable清理临时文件的C#方法

10
我有一个FileUploader类,可以选择性地提供一个zip文件,它会将其解压缩到临时位置并返回文件路径。
据我所了解,在FileUploader类上实现IDisposable接口,并使用Dispose方法删除所有临时文件,可以使类在引用超出上下文范围时自动清理自己?
然而,情况似乎并非如此,请问有人可以解释一下我该如何实现我的目标吗?
更新: 为了澄清,我的代码是:
    public ActionResult ImportFile()
    {
        FileUploader uploader = new FileUploader(ControllerContext, "file"); // where "file" is the posted form's file element

        uploader.SaveOrExtractFilesToTempLocation();

        foreach (string file in uploader.files)
        {
            try
            {
                 // do some stuff
            }
            catch (Exception err)
            {
                 // let the user know
            }
        }
        return View();
    }

我试图让FileUploader在ImportFile()方法完成后删除所有临时文件。


1
你能告诉我们 FileUploader 是做什么的,以及它与解压缩有什么关系吗?我可以提供一个作为 IDisposable 的样例 ZipExtracter,但这可能无法完全涵盖实际用途。 - Ani
FileUploader 只接受表单提交的文件,并返回上传的临时文件的物理位置。但是,如果提交的文件是一个 zip 文件,它会解压缩并返回所有已提取的临时文件的位置。 - Jimbo
7个回答

8
“脱离上下文”这个说法不够清晰,你需要做的是:
using(var file = new FileUploader(...)) {
    // do the work here
}

如果不使用using,就没有特殊处理。然后编译器将其转换为类似于以下内容:

var file = new FileUploader(...);
IDisposable tmp = file;
try {
    // do the work here
} finally {
    if(tmp != null) tmp.Dispose();
}

而正是这个导致了确定性清理。


7

您需要正确实现IDisposable。就像下面的类一样。

using System.IO;

/// <summary>
/// Represents a temporary storage on file system.
/// </summary>
public sealed partial class TempStorage
     : IDisposable
{
    #region Constructor

    private TempStorage()
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="TempStorage"/> class.
    /// </summary>
    /// <param name="path">The path to use as temp storage.</param>
    public TempStorage(string path)
    {
        this.Path = path;
        this.Clear();
        this.Create();
    }

    #endregion

    #region Properties

    private string Path
    {
        get;
        set;
    }

    #endregion

    #region Methods

    private void Create()
    {
        try
        {
            if (!Directory.Exists(this.Path))
            {
                Directory.CreateDirectory(this.Path);
            }
        }
        catch (IOException)
        {
        }
    }

    public void Clear()
    {
        try
        {
            if (Directory.Exists(this.Path))
            {
                Directory.Delete(this.Path, true);
            }
        }
        catch (IOException)
        {
        }
    }

    #endregion

    #region IDisposable

    /// <summary>
    /// An indicator whether this object is beeing actively disposed or not.
    /// </summary>
    private bool disposed;

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

    /// <summary>
    /// Throws an exception if something is tried to be done with an already disposed object.
    /// </summary>
    /// <remarks>
    /// All public methods of the class must first call this.
    /// </remarks>
    public void ThrowIfDisposed()
    {
        if (this.disposed)
        {
            throw new ObjectDisposedException(this.GetType().Name);
        }
    }

    /// <summary>
    /// Releases managed resources upon dispose.
    /// </summary>
    /// <remarks>
    /// All managed resources must be released in this
    /// method, so after disposing this object no other
    /// object is beeing referenced by it anymore.
    /// </remarks>
    private void ReleaseManagedResources()
    {
        this.Clear();
    }

    /// <summary>
    /// Releases unmanaged resources upon dispose.
    /// </summary>
    /// <remarks>
    /// All unmanaged resources must be released in this
    /// method, so after disposing this object no other
    /// object is beeing referenced by it anymore.
    /// </remarks>
    private void ReleaseUnmanagedResources()
    {
    }

    private void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            /* Release unmanaged ressources */
            this.ReleaseUnmanagedResources();

            if (disposing)
            {
                /* Release managed ressources */
                this.ReleaseManagedResources();
            }

            /* Set indicator that this object is disposed */
            this.disposed = true;
        }
    }

    #endregion
}

然后在您的主方法中使用using块内的类,如下所示:

using(TempStorage myStorage = new TempStorage("C:\temp")
{
    // rest of the main method here...
}

如果没有非托管资源,就不需要实现Dispose模式。只需使用Dispose方法删除临时文件即可。 - Lukas Körfer

2

Dispose只有在使用using关键字包装上下文时才会自动执行:

using (FileUploader uploader = new FileUploader(...))
    uploader.UploadFiles();

1

您可以实现终结器,以便在忘记调用Dispose(您真的不应该这样做!)并在终结器中调用Dispose时。当对象被垃圾回收时,将调用此方法-但它是不确定的,即无法找出何时调用。

但这是不能保证的


0

IDisposable适用于对象在某种方式下改变自身外部的东西,必须在对象消失之前清理。创建临时文件似乎符合条件。然而,有一些注意事项:

  1. 不幸的是,没有一种好的方法让处理器确定它运行时是否有异常挂起。如果我能选择,将会有一个IDisposeableEx接口,它将继承IDisposable但也实现一个Dispose(Ex as Exception)重载;这将像IDisposable一样工作,但Ex将作为InnerException传递给IDisposable本身可能抛出的任何异常。但事实上,这样的东西并不存在。因此,人们经常面临不舒服的选择:吞噬在处理时发生的异常,或者埋藏在处理之前发生的异常(可能是首次进行处理的先决条件)。两种选择都很糟糕。
  2. 终结器必须避免做任何可能阻塞或引发异常的事情。对于像文件清理这样的事情,可能有一个文件清理线程(针对一组类,而不是每个对象一个!)等待终结器发出信号,然后清理文件。如果尝试清理文件被阻塞,文件清理线程可能会被阻塞,但它不会影响整个应用程序。

我并不特别喜欢finalizers;重点应该放在让处理器工作上。


0

正如其他人所说,除非上下文被包装在using关键字中,否则不会自动调用Dispose。但是,您可以实现Dispose和Finalize模式(从终结器/析构函数中调用Dispose方法)。这样,即使您的代码没有直接调用Dispose(因为垃圾收集器最终会调用终结器),也有一种故障转移机制可以启动并删除临时文件。

本文阐述了这个概念,并提供了一些关于终结器工作原理的见解。


1
话虽如此,我认为最干净的方法是在您的FileUploader周围使用using块。 - Anders Fjeldstad
那么Dispose方法最终会在该对象上被调用吗?我问这个问题是因为我并不介意临时文件何时被删除,只要它们最终被删除就可以了 :) - Jimbo
通常情况下,当对象超出作用域后,您可以预期在某个未知时间运行终结器,但是在某些异常情况下,运行时将不会调用终结器。 - Anders Fjeldstad
请参考 http://msdn.microsoft.com/zh-cn/library/b1yfkh5e(v=VS.100).aspx 获取示例。 - Anders Fjeldstad

0

就像其他人所说的那样,你最好将其包装在using语句中,以强制调用Dispose方法。我相信这是微软建议使用任何实现IDispose接口的东西的方式,至少基于他们的代码分析规则。


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