我使用。net 3.5,试图递归删除目录使用:

Directory.Delete(myPath, true);

我的理解是,如果文件正在使用或存在权限问题,这应该抛出,但否则它应该删除目录及其所有内容。

然而,我偶尔会遇到这样的情况:

System.IO.IOException: The directory is not empty.
    at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
    at System.IO.Directory.DeleteHelper(String fullPath, String userPath, Boolean recursive)
    at System.IO.Directory.Delete(String fullPath, String userPath, Boolean recursive)
    ...

我并不惊讶于这个方法有时会抛出错误,但我惊讶于当递归为真时得到这个特定的消息。(我知道目录不是空的。)

是否有一个原因,我将看到这个而不是AccessViolationException?


当前回答

在Windows资源管理器中选择路径或子文件夹似乎足以阻止目录的一次执行。Delete(path, true),抛出如上所述的IOException和死亡,而不是引导Windows资源管理器到父文件夹并按预期进行。

其他回答

我很惊讶没有人想到这个简单的非递归方法,它可以删除包含只读文件的目录,而不需要更改每个文件的只读属性。

Process.Start("cmd.exe", "/c " + @"rmdir /s/q C:\Test\TestDirectoryContainingReadOnlyFiles"); 

(稍微改变一下,暂时不启动cmd窗口,这在互联网上都是可用的)

您可以通过运行以下命令来重现错误:

Directory.CreateDirectory(@"C:\Temp\a\b\c\");
Process.Start(@"C:\Temp\a\b\c\");
Thread.Sleep(1000);
Directory.Delete(@"C:\Temp\a\b\c");
Directory.Delete(@"C:\Temp\a\b");
Directory.Delete(@"C:\Temp\a");

当试图删除目录'b'时,它抛出IOException“目录不是空的”。这很愚蠢,因为我们刚刚删除了目录'c'。

在我的理解中,解释是目录'c'被标记为已删除。但是系统中还没有提交删除操作。系统已经回复任务已经完成,而实际上它还在处理中。系统可能会等待文件资源管理器已经关注到父目录才提交删除。

如果你查看删除函数(http://referencesource.microsoft.com/#mscorlib/system/io/directory.cs)的源代码,你会发现它使用本机Win32Native。RemoveDirectory函数。这种不等待行为在这里被注意到:

RemoveDirectory函数在关闭时标记要删除的目录。因此,直到关闭该目录的最后一个句柄,才会删除该目录。

(http://msdn.microsoft.com/en-us/library/windows/desktop/aa365488 (v = vs.85) . aspx)

休眠和重试是解决方案。参考ryascl的溶液。

在继续之前,检查一下你能控制的以下原因:

该文件夹是否设置为您的进程的当前目录?如果是,请先将其更改为其他内容。 您是否从该文件夹中打开了文件(或加载了DLL) ?(并且忘记关闭/卸载它)

否则,请检查以下不在你控制范围内的合理原因:

该文件夹中有标记为只读的文件。 您没有删除其中一些文件的权限。 文件或子文件夹在资源管理器或其他应用程序中打开。

如果是上述任何一个问题,在尝试改进删除代码之前,您应该了解为什么会发生这种情况。你的应用程序是否应该删除只读或不可访问的文件?是谁给它们做的标记,为什么?

一旦排除了上述原因,仍然存在虚假失败的可能性。如果任何人持有任何被删除的文件或文件夹的句柄,删除将失败,并且有人可能会枚举文件夹或读取其文件的原因有很多:

搜索索引器 要 备份软件

处理虚假失败的一般方法是尝试多次,在尝试之间暂停。显然,您不希望一直尝试下去,因此应该在一定次数的尝试后放弃,并抛出异常或忽略错误。是这样的:

private static void DeleteRecursivelyWithMagicDust(string destinationDir) {
    const int magicDust = 10;
    for (var gnomes = 1; gnomes <= magicDust; gnomes++) {
        try {
            Directory.Delete(destinationDir, true);
        } catch (DirectoryNotFoundException) {
            return;  // good!
        } catch (IOException) { // System.IO.IOException: The directory is not empty
            System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);

            // see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
            Thread.Sleep(50);
            continue;
        }
        return;
    }
    // depending on your use case, consider throwing an exception here
}

在我看来,像这样的帮助器应该用于所有的删除,因为虚假的失败总是可能的。然而,你应该根据你的用例调整这些代码,而不是盲目地复制它。

我的应用程序生成的内部数据文件夹,位于%LocalAppData%下,所以我的分析如下:

The folder is controlled solely by my application, and the user has no valid reason to go and mark things as read-only or inaccessible inside that folder, so I don't try to handle that case. There's no valuable user-created stuff in there, so there's no risk of forcefully deleting something by mistake. Being an internal data folder, I don't expect it to be open in explorer, at least I don't feel the need to specifically handle the case (i.e. I'm fine handling that case via support). If all attempts fail, I choose to ignore the error. Worst case, the app fails to unpack some newer resources, crashes and prompts the user to contact support, which is acceptable to me as long as it does not happen often. Or, if the app does not crash, it will leave some old data behind, which again is acceptable to me. I choose to limit retries to 500ms (50 * 10). This is an arbitrary threshold which works in practice; I wanted the threshold to be short enough so that users wouldn't kill the app, thinking that it has stopped responding. On the other hand, half a second is plenty of time for the offender to finish processing my folder. Judging from other SO answers which sometimes find even Sleep(0) to be acceptable, very few users will ever experience more than a single retry. I retry every 50ms, which is another arbitrary number. I feel that if a file is being processed (indexed, checked) when I try to delete it, 50ms is about the right time to expect the processing to be completed in my case. Also, 50ms is small enough to not result in a noticeable slowdown; again, Sleep(0) seems to be enough in many cases, so we don't want to delay too much. The code retries on any IO exceptions. I don't normally expect any exceptions accessing %LocalAppData%, so I chose simplicity and accepted the risk of a 500ms delay in case a legitimate exception happens. I also didn't want to figure out a way to detect the exact exception that I want to retry on.

If you are trying to recursively delete directory a and directory a\b is open in Explorer, b will be deleted but you will get the error 'directory is not empty' for a even though it is empty when you go and look. The current directory of any application (including Explorer) retains a handle to the directory. When you call Directory.Delete(true), it deletes from bottom up: b, then a. If b is open in Explorer, Explorer will detect the deletion of b, change directory upwards cd .. and clean up open handles. Since the file system operates asynchronously, the Directory.Delete operation fails due to conflicts with Explorer.

不完整的解决方案

我最初发布了以下解决方案,想法是中断当前线程,让资源管理器有时间释放目录句柄。

// incomplete!
try
{
    Directory.Delete(path, true);
}
catch (IOException)
{
    Thread.Sleep(0);
    Directory.Delete(path, true);
}

但是,只有当打开的目录是您正在删除的目录的直接子目录时,这才有效。如果a\b\c\d在资源管理器中是打开的,并且你在a上使用这个,这个技术在删除d和c后将会失败。

一个更好的解决方案

此方法将处理深层目录结构的删除,即使在资源管理器中打开了一个较低级别的目录。

/// <summary>
/// Depth-first recursive delete, with handling for descendant 
/// directories open in Windows Explorer.
/// </summary>
public static void DeleteDirectory(string path)
{
    foreach (string directory in Directory.GetDirectories(path))
    {
        DeleteDirectory(directory);
    }

    try
    {
        Directory.Delete(path, true);
    }
    catch (IOException) 
    {
        Directory.Delete(path, true);
    }
    catch (UnauthorizedAccessException)
    {
        Directory.Delete(path, true);
    }
}

尽管我们自己要做额外的递归工作,但我们仍然必须处理过程中可能发生的UnauthorizedAccessException。尚不清楚第一次删除尝试是否为第二次成功的删除尝试铺平了道路,或者仅仅是抛出/捕获异常导致的时间延迟,允许文件系统赶上。

通过在try块的开头添加Thread.Sleep(0),可以减少在典型条件下抛出和捕获的异常数量。此外,在系统负载较重的情况下,您可能会同时浏览这两个目录。删除尝试并失败。可以将此解决方案作为更健壮的递归删除的起点。

一般的答案

此解决方案仅处理与Windows资源管理器交互的特性。如果你想要一个坚如磐石的删除操作,有一件事要记住,任何东西(病毒扫描程序,无论什么)都可能在任何时候对你试图删除的东西有一个开放的句柄。所以你得稍后再试。多久之后,以及尝试了多少次,取决于删除对象的重要性。正如MSDN所示,

健壮的文件迭代代码必须考虑到许多复杂性 文件系统的。

这个无辜的声明,只提供了一个到NTFS参考文档的链接,应该会让你汗毛直竖。

(编辑:很多。这个答案原来只有第一个不完全解。)

这个答案基于:https://stackoverflow.com/a/1703799/184528。与我的代码不同的是,我们只递归许多删除子目录和文件时,需要调用目录。第一次尝试删除失败(这可能是因为windows资源管理器查看目录)。

    public static void DeleteDirectory(string dir, bool secondAttempt = false)
    {
        // If this is a second try, we are going to manually 
        // delete the files and sub-directories. 
        if (secondAttempt)
        {
            // Interrupt the current thread to allow Explorer time to release a directory handle
            Thread.Sleep(0);

            // Delete any files in the directory 
            foreach (var f in Directory.GetFiles(dir, "*.*", SearchOption.TopDirectoryOnly))
                File.Delete(f);

            // Try manually recursing and deleting sub-directories 
            foreach (var d in Directory.GetDirectories(dir))
                DeleteDirectory(d);

            // Now we try to delete the current directory
            Directory.Delete(dir, false);
            return;
        }

        try
        {
            // First attempt: use the standard MSDN approach.
            // This will throw an exception a directory is open in explorer
            Directory.Delete(dir, true);
        }
        catch (IOException)
        {
            // Try again to delete the directory manually recursing. 
            DeleteDirectory(dir, true);
        }
        catch (UnauthorizedAccessException)
        {
            // Try again to delete the directory manually recursing. 
            DeleteDirectory(dir, true);
        } 
    }