
Как посчитать количество подпапок на VB.NET
Число подпапок в каталоге VB.NET отдаёт одной строкой. Но при обходе всего дерева легко попасть на точки соединения, скрытые системные каталоги и бесконечные циклы. Покажем, как считать аккуратно.
Базовый вариант: GetDirectories
Задача посчитать папки возникает не реже, чем подсчёт файлов: показать в интерфейсе «12 проектов», проверить, сколько архивов по дням накопилось на сервере, решить, пора ли чистить каталог с выгрузками. Класс System.IO.Directory решает её так же, как для файлов: метод GetDirectories возвращает массив путей к подкаталогам, и нужное число равно его длине.
Метод возвращает только непосредственных потомков, то есть первый уровень вложенности. Файлы в результат не входят. Если папка не существует, будет DirectoryNotFoundException, если нет прав, UnauthorizedAccessException.
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, и дополнительного обращения к диску не нужно. Скрытые и системные каталоги отсекаются проверкой флагов.
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.
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Пустые папки и практическое применение
Частая практическая задача рядом с подсчётом: найти пустые каталоги, которые остались после удаления файлов. Папка пуста, если Directory.EnumerateFileSystemEntries(path).Any() возвращает False. Метод Any останавливается на первом найденном элементе, поэтому проверка быстрая даже для папок с тысячами файлов.
Другой сценарий: контроль ротации. Если программа каждый день создаёт папку с датой в имени, подсчёт по маске «2010-*» сразу покажет, сколько архивов лежит за год, и позволит удалить старые.
- для отчёта пользователю считайте как Проводник: без скрытых и системных папок;
- для служебной статистики считайте всё, но не заходите в ReparsePoint;
- перед удалением пустых папок обходите дерево снизу вверх, иначе родитель окажется непустым.
Производительность на сетевых дисках
На локальном SSD подсчёт тысяч папок занимает доли секунды, а на сетевой шаре каждое обращение к каталогу это запрос по сети. Дерево из нескольких десятков тысяч папок может обходиться минуты. Выносите такой обход в фоновый поток, показывайте прогресс и дайте пользователю возможность отменить операцию.
Как это делают сегодня
Вопросы и ответы
Почему программа насчитала больше папок, чем видно в Проводнике?
GetDirectories возвращает скрытые и системные каталоги. Отфильтруйте их по атрибутам Hidden и System, тогда результат совпадёт с Проводником.
Почему рекурсивный обход профиля пользователя зацикливается?
В профиле есть точки соединения вроде «Application Data», которые ссылаются сами на себя. Проверяйте атрибут ReparsePoint и не заходите в такие папки.
Что быстрее: GetDirectories или EnumerateDirectories?
Для одной папки разница почти незаметна. На больших деревьях EnumerateDirectories выигрывает: результаты приходят сразу и не нужно хранить весь массив в памяти.