如果目录内容在迭代过程中发生变化,Directory.EnumerateFiles 会发生什么情况?

What happens with Directory.EnumerateFiles if directory content changes during iteration?

我读过关于 Directory.EnumerateFiles and Directory.GetFiles 之间差异的讨论。

我知道他们在内部都使用 System.IO.FileSystemEnumerableFactory.CreateFileNameIterator()

区别在于EnumerateFiles可能使用了延迟执行(惰性),而GetFiles()做了ToArray,所以函数已经执行了。

但是如果在迭代过程中将文件和文件夹添加到字典中会发生什么情况。迭代是否只会迭代 EnumerateFiles() 期间存在的项目?

更糟糕的是:如果文件在迭代过程中被删除会发生什么:它们还会被迭代吗?

只有一种检查方式:

Directory.CreateDirectory(@"c:\Temp");
File.Create(@"c:\Temp\a.txt").Close();
File.Create(@"c:\Temp\b.txt").Close();
File.Create(@"c:\Temp\c.txt").Close();
foreach (var f in Directory.EnumerateFiles(@"c:\Temp"))
{
    Console.WriteLine(f);
    //Let's delete a file
    File.Delete(@"c:\Temp\c.txt");
    //Let's create a new file
    File.Create(@"c:\Temp\d.txt").Close();
}

最初 C:\Temp 包含 3 个文件:a.txt、b.txt 和 c.txt。在迭代过程中,其中一个文件被删除,一个被创建。最后,C:\Temp 包含以下文件:a.txt、b.txt 和 d.txt 但是,在控制台中您将看到原始内容该目录即:

c:\Temp\a.txt
c:\Temp\b.txt
c:\Temp\c.txt

感谢米哈尔·科莫罗夫斯基。然而,当我自己尝试他的解决方案时,我看到了 Directory.EnumerateFiles 和 Directory.GetFiles():

之间的显着区别
Directory.CreateDirectory(@"c:\MyTest");
// Create fies: b c e
File.CreateText(@"c:\MyTest\b.txt").Dispose();
File.CreateText(@"c:\MyTest\c.txt").Dispose();
File.CreateText(@"c:\MyTest\e.txt").Dispose();

string[] files = Directory.GetFiles(@"c:\MyTest");
var fileEnumerator = Directory.EnumerateFiles(@"c:\MyTest");

// delete file c; create file a d f
File.Delete(@"c:\MyTest\c.txt");
File.CreateText(@"c:\MyTest\a.txt").Dispose();
File.CreateText(@"c:\MyTest\d.txt").Dispose();
File.CreateText(@"c:\MyTest\f.txt").Dispose();

Console.WriteLine("Result from Directory.GetFiles");
foreach (var file in files) Console.WriteLine(file);
Console.WriteLine("Result from Directory.EnumerateFiles");
foreach (var file in fileEnumerator) Console.WriteLine(file);

这将给出不同的输出。

Result from Directory.GetFiles
c:\MyTest\b.txt
c:\MyTest\c.txt
c:\MyTest\e.txt
Result from Directory.EnumerateFiles
c:\MyTest\b.txt
c:\MyTest\d.txt
c:\MyTest\e.txt
c:\MyTest\f.txt

结果:

  • GetFiles 仍然看到旧文件:B C E 正如预期的那样
  • EnumerateFiles 看到了新文件 D 和 F。它正确地跳过了已删除的文件 C,但错过了新文件 A。

所以 EnumerateFiles 和 GetFiles 在用法上的区别不仅仅是性能。

  • GetFiles returns 调用函数时文件夹中的文件。这是意料之中的,因为它只是对字符串集合的枚举
  • EnumerateFiles 正确地跳过了删除的文件,但没有看到所有添加的文件。如果文件夹在枚举时发生变化,结果是相当不确定的。

因此,如果您希望在枚举时文件夹发生变化,请仔细选择所需的功能

  • 期望 GetFiles 查看已删除的文件
  • 预计 EnumerateFiles 会遗漏一些新文件。

我做了一个不同的实验,因为我对文件枚举速度慢的情况感兴趣,而在枚举目录中创建了更多文件。例如,如果枚举循环内有一个 SemaphoreSlim.WaitAsync(出于节流目的),则可能会发生枚举缓慢的情况。下面的实验首先从目标目录中删除所有文件,然后创建特定数量的初始文件,然后开始以 100 毫秒的延迟枚举文件,而另一个异步工作流以每 150 毫秒一个文件的速率创建更多文件.枚举器会看到新创建的文件吗?

static async Task Main(string[] args)
{
    const string FOLDER_PATH = @"C:\DirectoryEnumerateFilesTest";
    const int FILES_COUNT = 10;
    Console.WriteLine($"Deleting files");
    DeleteAllFiles(FOLDER_PATH);
    Console.WriteLine($"Creating files");
    await CreateFiles(FOLDER_PATH, startIndex: 1, filesCount: FILES_COUNT, delay: 0);
    Console.WriteLine($"Enumerating files while creating more files");
    var filePaths = Directory.EnumerateFiles(FOLDER_PATH);
    var cts = new CancellationTokenSource();
    var producer = CreateFiles(FOLDER_PATH,
        startIndex: 501, filesCount: 100, delay: 150, cts.Token);
    var enumeratedCount = 0;
    foreach (var filePath in filePaths)
    {
        Console.WriteLine($"Enumerated:   {Path.GetFileName(filePath)}");
        await Task.Delay(100);
        enumeratedCount++;
    }
    Console.WriteLine($"Total files enumerated: {enumeratedCount:#,0}");
    cts.Cancel();
    await producer;
}

private static void DeleteAllFiles(string folderPath)
{
    int count = 0;
    foreach (var filePath in Directory.GetFiles(folderPath))
    {
        File.Delete(filePath);
        Console.WriteLine($"File deleted: {Path.GetFileName(filePath)}");
        count++;
    }
    Console.WriteLine($"Total files deleted: {count:#,0}");
}

private static async Task CreateFiles(string folderPath,
    int startIndex, int filesCount, int delay, CancellationToken token = default)
{
    int count = 0;
    foreach (var i in Enumerable.Range(startIndex, filesCount))
    {
        var delayTask = Task.Delay(delay, token);
        await Task.WhenAny(delayTask);
        if (delayTask.IsCanceled) break;
        var fileName = $"File-{i:000}.txt";
        var filePath = Path.Combine(folderPath, fileName);
        File.WriteAllText(filePath, "Content");
        count++;
        Console.WriteLine($"File created: {fileName}");
    }
    Console.WriteLine($"Total files created: {count:#,0}");
}

答案是:这取决于初始文件的数量和文件名的长度。阈值为 大约 50 个初始文件 ,但当文件的文件名较长时,阈值会变小。枚举最终会停止,前提是枚举器比文件生成器工作得更快,在这种情况下,许多文件将保持未被观察到(通常约为 20 个)。

这是上述 FILES_COUNT = 10 实验的输出(意味着创建枚举器时有 10 个现有文件)。

Deleting files
Total files deleted: 0
Creating files
File created: File-001.txt
File created: File-002.txt
File created: File-003.txt
File created: File-004.txt
File created: File-005.txt
File created: File-006.txt
File created: File-007.txt
File created: File-008.txt
File created: File-009.txt
File created: File-010.txt
Total files created: 10
Enumerating files while creating more files
Enumerated:   File-001.txt
Enumerated:   File-002.txt
File created: File-501.txt
Enumerated:   File-003.txt
File created: File-502.txt
Enumerated:   File-004.txt
Enumerated:   File-005.txt
File created: File-503.txt
Enumerated:   File-006.txt
File created: File-504.txt
Enumerated:   File-007.txt
Enumerated:   File-008.txt
File created: File-505.txt
Enumerated:   File-009.txt
File created: File-506.txt
Enumerated:   File-010.txt
Total files enumerated: 10
File created: File-507.txt
Total files created: 7

10 个文件太少,因此 none 之后创建的文件被枚举器观察到了。