Рекурсивный поиск файлов в C# — Будет ли работать такой конструкт?

Отличный вопрос! Давайте подробно разберем рекурсивный поиск файлов в C# и проанализируем возможные конструкции.

Базовые подходы к рекурсивному поиску

1. Классический рекурсивный метод

public static List<string> FindFilesRecursive(string rootPath, string pattern = "*")
{
    var results = new List<string>();
    
    try
    {
        // Добавляем файлы из текущей директории
        results.AddRange(Directory.GetFiles(rootPath, pattern));
        
        // Рекурсивно обходим поддиректории
        foreach (string subDirectory in Directory.GetDirectories(rootPath))
        {
            results.AddRange(FindFilesRecursive(subDirectory, pattern));
        }
    }
    catch (UnauthorizedAccessException)
    {
        // Обработка отсутствия прав доступа
        Console.WriteLine($"Нет доступа к: {rootPath}");
    }
    catch (DirectoryNotFoundException)
    {
        // Обработка несуществующей директории
        Console.WriteLine($"Директория не найдена: {rootPath}");
    }
    
    return results;
}

// Использование
var files = FindFilesRecursive(@"C:MyProject", "*.txt");

2. Использование Stack (итеративный подход)

public static List<string> FindFilesIterative(string rootPath, string pattern = "*")
{
    var results = new List<string>();
    var stack = new Stack<string>();
    stack.Push(rootPath);

    while (stack.Count > 0)
    {
        string currentPath = stack.Pop();
        
        try
        {
            // Добавляем файлы
            results.AddRange(Directory.GetFiles(currentPath, pattern));
            
            // Добавляем поддиректории в стек
            foreach (string subDirectory in Directory.GetDirectories(currentPath))
            {
                stack.Push(subDirectory);
            }
        }
        catch (UnauthorizedAccessException)
        {
            Console.WriteLine($"Нет доступа к: {currentPath}");
        }
    }
    
    return results;
}

Анализ возможных конструктов

Будет ли работать такой код?

// Пример 1: Простая рекурсия
public void FindFiles(string path)
{
    foreach (var file in Directory.GetFiles(path))
        Console.WriteLine(file);
    
    foreach (var dir in Directory.GetDirectories(path))
        FindFiles(dir); // Рекурсивный вызов
}

Ответ: Да, будет работать, но с ограничениями:

  • Нет обработки исключений
  • Риск StackOverflowException при глубокой вложенности
  • Нет фильтрации по шаблону

Потенциальные проблемы и их решение

1. Исключения доступа

public static IEnumerable<string> SafeFindFiles(string rootPath, string pattern = "*")
{
    // Обработка исключений для каждой директории
    IEnumerable<string> files = Enumerable.Empty<string>();
    IEnumerable<string> directories = Enumerable.Empty<string>();
    
    try
    {
        files = Directory.GetFiles(rootPath, pattern);
        directories = Directory.GetDirectories(rootPath);
    }
    catch (UnauthorizedAccessException)
    {
        yield break;
    }
    catch (DirectoryNotFoundException)
    {
        yield break;
    }
    
    foreach (var file in files)
        yield return file;
        
    foreach (var directory in directories)
    {
        foreach (var file in SafeFindFiles(directory, pattern))
            yield return file;
    }
}

2. Глубокая рекурсия

public static List<string> FindFilesWithDepthLimit(string path, string pattern = "*", int maxDepth = 10)
{
    var results = new List<string>();
    FindFilesWithDepthLimit(path, pattern, maxDepth, 0, results);
    return results;
}

private static void FindFilesWithDepthLimit(string path, string pattern, int maxDepth, int currentDepth, List<string> results)
{
    if (currentDepth > maxDepth) return;
    
    try
    {
        results.AddRange(Directory.GetFiles(path, pattern));
        
        foreach (var dir in Directory.GetDirectories(path))
        {
            FindFilesWithDepthLimit(dir, pattern, maxDepth, currentDepth + 1, results);
        }
    }
    catch (Exception ex) when (ex is UnauthorizedAccessException or DirectoryNotFoundException)
    {
        // Игнорируем проблемные директории
    }
}

Современные подходы в .NET

1. Использование EnumerationOptions ( .NET 5+ )

public static IEnumerable<string> FindFilesModern(string rootPath, string pattern = "*")
{
    var options = new EnumerationOptions
    {
        RecurseSubdirectories = true,
        IgnoreInaccessible = true,
        ReturnSpecialDirectories = false,
        MaxRecursionDepth = 32
    };
    
    return Directory.EnumerateFiles(rootPath, pattern, options);
}

2. Асинхронный поиск

public static async Task<List<string>> FindFilesAsync(string rootPath, string pattern = "*")
{
    var results = new List<string>();
    await FindFilesAsync(rootPath, pattern, results);
    return results;
}

private static async Task FindFilesAsync(string path, string pattern, List<string> results)
{
    await Task.Run(() =>
    {
        try
        {
            results.AddRange(Directory.GetFiles(path, pattern));
            
            var directories = Directory.GetDirectories(path);
            var tasks = directories.Select(dir => FindFilesAsync(dir, pattern, results));
            Task.WaitAll(tasks.ToArray());
        }
        catch (Exception ex) when (ex is UnauthorizedAccessException or DirectoryNotFoundException)
        {
            // Логируем, но продолжаем работу
        }
    });
}

Лучшие практики

1. Использование EnumerateFiles вместо GetFiles

// Плохо - загружает все файлы в память сразу
string[] allFiles = Directory.GetFiles(path, "*", SearchOption.AllDirectories);

// Хорошо - ленивое перечисление
IEnumerable<string> files = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories);

2. Обработка разных сценариев

public static class FileSearcher
{
    public static SearchResult FindFiles(SearchOptions options)
    {
        var result = new SearchResult();
        FindFiles(options.RootPath, options, result, 0);
        return result;
    }
    
    private static void FindFiles(string path, SearchOptions options, SearchResult result, int depth)
    {
        if (depth > options.MaxDepth) return;
        
        try
        {
            // Поиск файлов
            var files = Directory.EnumerateFiles(path, options.Pattern);
            foreach (var file in files)
            {
                if (options.Filter == null || options.Filter(file))
                {
                    result.FoundFiles.Add(file);
                }
            }
            
            // Рекурсивный обход директорий
            if (options.IncludeSubdirectories)
            {
                foreach (var dir in Directory.EnumerateDirectories(path))
                {
                    FindFiles(dir, options, result, depth + 1);
                }
            }
        }
        catch (Exception ex)
        {
            result.Errors.Add(new SearchError { Path = path, Exception = ex });
        }
    }
}

public class SearchOptions
{
    public string RootPath { get; set; }
    public string Pattern { get; set; } = "*";
    public bool IncludeSubdirectories { get; set; } = true;
    public int MaxDepth { get; set; } = int.MaxValue;
    public Func<string, bool> Filter { get; set; }
}

public class SearchResult
{
    public List<string> FoundFiles { get; } = new();
    public List<SearchError> Errors { get; } = new();
}

Вывод

Да, рекурсивный поиск файлов в C# будет работать, но важно учитывать:

  1. Обработку исключений - особенно UnauthorizedAccessException
  2. Глубину рекурсии - риск StackOverflowException
  3. Производительность - используйте EnumerateFiles вместо GetFiles
  4. Гибкость - предусмотрите различные сценарии использования

Для большинства случаев в современных версиях .NET рекомендуется использовать встроенные методы с EnumerationOptions, а для сложных сценариев - реализовывать собственные решения с учетом описанных лучших практик.