如何在C#中自动删除临时文件?

72

如何确保应用程序关闭或崩溃时删除临时文件? 最理想的情况是,我希望获取一个临时文件,使用它,然后忘记它。

目前,我会保留我的临时文件列表,并使用在Application.ApplicationExit上触发的EventHandler来删除它们。

是否有更好的方法?


3
很遗憾,.NET 没有像 Java 中的 File 类中的 deleteOnExit() 函数... 尽管如果文件没有关闭,它也无法正常工作。 - Powerlord
我认为FileOptions - DeleteOnClose选项可以在关闭文件流时删除文件流。此选项需要在创建文件流时提供。https://learn.microsoft.com/en-us/dotnet/api/system.io.fileoptions?view=net-6.0 - Shankar S
9个回答

88

如果进程被提前杀死,那就没有什么是可以保证的,但是我使用"using"来做这件事..

using System;
using System.IO;
sealed class TempFile : IDisposable
{
    string path;
    public TempFile() : this(System.IO.Path.GetTempFileName()) { }

    public TempFile(string path)
    {
        if (string.IsNullOrEmpty(path)) throw new ArgumentNullException("path");
        this.path = path;
    }
    public string Path
    {
        get
        {
            if (path == null) throw new ObjectDisposedException(GetType().Name);
            return path;
        }
    }
    ~TempFile() { Dispose(false); }
    public void Dispose() { Dispose(true); }
    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            GC.SuppressFinalize(this);                
        }
        if (path != null)
        {
            try { File.Delete(path); }
            catch { } // best effort
            path = null;
        }
    }
}
static class Program
{
    static void Main()
    {
        string path;
        using (var tmp = new TempFile())
        {
            path = tmp.Path;
            Console.WriteLine(File.Exists(path));
        }
        Console.WriteLine(File.Exists(path));
    }
}

现在,当TempFile被处理或垃圾回收时,该文件将被删除(如果可能)。你可以根据需要将其作为紧密范围的使用,或者收集到某个地方使用。


13
很少有恰当的场合可以使用空的 catch 块。 - Robert Rossney
我可能会实现这个的变体。它很直接并且易于理解。 - Nifle
这段代码能删除其他应用程序使用的临时文件吗? - gasroot
4
@KevinPanko 因为我还没有花时间思考所有可能的继承场景,以确定在子类化时是否会按预期运行。如果您想这样做,请随意。 - Marc Gravell
需要使用终结器吗?我认为System.IO.File是托管的,所以您可以在不需要终结器的情况下使用它?[终结器]令人困惑、容易出错且被广泛误解。它的语法非常熟悉于C++用户,但语义上却有惊人的不同。在大多数情况下,使用该功能是危险的、不必要的或者是一个错误的症状。参考:http://www.informit.com/articles/article.aspx?p=2425867 - PMBjornerud
3
@PMBjornerud 是的,在这种情况下,最终器的目的与 File 对象无关。它与类的意图有关,即(参见问题)在完成时删除文件。我们想要这样做,即使有人未能正确地使用 Dispose()。我们甚至没有一个 File 实例。 - Marc Gravell

80

考虑使用FileOptions.DeleteOnClose标志:

using (FileStream fs = new FileStream(Path.GetTempFileName(),
       FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None,
       4096, FileOptions.RandomAccess | FileOptions.DeleteOnClose))
{
    // temp file exists
}

// temp file is gone

在大多数情况下,这是最简单/最好的解决方案; 让操作系统来处理它。我编辑了代码,使其更清晰,并将缓冲区大小更改为与FileStream默认值相匹配。顺便说一句,我更喜欢FileOptions.CreateNew,但我保留了FileMode.OpenOrCreate,因为你有意选择了它。 - piedar
3
即使设置了FileOptions.DeleteOnClose并且不考虑FileShare.ReadWrite,另一个进程也无法访问创建的文件。因此,总体来说,这段代码作为临时文件没有意义。 - Tomas
@Tomas 不知道你是否能详细说明一下为什么这段代码片段即使使用了DeleteOnClose也不允许其他进程访问?(我并不是在反驳你,因为我刚刚写了同样的using语句,但遇到了一个进程仍然持有该文件的问题,所以我找到了这个线程!)我是C#新手,似乎无法确定具体是什么原因导致了这种情况,以及推荐的替代方案是什么(使用File.Create而不是new FileStream?)。 - Kieran Osgood
1
@piedar Path.GetTempFileName() 已经创建了文件,因此 FileOptions.CreateNew 不起作用并会抛出一个 IOException: The file ... already exists - Bouke

22

12
如果您需要多次打开文件(例如在以后的时间或由另一个进程),请小心使用此方法。例如,如果您希望打开、填充和关闭文件,它可能会立即消失,因此您无法使用刚刚创建的内容。您必须保留原始句柄足够长的时间,以便第二个句柄能够打开,但如果没有使用适当的标志打开,则可能会导致共享冲突。在.NET 4中(可能更早),只需使用File.Create(path, 0x1000, FileOptions.DeleteOnClose)即可,无需使用P/Invoke。 - Triynko
6
我想知道是否有办法在使用System.Diagnostics.Process.Start(myFile)时与FileOptions.DeleteOnClose一起使用。我想要显示该文件并立即删除。 - knockando
@knockando 通常是通过等待进程完成后删除文件来完成的。我不认为可以使用 DeleteOnClose 来实现这一点。 - Paul Groke

10
我建议使用.NET的TempFileCollection类进行操作,因为这个类是内置的,在旧版本的.NET中也可以使用,并且实现了IDisposable接口,因此如果与using关键字结合使用,则会在使用后自行清理。以下是一个从嵌入资源中提取文本的示例(通过项目属性页面->资源选项卡添加,如此描述:如何将文本文件嵌入.NET程序集?,然后在嵌入文件的属性设置中将其设置为“EmbeddedResource”)。
    // Extracts the contents of the embedded file, writes them to a temp file, executes it, and cleans up automatically on exit.
    private void ExtractAndRunMyScript()
    {
        string vbsFilePath;

        // By default, TempFileCollection cleans up after itself.
        using (var tempFiles = new System.CodeDom.Compiler.TempFileCollection())
        {
            vbsFilePath= tempFiles.AddExtension("vbs");

            // Using IntelliSense will display the name, but it's the file name
            // minus its extension.
            System.IO.File.WriteAllText(vbsFilePath, global::Instrumentation.Properties.Resources.MyEmbeddedFileNameWithoutExtension);

            RunMyScript(vbsFilePath);
        }

        System.Diagnostics.Debug.Assert(!File.Exists(vbsFilePath), @"Temp file """ + vbsFilePath+ @""" has not been deleted.");
    }

6

我使用了更可靠的解决方案:

using System.IO;
using System.Reflection;
 
namespace Helpers
{
    public static partial class TemporaryFiles
    {
        private const string UserFilesListFilenamePrefix = ".used-temporary-files.txt";
        static private readonly object UsedFilesListLock = new object();
 
        private static string GetUsedFilesListFilename()
        {
            return Assembly.GetEntryAssembly().Location + UserFilesListFilenamePrefix;
        }
 
        private static void AddToUsedFilesList(string filename)
        {
            lock (UsedFilesListLock)
            {
                using (var writer = File.AppendText(GetUsedFilesListFilename()))
                    writer.WriteLine(filename);
            }
        }
 
        public static string UseNew()
        {
            var filename = Path.GetTempFileName();
            AddToUsedFilesList(filename);
            return filename;
        }
 
        public static void DeleteAllPreviouslyUsed()
        {
            lock (UsedFilesListLock)
            {
                var usedFilesListFilename = GetUsedFilesListFilename();
 
                if (!File.Exists(usedFilesListFilename))
                    return;
 
                using (var listFile = File.Open(usedFilesListFilename, FileMode.Open))
                {
                    using (var reader = new StreamReader(listFile))
                    {
                        string tempFileToDelete;
                        while ((tempFileToDelete = reader.ReadLine()) != null)
                        {
                            if (File.Exists(tempFileToDelete))
                                File.Delete(tempFileToDelete);
                        }
                    }
                }
 
                // Clean up
                using (File.Open(usedFilesListFilename, FileMode.Truncate)) { }
            }
        }
    }
}

每次需要临时文件时,请使用以下方法:

var tempFile = TemporaryFiles.UseNew();

为了确保应用程序关闭或崩溃后删除所有临时文件,请放置以下代码:
TemporaryFiles.DeleteAllPreviouslyUsed();

在应用程序启动时。

或者你可以使用我们LinksPlatform类库中的TemporaryFiles抽象(可作为NuGet软件包获得)。 - Konard

3

我不是主要的C#程序员,但在C++中,我会使用RAII。有一些关于在C#中使用类似RAII行为的提示在线上,但大多数似乎使用终结器-这是不确定的。

我认为有一些Windows SDK函数可以创建临时文件,但不知道它们是否在程序终止时自动删除。有GetTempPath函数,但其中的文件仅在注销或重新启动时才会被删除,如果我没记错的话。

附注:C# 析构函数文档 表示您可以并且应该在其中释放资源,我觉得有点奇怪。 如果是这样,您可以在析构函数中简单地删除临时文件,但这可能不完全确定。


当进程异常退出时,你如何期望RAII工作?OP明确地问道:“...或者崩溃”。 - MarkusSchaber
如果是异常,RAII很好用。但如果你拔掉电脑的电源线,那就没有办法了。 :) - csl
@csl:还有其他方法(例如强制进程终止),这些方法会阻止RAII的工作。 - MarkusSchaber
@MarkusSchaber:正是我的观点。如果你决定删除这些文件,你可以创建一个哨兵PID来代替你完成,将文件挂钩到Windows的删除-温暖重启或其他操作中。OP说“关闭或崩溃”,而“使用”也不包括崩溃。 - csl
@csl:同意,但是指向 FILE_FLAG_DELETE_ON_CLOSE 的答案接近目标。 - MarkusSchaber
显示剩余2条评论

3
很高兴看到您想要负责,但如果文件不是非常大(>50MB),您可以像其他人(包括MS)一样将它们保留在临时目录中。磁盘空间是充足的。 正如csl 发布的那样,GetTempPath是正确的方法。空间不足的用户将能够运行磁盘清理,您的文件(以及其他人的文件)也将被清理掉。

13
我个人认为临时文件就是临时文件,应该在不再需要时立即删除。我讨厌我的磁盘被各种垃圾堆积。可悲的是没有人(包括微软)关心我的电脑状态... - user39603
4
同意。如果由于某种原因无法清理所有文件,则最好将它们放在临时目录中,而不是其他任何地方。 - user39603
3
当现实与人们的理想差距较大时,人们往往不喜欢这种情况。 - StingyJack
2
问题在于Windows无法知道何时应删除临时文件,因此它必须将它们留在磁盘上,让程序员记得删除它们。如果他们能记得删除就好了!更多的程序员不使用FileOptions DeleteOnClose真是可惜。 - Matthew Lock
5
在大多数情况下,当出现突然停电的情况时,保留临时文件可能是可以接受的。而当进程被终止时也可以考虑这样做。但是,如果应用程序既没有被终止也没有因停电等原因中断,保留临时文件肯定是不可取的。请注意,此翻译仅供参考,不得用于正式场合。 - Paul Groke
显示剩余2条评论

0
你可以在启动时启动一个线程,删除存在于“不应该”存在的文件,以从崩溃中恢复。

-2
如果您正在构建一个Windows窗体应用程序,您可以使用以下代码:
    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        File.Delete("temp.data");
    }

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