Сохраняет ли метод Distinct () исходный порядок последовательности неизменным?

84

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

Джон Скит и другие предложили использовать следующее:

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

Справка:

Гарантируется ли, что порядок уникальных элементов будет таким же, как и раньше? Если да, дайте ссылку, подтверждающую это, поскольку я ничего не нашел в документации.

Нитеш
источник
5
@ColonelPanic - здесь официальная документация msdn.microsoft.com/en-us/library/bb348436(v=vs.110).aspx явно заявляет: «Метод Distinct () возвращает неупорядоченную последовательность, не содержащую повторяющихся значений».
Evk
@Evk «Неупорядоченная последовательность» - это не то же самое, что «исходный порядок последовательности».
Nitesh
3
Я считаю, что «неупорядоченный» означает «в произвольном порядке», что также подразумевает «не обязательно в исходном порядке».
Evk
У меня просто возникла проблема с отличием от oracle12 Entity Framework 6. В моем случае у меня был orderby перед дезинфекцией в моем предложении linq, и порядок пропал. select (). OrderBy (). Distinct (). ToList () не работал, пока работал select (). OrderBy (). Distinct (). ToList ().
Карл
2
@Karl, эти выражения совпадают. :)
pvgoran

Ответы:

77

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

Возможно, вы захотите прочитать мое сообщение в блоге о реализации Distinct () в Edulinq .

Обратите внимание, что даже если бы это было гарантировано для LINQ to Objects (что лично я думаю, что так и должно быть), это ничего не значило бы для других поставщиков LINQ, таких как LINQ to SQL.

Уровень гарантий, предоставляемых в LINQ to Objects, иногда немного непоследователен, ИМО. Некоторые оптимизации задокументированы, другие нет. Черт возьми, часть документации совершенно неверна .

Джон Скит
источник
Я принимаю это, потому что 1) он четко отвечает на мою озабоченность, гарантирован он или нет 2) Связанный пост углубляется в недокументированные аспекты Distinct 3) Связанный пост также имеет образец реализации, который можно использовать в качестве ссылки для реализации Distinct на Списки с этой гарантией.
Nitesh
25

В .NET Framework 3.5 дизассемблирование CIL реализации Linq-to-Objects Distinct() показывает, что порядок элементов сохраняется, однако это не документированное поведение.

Я провел небольшое расследование с Reflector. После дизассемблирования System.Core.dll, Version = 3.5.0.0 вы можете увидеть, что Distinct () - это метод расширения, который выглядит так:

public static class Emunmerable
{
    public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source)
    {
        if (source == null)
            throw new ArgumentNullException("source");

        return DistinctIterator<TSource>(source, null);
    }
}

Итак, интересен DistinctIterator, который реализует IEnumerable и IEnumerator. Вот упрощенная (удаленные goto и ярлыки) реализация этого IEnumerator:

private sealed class DistinctIterator<TSource> : IEnumerable<TSource>, IEnumerable, IEnumerator<TSource>, IEnumerator, IDisposable
{
    private bool _enumeratingStarted;
    private IEnumerator<TSource> _sourceListEnumerator;
    public IEnumerable<TSource> _source;
    private HashSet<TSource> _hashSet;    
    private TSource _current;

    private bool MoveNext()
    {
        if (!_enumeratingStarted)
        {
            _sourceListEnumerator = _source.GetEnumerator();
            _hashSet = new HashSet<TSource>();
            _enumeratingStarted = true;
        }

        while(_sourceListEnumerator.MoveNext())
        {
            TSource element = _sourceListEnumerator.Current;

             if (!_hashSet.Add(element))
                 continue;

             _current = element;
             return true;
        }

        return false;
    }

    void IEnumerator.Reset()
    {
        throw new NotSupportedException();
    }

    TSource IEnumerator<TSource>.Current
    {
        get { return _current; }
    }

    object IEnumerator.Current
    {        
        get { return _current; }
    }
}

Как видите, перечисление идет в порядке, заданном исходным перечислением (список, по которому мы вызываем Distinct). Hashsetиспользуется только для определения того, вернули ли мы такой элемент или нет. Если нет - возвращаем, иначе продолжаем перечисление по источнику.

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

Сергей Березовский
источник
8
Это хорошо задокументированное поведение?
abatishchev
4
Связанный ответ содержит ссылку на документацию, в которой говорится: «Последовательность результатов неупорядочена».
mgronber
5
@lazyberezovsky: Вопрос касается гарантий , а не общей реализации . (Как я уже сказал, я был бы удивлен, если реализация когда-либо изменится для разных платформ / версий, но это не является гарантией.)
LukeH
5
@lazyberezovsky: я из C \ C ++, где многие вещи не определены, и очень часто спрашивают, гарантировано ли что-то. Также я использую Distinct () в приложении Silverlight, которое есть как на Mac, так и на Windows, поэтому мы не можем выбрать «общую реализацию», это должно быть гарантировано.
Nitesh
43
@lazyberezovsky: Когда люди говорят о гарантиях, они обычно имеют в виду задокументированное поведение, на которое можно положиться. Например, документы для GroupBy действительно определяют поведение, но документы для Distinct не .
Джон Скит,
14

Согласно документации последовательность не упорядочена.

мгронбер
источник
3
Дополнительная информация для его поиска: По ссылке см. Раздел «Примечания». «Последовательность результатов неупорядочена».
Curtis Yallop
6

Да , Enumerable.Distinct сохраняет порядок. Если предположить, что метод является ленивым, «выдает разные значения, как только они видны», он следует автоматически. Подумай об этом.

В исходных .NET Reference подтверждает. Он возвращает подпоследовательность, первый элемент в каждом классе эквивалентности.

foreach (TSource element in source)
    if (set.Add(element)) yield return element;

Реализация .NET Core аналогична.

К сожалению, документация для Enumerable.Distinct запуталась в этом вопросе:

Последовательность результатов неупорядочена.

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

Полковник Паник
источник
7
Источник - не спецификация. То, что вы обнаружили, является совпадением и может оказаться недействительным после следующего обновления.
Хенк
@HenkHolterman В общем, согласен, реализации могут меняться. Например, .NET 4.5 изменил алгоритм сортировки за Array.Sort. Однако в этом конкретном случае любая разумная реализация Enumerable.Distinct наверняка будет ленивой («выдает различные значения сразу после того, как они видны»), и из этого следует свойство сохранения порядка. Ленивое вычисление - это основной принцип LINQ to Objects; отменить это было бы немыслимо.
Colonel Panic
1
Я видел реализации с использованием .net 4.6, в которых при вызове dbQuery.OrderBy(...).Distinct().ToList()не возвращался список в порядке, заданном порядком по предикату - удаление Distinct (которое оказалось избыточным) исправило ошибку в моем случае
Rowland Shaw
1

По умолчанию при использовании Distinct оператор linq использует метод Equals, но вы можете использовать свой собственный IEqualityComparer<T>объект, чтобы указать, когда два объекта равны, с реализацией пользовательской логики GetHashCodeи Equalsметодом. Помните, что:

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

После того, как вы есть , MyTypeи через MyTypeEqualityComparerклассы следуют код не обеспечивает последовательность сохранять порядок:

var cmp = new MyTypeEqualityComparer();
var lst = new List<MyType>();
// add some to lst
var q = lst.Distinct(cmp);

В следующей научной библиотеке я реализовал метод расширения, чтобы гарантировать, что набор Vector3D поддерживает порядок при использовании определенного метода расширения.DistinctKeepOrder :

соответствующий код:

/// <summary>
/// support class for DistinctKeepOrder extension
/// </summary>
public class Vector3DWithOrder
{
    public int Order { get; private set; }
    public Vector3D Vector { get; private set; }
    public Vector3DWithOrder(Vector3D v, int order)
    {
        Vector = v;
        Order = order;
    }
}

public class Vector3DWithOrderEqualityComparer : IEqualityComparer<Vector3DWithOrder>
{
    Vector3DEqualityComparer cmp;

    public Vector3DWithOrderEqualityComparer(Vector3DEqualityComparer _cmp)
    {
        cmp = _cmp;
    }

    public bool Equals(Vector3DWithOrder x, Vector3DWithOrder y)
    {
        return cmp.Equals(x.Vector, y.Vector);
    }

    public int GetHashCode(Vector3DWithOrder obj)
    {
        return cmp.GetHashCode(obj.Vector);
    }
}

Вкратце Vector3DWithOrderинкапсулируйте тип и целое число порядка, аVector3DWithOrderEqualityComparer инкапсулирует компаратор исходного типа.

и это помощник метода для обеспечения порядка

/// <summary>
/// retrieve distinct of given vector set ensuring to maintain given order
/// </summary>        
public static IEnumerable<Vector3D> DistinctKeepOrder(this IEnumerable<Vector3D> vectors, Vector3DEqualityComparer cmp)
{
    var ocmp = new Vector3DWithOrderEqualityComparer(cmp);

    return vectors
        .Select((w, i) => new Vector3DWithOrder(w, i))
        .Distinct(ocmp)
        .OrderBy(w => w.Order)
        .Select(w => w.Vector);
}

Примечание : дальнейшие исследования могут позволить найти более общий (использование интерфейсов) и оптимизированный способ (без инкапсуляции объекта).

Лоренцо Делана
источник
1

Это сильно зависит от вашего linq-провайдера. В Linq2Objects вы можете использовать внутренний исходный код для Distinct, что заставляет предположить, что исходный порядок сохраняется.

Однако для других провайдеров, которые разрешают, например, некоторый вид SQL, это не обязательно так, поскольку ORDER BY-statement обычно идет после любого агрегирования (например, Distinct). Итак, если ваш код такой:

myArray.OrderBy(x => anothercol).GroupBy(x => y.mycol);

это переводится в SQL примерно так:

SELECT * FROM mytable GROUP BY mycol ORDER BY anothercol;

Это, очевидно, сначала группирует ваши данные, а затем сортирует их. Теперь вы застряли на собственной логике СУБД о том, как это выполнить. В некоторых СУБД это даже не разрешено. Представьте себе следующие данные:

mycol anothercol
1     2
1     1
1     3
2     1
2     3

при выполнении myArr.OrderBy(x => x.anothercol).GroupBy(x => x.mycol)предполагаем следующий результат:

mycol anothercol
1     1
2     1

Но СУБД может агрегировать столбец anothercol таким образом, чтобы всегда использовалось значение первой строки, в результате чего получались следующие данные:

mycol anothercol
1    2
2    1

что после заказа приведет к такому:

mycol anothercol
2    1
1    2

Это похоже на следующее:

SELECT mycol, First(anothercol) from mytable group by mycol order by anothercol;

что является полностью обратным порядком, чем вы ожидали.

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

HimBromBeere
источник