42

.NET 4.0 中有一个很好的新方法,可以通过枚举以流的方式获取目录中的文件。

这里的问题是,如果希望枚举所有文件,可能事先不知道哪些文件或文件夹受访问保护,并可能引发 UnauthorizedAccessException。

要重现,只需运行以下片段:

foreach (var file in Directory.EnumerateFiles(@"c:\", "*", SearchOption.AllDirectories))
{
   // whatever
}

在此 .NET 方法存在之前,可以通过在字符串数组返回方法上实现递归迭代器来实现大致相同的效果。但它并不像新的 .NET 方法那么懒惰。

那么该怎么办?使用此方法时,UnauthorizedAccessException 是否可以被抑制或成为现实?

在我看来,该方法应该具有接受处理任何异常的动作的重载。

4

6 回答 6

32

我无法让上述工作,但这是我的实现,我已经在“Win7”框上的 c:\users 上对其进行了测试,因为如果有所有这些“讨厌的”目录:

SafeWalk.EnumerateFiles(@"C:\users", "*.jpg", SearchOption.AllDirectories).Take(10)

班级:

public static class SafeWalk
{
    public static IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOpt)
    {   
        try
        {
            var dirFiles = Enumerable.Empty<string>();
            if(searchOpt == SearchOption.AllDirectories)
            {
                dirFiles = Directory.EnumerateDirectories(path)
                                    .SelectMany(x => EnumerateFiles(x, searchPattern, searchOpt));
            }
            return dirFiles.Concat(Directory.EnumerateFiles(path, searchPattern));
        }
        catch(UnauthorizedAccessException ex)
        {
            return Enumerable.Empty<string>();
        }
    }
}
于 2011-05-10T23:12:10.987 回答
9

上述答案的问题是不处理子目录中的异常。这将是处理这些异常的更好方法,因此您可以从所有子目录中获取所有文件,除了那些抛出访问异常的文件:

    /// <summary>
    /// A safe way to get all the files in a directory and sub directory without crashing on UnauthorizedException or PathTooLongException
    /// </summary>
    /// <param name="rootPath">Starting directory</param>
    /// <param name="patternMatch">Filename pattern match</param>
    /// <param name="searchOption">Search subdirectories or only top level directory for files</param>
    /// <returns>List of files</returns>
    public static IEnumerable<string> GetDirectoryFiles(string rootPath, string patternMatch, SearchOption searchOption)
    {
        var foundFiles = Enumerable.Empty<string>();

        if (searchOption == SearchOption.AllDirectories)
        {
            try
            {
                IEnumerable<string> subDirs = Directory.EnumerateDirectories(rootPath);
                foreach (string dir in subDirs)
                {
                    foundFiles = foundFiles.Concat(GetDirectoryFiles(dir, patternMatch, searchOption)); // Add files in subdirectories recursively to the list
                }
            }
            catch (UnauthorizedAccessException) { }
            catch (PathTooLongException) {}
        }

        try
        {
            foundFiles = foundFiles.Concat(Directory.EnumerateFiles(rootPath, patternMatch)); // Add files from the current directory
        }
        catch (UnauthorizedAccessException) { }

        return foundFiles;
    }
于 2013-12-21T13:58:56.707 回答
6

我知道这MoveNext是引发异常的原因。

我尝试编写一个安全遍历序列并尝试忽略MoveNext异常的方法。但是我不确定MoveNext当它抛出异常时是否推进位置,所以这也可能是无限循环。这也是一个坏主意,因为我们将依赖实现细节。

但这太有趣了!

public static IEnumerable<T> SafeWalk<T> (this IEnumerable<T> source)
{
    var enumerator = source.GetEnumerator();
    bool? hasCurrent = null;

    do {
        try {
            hasCurrent = enumerator.MoveNext();
        } catch {
            hasCurrent = null; // we're not sure
        }

        if (hasCurrent ?? false) // if not sure, do not return value
            yield return enumerator.Current;

    } while (hasCurrent ?? true); // if not sure, continue walking
}

foreach (var file in Directory.EnumerateFiles("c:\\", "*", SearchOption.AllDirectories)
                              .SafeWalk())
{
    // ...
}

这仅在以下条件关于框架对此迭代器的实现为真时才有效(参见FileSystemEnumerableIterator<TSource>Reflector 以供参考):

  • MoveNext失败时提升自己的位置;
  • MoveNext最后一个元素失败时,后续调用将返回false而不是抛出异常;
  • 对于不同版本的 .NET Framework,此行为是一致的;
  • 我没有犯任何逻辑或语法错误。

即使它有效,也请不要在生产中使用它!
但我真的很想知道是不是这样。

于 2011-02-23T23:40:44.270 回答
2

发布为答案,因为我没有代表添加评论,更不用说编辑现有答案了。我的要求是尽量减少内存分配、冗余变量,并让系统对目录进行一次枚举。

static IEnumerable<string> FindFiles(string path, string filter = "*", bool recursive = false)
{
    IEnumerator<string> fEnum;
    try
    {
        fEnum = Directory.EnumerateFiles(path, filter, recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).GetEnumerator();
    }
    catch (UnauthorizedAccessException) { yield break; }
    while (true)
    {
        try { if (!fEnum.MoveNext()) break; }
        catch (UnauthorizedAccessException) { continue; }
        yield return fEnum.Current;
    }
}

Dan Bechard 在评论中提到:

不幸的是,MoveNext() 在抛出异常时不会提前其位置。

这可能已在较新版本的 .Net 或 Windows 10 版本中得到修复?我在 Windows 10 上的 .NET 5.0 中没有这个问题。通过搜索我的整个系统驱动器进行了测试。


在 VB.NET 中:

Public Iterator Function FindFiles(path As String, Optional filter As String = "*", Optional recursive As Boolean = False) As IEnumerable(Of String)

    Dim fEnum As IEnumerator(Of String)
    Dim searchDepth = If(recursive, SearchOption.AllDirectories, SearchOption.TopDirectoryOnly)

    Try
        fEnum = Directory.EnumerateFiles(path, filter, searchDepth).GetEnumerator()
    Catch uae As UnauthorizedAccessException
        Return
    End Try

    Do While True
        Try
            If Not fEnum.MoveNext() Then Exit Do
            Yield fEnum.Current
        Catch uae As UnauthorizedAccessException

        End Try

    Loop

End Function
于 2020-11-29T11:29:07.917 回答
1

我迟到了,但我建议改用可观察模式:

public class FileUtil
{
  private static void FindFiles_(string path, string pattern,
    SearchOption option, IObserver<string> obs, CancellationToken token)
  {
    try
    {
      foreach (var file in Directory.EnumerateFiles(path, pattern,
        SearchOption.TopDirectoryOnly))
      {
        if (token.IsCancellationRequested) break;
        obs.OnNext(file);
      }

      if (option != SearchOption.AllDirectories) return;

      foreach (var dir in Directory.EnumerateDirectories(path, "*", 
        SearchOption.TopDirectoryOnly))
      {
        FindFiles_(dir, pattern, option, obs, token);
      }
    }
    catch (UnauthorizedAccessException) { }
    catch (PathTooLongException) { }
    catch (IOException) { }
    catch (Exception err) { obs.OnError(err); }
  }

  public static IObservable<string> GetFiles(string root, string pattern,
    SearchOption option)
  {
    return Observable.Create<string>(
      (obs, token) =>
        Task.Factory.StartNew(
          () =>
          {
            FindFiles_(root, pattern, option, obs, token);
            obs.OnCompleted();
          },
          token));
  }
}
于 2018-06-19T14:54:34.127 回答
0

我自己实现了一个围绕这个工作的类,因为以前的答案似乎没有做我想做的事。这只是跳过它无法访问的所有文件和文件夹,并返回它可以访问的所有文件。

public static class SafeWalk
{
    public static IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOpt)
    {
        if (searchOpt == SearchOption.TopDirectoryOnly)
        {
            return Directory.EnumerateFiles(path, searchPattern, SearchOption.TopDirectoryOnly);
        }

        List<string> folders = new List<string>() { path };
        int folCount = 1;
        List<string> files = new List<string>() { };

        for (int i = 0; i < folCount; i++)
        {
            try
            {
                foreach (var newDir in Directory.EnumerateDirectories(folders[i], "*", SearchOption.TopDirectoryOnly))
                {
                    folders.Add(newDir);
                    folCount++;
                    try
                    {

                        foreach (var file in Directory.EnumerateFiles(newDir, searchPattern))
                        {
                            files.Add(file);
                        }
                    } catch (UnauthorizedAccessException)
                    {
                        // Failed to read a File, skipping it.
                    }
                }
            }
            catch (UnauthorizedAccessException)
            {
                // Failed to read a Folder, skipping it.
                continue;
            }
        }
        return files;
    }
}

可以像常规的 EnumerateFiles 函数一样使用,只需使用 SafeWalk.EnumerateFiles(...) 而不是 Dictionary.EnumerateFiles(...)

于 2020-05-04T18:13:37.530 回答