При использовании 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 шт):

Автор решения: aepot

Чтобы 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);

Теперь будет работать как надо.

→ Ссылка
Автор решения: InterceptorTSK

Наипримитивнейшая запредельно простейшая реализация поиска путей, таких где лежат нужные файлы с нужными расширениями.

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().

Далее. Почему на словаре, а не на хеш-сете? Потому что в словарь помимо пути (ключа), можно добавить что либо по найденным файлам, например их найденное количество в директории, или же список этих самых файлов, и так далее. Впрочем если словарь не нужен, то можно легко уйти в хеш-сет.

Проблемы:

Можно долго и нудно писать про то, что это всё можно оптимизировать через параллельное исполнение, и оно действительно ускорит поиск. Можно написать про оптимизацию поиска строки пути через сравнение "с конца строки". И так далее. Это всё ерунда. Потому что на самом деле сильно проще вы́читать мфт-зону с диска, её и распарсить. Поиск через мфт-зону работает на порядок быстрее. Сильно быстрее. Это в порядке общих соображений.

→ Ссылка