Что лучше для создания отдельных структур данных: HashSet или Linq Distinct()?

Мне интересно, могу ли я получить консенсус относительно того, какой метод лучше подходит для создания отдельного набора элементов: a C# HashSet или с помощью IEnumerable .Distinct(), который является функцией Linq?

Скажем, я просматриваю результаты запроса из базы данных с помощью DataReader, а мои параметры - добавлять объекты, которые я создаю, в List<SomeObject> или в HashSet<SomeObject>. С опцией List я завершаю что-то вроде:

myList = myList.Distinct().ToList<SomeObject>();

С HashSet, я понимаю, что добавление к нему элементов само по себе заботится о не дублировании, предполагая, что вы переопределили методы GetHashCode() и Equals() в SomeObject. Я в основном обеспокоен факторами риска и эффективности параметров.

Спасибо.

Ответы

Ответ 1

"Лучше" - это сложное слово для использования - это может означать так много разных вещей для разных людей.

Для удобства чтения я бы пошел на Distinct(), поскольку я лично считаю это более понятным.

Для производительности я подозреваю, что реализация HashSet, созданная вручную, может выполняться умеренно быстрее, но я сомневаюсь, что это будет совсем иначе, поскольку внутренняя реализация Distinct, без сомнения, сама использует некоторую форму хэширования.

Для того, что я считаю "лучшей" реализацией... Я думаю, что вы должны использовать Distinct, но каким-то образом переместите это на уровень базы данных, то есть измените базовую базу данных SELECT перед тем, как заполнить DataReader.

Ответ 2

Энтони Пегем сказал это лучше всего. Используйте правильный инструмент для работы. Я говорю об этом, потому что Distinct или HashSet не так сильно отличается от производительности. Используйте HashSet, когда коллекция всегда должна содержать только отдельные элементы. Он также говорит программисту, что вы не можете добавить к нему дубликаты. Используйте обычные List<T> и .Distinct() ont, когда вам придется добавлять дубликаты и удалять дубликаты позже. Цель имеет значение.

В общем случае

a) HashSet может не принести пользы, если вы добавляете новые объекты из db, и вы не указали собственный Equals собственный. Каждый объект из db может быть новым экземпляром для вашего хешета (если вы только новичок), и это приведет к дублированию в коллекции. В этом случае используйте обычный List<T>.

b) Если у вас есть определитель равенства, определенный для hashset, и ваша коллекция всегда должна содержать только отдельные объекты, используйте hashset.

c) Если у вас есть сопоставитель равенства, определенный для hashset, и вам нужны только отдельные объекты из db, но коллекция не всегда должна содержать только отдельные объекты (то есть дубликаты, которые необходимо добавить позже), более быстрый подход - получить элементы от db до hashset, а затем возвращают обычный список из этого хэшета.

d) Лучшее, что вам нужно сделать, это предоставить задачу удаления дубликатов в базу данных, это правильный инструмент И этот первый класс!

Что касается различий в производительности, в моем тестировании я всегда находил HashSet быстрее, но тогда это было только маргинальным. Это очевидно, учитывая подход List, который вы должны сначала добавить, а затем сделать отчетливым.

Метод тестирования: начиная с двух общих функций,

public static void Benchmark(Action method, int iterations = 10000)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < iterations; i++)
        method();

    sw.Stop();
    MsgBox.ShowDialog(sw.Elapsed.TotalMilliseconds.ToString());
}

public static List<T> Repeat<T>(this ICollection<T> lst, int count)
{
    if (count < 0)
        throw new ArgumentOutOfRangeException("count");

    var ret = Enumerable.Empty<T>();

    for (var i = 0; i < count; i++)
        ret = ret.Concat(lst);

    return ret.ToList();
}

Реализация:

var d = Enumerable.Range(1, 100).ToList().Repeat(100);
HashSet<int> hash = new HashSet<int>();

Benchmark(() =>
{
    hash.Clear();
    foreach (var item in d)
    {
        hash.Add(item);
    }
});

~ 3300 мс

var d = Enumerable.Range(1, 100).ToList().Repeat(100);
List<int> list = new List<int>();

Benchmark(() =>
{
    list.Clear();
    foreach (var item in d)
    {
        list.Add(item);
    }

    list = list.Distinct().ToList();
});

~ 5800 мс

Разница в 2,5 секунды не является плохим для списка из 10000 объектов при повторении 10000 раз. Для нормальных случаев разница будет едва заметной.

Лучший подход, возможно, для вас с вашим текущим дизайном:

var d = Enumerable.Range(1, 100).ToList().Repeat(100);
HashSet<int> hash = new HashSet<int>();
List<int> list = new List<int>();

Benchmark(() =>
{
    hash.Clear();
    foreach (var item in d)
    {
        hash.Add(item);
    }

    list = hash.ToList();
});

~ 3300 мс

Нет существенной разницы, см.


Частично несвязанный - после публикации этого ответа мне было любопытно узнать, какой лучший способ удалить дубликаты из обычного списка.

var d = Enumerable.Range(1, 100).ToList().Repeat(100);
HashSet<int> hash = new HashSet<int>();
List<int> list = new List<int>();

Benchmark(() =>
{
    hash = new HashSet<int>(d);
});

~ 3900 мс

var d = Enumerable.Range(1, 100).ToList().Repeat(100);
List<int> list = new List<int>();

Benchmark(() =>
{
    list = d.Distinct().ToList();
});

~ 3200 мс

Здесь правильный инструмент Distinct быстрее, чем hackish HashSet! Возможно, это накладные расходы на создание хэш-набора.


Я тестировал с различными другими комбинациями, такими как ссылочные типы, без дубликатов в исходном списке и т.д. Результаты согласуются.

Ответ 3

Что лучше, что наиболее выразительно в описании вашего намерения. Внутренние детали реализации более или менее одинаковы, разница заключается в том, "кто пишет код?"

Если вы намереваетесь создать с нуля отдельный набор элементов из источника, который не является совокупностью указанных элементов, я бы сказал, что для HashSet<T>. Вы должны создать элемент, вам нужно собрать коллекцию, вы могли бы также построить правильную с самого начала.

В противном случае, если у вас уже есть коллекция элементов, и вы хотите удалить дубликаты, я бы сказал, что вы вызываете Distinct(). У вас уже есть коллекция, вам просто нужен выразительный способ получить отдельные элементы из нее.

Ответ 4

Для больших коллекций HashSet скорее всего будет быстрее. Он полагается на хэш-код объектов, чтобы быстро определить, существует ли элемент в наборе.

На практике это (скорее всего) не будет иметь значения (но вы должны измерить, если вам интересно).

Я инстинктивно предположил, что HashSet будет быстрее, из-за быстрой проверки хэша, которую он использует. Тем не менее, я просмотрел текущую (4.0) реализацию Distinct в исходных источниках и использует аналогичный класс Set (который также полагается на хэширование) под обложками. Вывод; нет практических различий в производительности.

В вашем случае я бы пошел с .Distinct для чтения - он ясно передает намерение кода. Однако я согласен с одним из других ответов, что вы, вероятно, должны выполнить эту операцию в БД, если это возможно.

Ответ 5

Если yor зацикливается на результатах DbReader, добавив ваши повторы в Hashset, было бы лучше, чем добавление его в список, а затем выполнение отличия. Вы сохранили бы одну итерацию. (Distinct внутренне использует HashSet)