При использовании HashSet<> с объектами класса DirectoryInfo C# .NET возникает неприятная ситуация
На вход поступает некий список файлов List<> files. Необходимо вернуть список всех директорий List<>, которые содержат файлы с расширением .mp3 и .wav. Каждую директорию нужно вернуть только один раз, порядок значения не имеет.
Сразу подумал, что HashSet<> здесь прекрасно подойдет для отсечения дубликатов, после просто конвертировать ToList и готово.
public static List<DirectoryInfo> GetAlbums(List<FileInfo> files)
{
HashSet<DirectoryInfo> albums = new HashSet<DirectoryInfo>();
foreach (var file in files)
if (file.Extension == ".mp3" || file.Extension == ".wav")
albums.Add(file.Directory);
return albums.ToList;
}
Но повторяющиеся объекты все равно добавляются. Насколько я понял это потому, что они типа DirectoryInfo, и они не переопределяют метод hashCode() который использует HashSet<> для сортировки хэшем, и соответственно проверки на повторяющиеся объекты.
Конвертировать к string, чтобы дубликаты отсеивались и потом перебирать весь HashSet string добавляя в List DirectoryInfo как-то слишком примитивно и затратно.
albums.Add(file.Directory.ToString());
foreach (var album in albums)
list.Add(new DirectoryInfo(album));
return list;
Если добавлять сразу в List DirectoryInfo и для отсечения повторов добавить в if дополнительное условие:
(!albums.Contains(file.Directory)
Так тоже не работает, if пропускает дубликаты, потому что Метод Contains() со ссылочными типами работает по другому (как я понял, может и не из-за этого).
В общем объясните почему происходят такие ситуации? Как их можно исправить, работая именно с HashSet<> или List<>, не прибегая к описанным мной действиям и LINQ?
Ответы (2 шт):
Чтобы HashSet<T> работал, тип данных должен реализовать интерфейс IEquitable<T>, то есть должно быть возможность сравнения. DirectoryInfo не реализует, а значит сравнение происходит только ссылке как у обычного объекта.
Чтобы это обойти, нужно создать свой компарер, например сравнивающий пути к каталогам.
public class DirectoryInfoComparer : IEqualityComparer<DirectoryInfo>
{
public bool Equals(DirectoryInfo left, DirectoryInfo right)
{
return left.FullName == right.FullName;
}
public int GetHashCode(DirectoryInfo di)
{
return di.FullName.GetHashCode();
}
}
И передать его в конструктор хэшсета
DirectoryInfoComparer comparer = new();
HashSet<DirectoryInfo> albums = new(comparer);
Теперь будет работать как надо.
Наипримитивнейшая запредельно простейшая реализация поиска путей, таких где лежат нужные файлы с нужными расширениями.
public static System.Collections.Generic.Dictionary<string, object> Pathes
= new System.Collections.Generic.Dictionary<string, object>();
public static void GetDirectories(string directory)
{
var files = System.IO.Directory.EnumerateFiles(directory);
foreach (var file in files)
if (file.EndsWith(".flac", System.StringComparison.OrdinalIgnoreCase) ||
file.EndsWith(".wav", System.StringComparison.OrdinalIgnoreCase) ||
file.EndsWith(".mp3", System.StringComparison.OrdinalIgnoreCase))
{
Pathes.Add(directory, null);
break;
}
var directories = System.IO.Directory.EnumerateDirectories(directory);
foreach (var subDirectory in directories)
try { GetDirectories(subDirectory); }
catch { }
}
Использование
public static void Main()
{
GetDirectories(@"C:\");
foreach (var path in Pathes)
System.Console.WriteLine(path);
}
Автор зачем то пользует System.IO.DirectoryInfo, но этот объект содержит много полей и кучу не нужного автору. И вычитывать DirectoryInfo не нужно. Нужно вычитывать строки. А строки по-умолчанию имеют реализованные GetHashCode() и Equals().
Далее. Почему на словаре, а не на хеш-сете? Потому что в словарь помимо пути (ключа), можно добавить что либо по найденным файлам, например их найденное количество в директории, или же список этих самых файлов, и так далее. Впрочем если словарь не нужен, то можно легко уйти в хеш-сет.
Проблемы:
Можно долго и нудно писать про то, что это всё можно оптимизировать через параллельное исполнение, и оно действительно ускорит поиск. Можно написать про оптимизацию поиска строки пути через сравнение "с конца строки". И так далее. Это всё ерунда. Потому что на самом деле сильно проще вы́читать мфт-зону с диска, её и распарсить. Поиск через мфт-зону работает на порядок быстрее. Сильно быстрее. Это в порядке общих соображений.