Как посчитать количество подпапок на VB.NET

РазделVB.NET и WinFormsУровеньСредний Чтение7 минОпубликовано ТегиVB.NETФайловая системаSystem.IO

Число подпапок в каталоге VB.NET отдаёт одной строкой. Но при обходе всего дерева легко попасть на точки соединения, скрытые системные каталоги и бесконечные циклы. Покажем, как считать аккуратно.

Главное. Для одного уровня хватает Directory.GetDirectories(path).Length. Для всего дерева обходите каталоги стеком через EnumerateDirectories, пропускайте закрытые папки и никогда не заходите внутрь точек соединения.

Базовый вариант: GetDirectories

Задача посчитать папки возникает не реже, чем подсчёт файлов: показать в интерфейсе «12 проектов», проверить, сколько архивов по дням накопилось на сервере, решить, пора ли чистить каталог с выгрузками. Класс System.IO.Directory решает её так же, как для файлов: метод GetDirectories возвращает массив путей к подкаталогам, и нужное число равно его длине.

Метод возвращает только непосредственных потомков, то есть первый уровень вложенности. Файлы в результат не входят. Если папка не существует, будет DirectoryNotFoundException, если нет прав, UnauthorizedAccessException.

FolderCount.vbVB.NET
Imports System.IO

Public Module FolderCount
    ' Число подпапок первого уровня
    Public Function GetFolderCount(path As String) As Integer
        Return Directory.GetDirectories(path).Length
    End Function

    ' Число подпапок по маске, например "2010-*"
    Public Function GetFolderCount(path As String, mask As String) As Integer
        Return Directory.GetDirectories(path, mask).Length
    End Function
End Module

Тот же результат через My.Computer.FileSystem

В VB.NET есть ещё пространство My, и многие сопровождаемые проекты пользуются именно им. Метод My.Computer.FileSystem.GetDirectories возвращает коллекцию ReadOnlyCollection, у которой свойство Count даёт нужное значение. Параметр SearchOption.SearchAllSubDirectories включает обход вглубь.

Разница с System.IO в основном стилистическая, но есть нюанс: методы My при рекурсивном обходе тоже строят весь список целиком. На больших деревьях они так же расходуют память и так же падают на первой закрытой папке, поэтому для серьёзного обхода всё равно стоит писать свой цикл.

Скрытые и системные папки

GetDirectories возвращает все подкаталоги, включая скрытые. В корне системного диска это $Recycle.Bin, System Volume Information, Recovery и другие папки, которых пользователь в Проводнике не видит. Если программа показывает «на диске C: 23 папки», а пользователь насчитал 9, причина именно в этом.

Чтобы считать как Проводник, удобно работать через DirectoryInfo: у каждого элемента уже есть свойство Attributes, и дополнительного обращения к диску не нужно. Скрытые и системные каталоги отсекаются проверкой флагов.

VisibleFolders.vbVB.NET
Imports System.IO
Imports System.Linq

Public Module VisibleFolders
    ' Считает только папки, которые видны в Проводнике по умолчанию
    Public Function CountVisible(path As String) As Integer
        Dim hidden = FileAttributes.Hidden Or FileAttributes.System
        Return New DirectoryInfo(path).EnumerateDirectories().
            Count(Function(d) (d.Attributes And hidden) = 0)
    End Function
End Module

Точки соединения и бесконечные циклы

Начиная с Windows Vista в профиле пользователя и в корне диска лежат точки соединения (junction) для совместимости со старыми программами: «Application Data», «Мои документы», «Documents and Settings». Они выглядят как папки, но ведут в другое место. Внутри «Application Data» лежит ещё одна «Application Data», которая ведёт туда же, и наивный рекурсивный обход уходит в цикл, пока не упрётся в предельную длину пути.

Такие папки помечены атрибутом ReparsePoint. Правильная стратегия: считать их, если они нужны в статистике, но не заходить внутрь. Тогда и число совпадёт с реальной структурой, и обход гарантированно завершится. На те же грабли наступают символические ссылки на каталоги, которые создаёт mklink /D.

FolderTreeCount.vbVB.NET
Imports System.IO
Imports System.Collections.Generic

Public Module FolderTreeCount
    ' Считает все подпапки дерева, не заходя в точки соединения
    Public Function CountTree(root As String) As Long
        Dim total As Long = 0
        Dim pending As New Stack(Of DirectoryInfo)
        pending.Push(New DirectoryInfo(root))
        While pending.Count > 0
            Dim current = pending.Pop()
            Try
                For Each d In current.EnumerateDirectories()
                    total += 1
                    ' junction и symlink считаем, но не обходим
                    If (d.Attributes And FileAttributes.ReparsePoint) = 0 Then
                        pending.Push(d)
                    End If
                Next
            Catch ex As UnauthorizedAccessException
                ' нет прав на папку: пропускаем её содержимое
            End Try
        End While
        Return total
    End Function
End Module
Обход через стек вместо рекурсии избавляет от StackOverflowException на очень глубоких деревьях, а EnumerateDirectories (.NET 4) не строит промежуточных массивов.

Пустые папки и практическое применение

Частая практическая задача рядом с подсчётом: найти пустые каталоги, которые остались после удаления файлов. Папка пуста, если Directory.EnumerateFileSystemEntries(path).Any() возвращает False. Метод Any останавливается на первом найденном элементе, поэтому проверка быстрая даже для папок с тысячами файлов.

Другой сценарий: контроль ротации. Если программа каждый день создаёт папку с датой в имени, подсчёт по маске «2010-*» сразу покажет, сколько архивов лежит за год, и позволит удалить старые.

  • для отчёта пользователю считайте как Проводник: без скрытых и системных папок;
  • для служебной статистики считайте всё, но не заходите в ReparsePoint;
  • перед удалением пустых папок обходите дерево снизу вверх, иначе родитель окажется непустым.

Производительность на сетевых дисках

На локальном SSD подсчёт тысяч папок занимает доли секунды, а на сетевой шаре каждое обращение к каталогу это запрос по сети. Дерево из нескольких десятков тысяч папок может обходиться минуты. Выносите такой обход в фоновый поток, показывайте прогресс и дайте пользователю возможность отменить операцию.

Как это делают сегодня

В .NET Core 2.1 и новее подсчёт дерева делается через EnumerationOptions: RecurseSubdirectories, IgnoreInaccessible и AttributesToSkip = FileAttributes.ReparsePoint Or FileAttributes.Hidden Or FileAttributes.System. В .NET 6 появилось свойство LinkTarget у FileSystemInfo, по которому удобно отличать ссылки от обычных папок. Длинные пути больше 260 символов поддерживаются без префикса \\?\.

Вопросы и ответы

Почему программа насчитала больше папок, чем видно в Проводнике?

GetDirectories возвращает скрытые и системные каталоги. Отфильтруйте их по атрибутам Hidden и System, тогда результат совпадёт с Проводником.

Почему рекурсивный обход профиля пользователя зацикливается?

В профиле есть точки соединения вроде «Application Data», которые ссылаются сами на себя. Проверяйте атрибут ReparsePoint и не заходите в такие папки.

Что быстрее: GetDirectories или EnumerateDirectories?

Для одной папки разница почти незаметна. На больших деревьях EnumerateDirectories выигрывает: результаты приходят сразу и не нужно хранить весь массив в памяти.

Читайте также