C# Список всех листовых подкаталогов с помощью EnumerateDirectories

Всем доброе утро, у меня есть папка, которая содержит тысячи подкаталогов на разной глубине. Мне нужно перечислить все каталоги, которые не содержат подкаталогов (пресловутый «конец строки»). Хорошо, если они содержат файлы. Есть ли способ сделать это с помощью EnumerateDirectories?

Например, если полностью рекурсивный EnumerateDirectories возвратил:

/files/
/files/q
/files/q/1
/files/q/2
/files/q/2/examples
/files/7
/files/7/eb
/files/7/eb/s
/files/7/eb/s/t

Меня интересует только:

/files/q/1
/files/q/2/examples
/files/7/eb/s/t

person Sam2S    schedule 23.07.2013    source источник


Ответы (2)


Это должно работать:

var folderWithoutSubfolder = Directory.EnumerateDirectories(root, "*.*", SearchOption.AllDirectories)
     .Where(f => !Directory.EnumerateDirectories(f, "*.*", SearchOption.TopDirectoryOnly).Any());
person Tim Schmelter    schedule 23.07.2013

Если вы хотите избежать двойного вызова EnumerateDirectories() для каждого каталога, вы можете реализовать это следующим образом:

public IEnumerable<string> EnumerateLeafFolders(string root)
{
    bool anySubfolders = false;

    foreach (var subfolder in Directory.EnumerateDirectories(root))
    {
        anySubfolders = true;

        foreach (var leafFolder in EnumerateLeafFolders(subfolder))
            yield return leafFolder;
    }

    if (!anySubfolders)
        yield return root;
}

Я провел несколько временных тестов, и для меня этот подход более чем в два раза быстрее, чем использование подхода Linq.

Я провел этот тест, используя релизную сборку, запущенную вне любого отладчика. Я запускал его на SSD, содержащем большое количество папок — общее количество папок LEAF было 25035.

Мои результаты для ВТОРОГО запуска программы (первый запуск был для предварительного прогрева дискового кеша):

Calling Using linq.  1 times took 00:00:08.2707813
Calling Using yield. 1 times took 00:00:03.6457477
Calling Using linq.  1 times took 00:00:08.0668787
Calling Using yield. 1 times took 00:00:03.5960438
Calling Using linq.  1 times took 00:00:08.1501002
Calling Using yield. 1 times took 00:00:03.6589386
Calling Using linq.  1 times took 00:00:08.1325582
Calling Using yield. 1 times took 00:00:03.6563730
Calling Using linq.  1 times took 00:00:07.9994754
Calling Using yield. 1 times took 00:00:03.5616040
Calling Using linq.  1 times took 00:00:08.0803573
Calling Using yield. 1 times took 00:00:03.5892681
Calling Using linq.  1 times took 00:00:08.1216921
Calling Using yield. 1 times took 00:00:03.6571429
Calling Using linq.  1 times took 00:00:08.1437973
Calling Using yield. 1 times took 00:00:03.6606362
Calling Using linq.  1 times took 00:00:08.0058955
Calling Using yield. 1 times took 00:00:03.6477621
Calling Using linq.  1 times took 00:00:08.1084669
Calling Using yield. 1 times took 00:00:03.5875057

Как видите, использование подхода yield значительно быстрее. (Возможно, потому что он не перечисляет каждую папку дважды.)

Мой тестовый код:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;

namespace Demo
{
    class Program
    {
        private void run()
        {
            string root = "F:\\TFROOT";

            Action test1 = () => leafFolders1(root).Count();
            Action test2 = () => leafFolders2(root).Count();

            for (int i = 0; i < 10; ++i)
            {
                test1.TimeThis("Using linq.");
                test2.TimeThis("Using yield.");
            }
        }

        static void Main()
        {
            new Program().run();
        }

        static IEnumerable<string> leafFolders1(string root)
        {
            var folderWithoutSubfolder = Directory.EnumerateDirectories(root, "*.*", SearchOption.AllDirectories)
                 .Where(f => !Directory.EnumerateDirectories(f, "*.*", SearchOption.TopDirectoryOnly).Any());

            return folderWithoutSubfolder;
        }

        static IEnumerable<string> leafFolders2(string root)
        {
            bool anySubfolders = false;

            foreach (var subfolder in Directory.EnumerateDirectories(root))
            {
                anySubfolders = true;

                foreach (var leafFolder in leafFolders2(subfolder))
                    yield return leafFolder;
            }

            if (!anySubfolders)
                yield return root;
        }
    }

    static class DemoUtil
    {
        public static void Print(this object self)
        {
            Console.WriteLine(self);
        }

        public static void Print(this string self)
        {
            Console.WriteLine(self);
        }

        public static void Print<T>(this IEnumerable<T> self)
        {
            foreach (var item in self)
                Console.WriteLine(item);
        }

        public static void TimeThis(this Action action, string title, int count = 1)
        {
            var sw = Stopwatch.StartNew();

            for (int i = 0; i < count; ++i)
                action();

            Console.WriteLine("Calling {0} {1} times took {2}",  title, count, sw.Elapsed);
        }
    }
}
person Matthew Watson    schedule 23.07.2013
comment
EnumerateDirectories оценивается лениво, поэтому дополнительный вызов, сделанный в ответе Тима, довольно дешев. Когда я сравнивал ваш код с кодом Тима, Тим работал чуть меньше половины времени. Я предполагаю, что это связано с тем, что рекурсивное использование итератора добавляет много накладных расходов. - person Brian; 24.07.2013
comment
@Brian Вы запускали тест несколько раз, чтобы устранить артефакты, вызванные кэшированием диска при первом прогоне? - person Matthew Watson; 24.07.2013
comment
Да. Я запускал тесты пару сотен раз, прежде чем начать, скомпилировал с оптимизацией и запускал каждый тест один раз (чтобы избежать артефактов джиттера) перед запуском таймеров. - person Brian; 24.07.2013
comment
@ Брайан Я вижу, что мой метод работает более чем в два раза быстрее. Я прикреплю свой тестовый код, чтобы вы могли его увидеть. Убедитесь, что вы запустили его ВНЕ отладчика (иначе он будет работать в режиме отладки, даже если он скомпилирован как релиз) - person Matthew Watson; 24.07.2013
comment
Я попытался изменить ваш код, чтобы он не использовал рекурсию (см. pastebin.com/eZdbM0i6). Когда я протестировал его, он работал примерно на 10% быстрее, чем у Тима. Эти преимущества в основном не исчезли, если я добавил дополнительный вызов Directory.EnumerateDirectories, поэтому я думаю, что они связаны с накладными расходами LINQ. В любом случае, возможно, что посторонние вызовы EnumerateDirectories обходятся вашей системе дороже. - person Brian; 24.07.2013
comment
@Brian Вы пробовали точный код, который я разместил выше (но, конечно, изменив корневой путь на что-то подходящее)? Моя система не является чем-то необычным (Windows 8.0 x64), и я вижу аналогичные результаты на других системах. - person Matthew Watson; 24.07.2013
comment
Должно быть, я сделал что-то не так, потому что у вас быстрее. И мое изменение дало (я разместил неверный URL-адрес - см. pastebin.com/E7k2xgcW) незначительную пользу. - person Brian; 24.07.2013