Что лучше для создания различных структур данных: HashSet или Linq Distinct ()? - PullRequest
21 голосов
/ 10 июня 2011

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

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

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

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

Спасибо.

Ответы [ 6 ]

22 голосов
/ 22 ноября 2012

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

В общем

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

b) Если у вас есть определитель сравнения, определенный для hashset, и ваша коллекция всегда должна содержать только отдельные объекты, используйте 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 быстрее, чем хакерский HashSet! Возможно, это накладные расходы на создание хеш-набора.


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

13 голосов
/ 10 июня 2011

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

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

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

4 голосов
/ 10 июня 2011

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

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

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

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

1 голос
/ 10 июня 2011

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

1 голос
/ 10 июня 2011

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

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

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

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

0 голосов
/ 10 июня 2011

Реализация Distinct может использовать HashSet.Взгляните на реализацию Edulinq Джона Скита .

...