Чи метод Distinct () зберігає незмінним оригінальне впорядкування послідовностей?


84

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

Джон Скіт та інші запропонували використовувати наступне:

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

Довідково:

Чи гарантовано порядок унікальних елементів буде таким же, як і раніше? Якщо так, будь ласка, надайте посилання, яке підтверджує це, оскільки я не міг знайти в ньому нічого в документації.


5
@ColonelPanic - офіційна документація тут msdn.microsoft.com/en-us/library/bb348436(v=vs.110).aspx прямо заявляє "метод Distinct () повертає невпорядковану послідовність, що не містить повторюваних значень".
Evk

@Evk "Невпорядкована послідовність" - це не те саме, що "оригінальне впорядкування послідовності".
Нітеш,

3
Я вважаю, що "необмежений порядок" означає "не в певному порядку", що також означає "не потрібно в початковому порядку послідовності".
Evk

У мене щойно виникла проблема з різницею з oracle12 Entity Framework 6. У моєму випадку я провів порядок до дезінфекції в моєму пункті linq, і замовлення зникло. select (). OrderBy (). Distinct (). ToList () не працював, поки select (). OrderBy (). Distinct (). ToList () спрацював.
Карл

2
@Karl, ці вирази однакові. :)
pvgoran

Відповіді:


77

Це не гарантовано, але це найбільш очевидна реалізація. Було б важко реалізувати потоковою передачею (тобто такою, щоб вона повертала результати якомога швидше, прочитавши якомога менше), не повертаючи їх у порядку.

Можливо, ви захочете прочитати мій допис у блозі про реалізацію Edulinq Distinct () .

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

Рівень гарантій, що надаються в LINQ для об’єктів, іноді трохи не узгоджується, ІМО. Деякі оптимізації задокументовані, інші ні. Чорт візьми, частина документації абсолютно невірна .


Я приймаю це, оскільки 1) Це чітко відповідає на моє занепокоєння, гарантоване це чи ні. 2) Пов’язаний пост глибше заглиблюється в недокументовані аспекти виразного 3) Пов’язаний пост також має зразок реалізації, який може бути використаний як посилання для реалізації виразного Списки з цією гарантією.
Нітеш

25

У .NET Framework 3.5 розбирання CIL реалізації Linq-to-Objects Distinct()показує, що порядок елементів зберігається, проте це не задокументована поведінка.

Я провів невелике розслідування з Reflector. Після розбирання System.Core.dll, Версія = 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; }
    }
}

Як бачите - перерахування відбувається в порядку, наданому джерелом enumerable (список, до якого ми телефонуємо Distinct). Hashsetвикористовується лише для визначення того, чи ми вже повернули такий елемент чи ні. Якщо ні, ми повертаємо його, інакше - продовжуємо перераховувати за джерелом.

Отже, гарантується, що Distinct()елементи повертатимуться точно в тому самому порядку , що надаються колекцією, до якої було застосовано Distinct.


8
Це добре задокументована поведінка?
абатищев

4
Відповідна відповідь містить посилання на документацію, яка говорить: "Послідовність результатів невпорядкована."
mgronber

5
@lazyberezovsky: Питання задається гарантіями , а не загальним впровадженням . (Як я вже говорив, я був би здивований, якщо реалізація коли-небудь зміниться на різних платформах / версіях, але це не означає гарантії.)
LukeH

5
@lazyberezovsky: Я з C \ C ++, де багато речей не визначено і дуже часто просять про те, щоб щось було гарантовано. Також я використовую Distinct () у додатку Silverlight, який є як на Mac, так і на Windows, тому ми не можемо зупинитися на "загальній реалізації", це повинно бути гарантовано.
Нітеш

43
@lazyberezovsky: Коли люди говорять про гарантії, вони зазвичай мають на увазі задокументовану поведінку, на яку розумно покладатися. Наприклад, документи для GroupBy дійсно визначають поведінку, але документи для Distinct НЕ .
Джон Скіт,


6

Так , незліченний. Виразний зберігає порядок. Якщо припустити, що метод ледачий "дає чіткі значення, як тільки їх бачать", він слідує автоматично. Подумай над цим.

Джерело посилання .NET підтверджує. Він повертає підпослідовність, перший елемент у кожному класі еквівалентності.

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

Реалізація .NET Core є подібною.

Неприємно , але документація для Enumerable.Disinct з цього питання заплутана:

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

Я лише уявляю, що вони означають "послідовність результатів не сортується". Ви можете реалізувати Distinct шляхом попереднього упорядкування, а потім порівняння кожного елемента з попереднім, але це не буде лінивим, як визначено вище.


7
Джерело не є специфікацією. Виявлено випадковість і може бути недійсним після наступного оновлення.
Хенк Холтерман,

@HenkHolterman Загалом, я б погодився, реалізації можуть змінюватися. Наприклад, .NET 4.5 змінив алгоритм сортування за Array.Sort. Однак у цьому конкретному випадку будь-яка розумна реалізація Enumerable.Distinct, безсумнівно, буде лінивою ("дає чіткі значення, як тільки їх побачать"), і властивість збереження замовлення випливає з цього. Ледача оцінка є основним принципом LINQ to Objects; скасувати це було б немислимо.
Полковник Панік

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);

У бібліотеці follow sci я реалізував метод розширення, щоб забезпечити підтримку набору 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інкапсулюйте тип і ціле число порядку, whileVector3DWithOrderEqualityComparer інкапсулює порівняльник оригінального типу.

і це помічник методу для забезпечення порядку

/// <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-заявлення зазвичай приходить після будь-якого агрегування (наприклад 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

Але СУБД може агрегувати інший стовпецьcol, так що завжди використовується значення першого рядка, в результаті чого отримуються такі дані:

mycol anothercol
1    2
2    1

що після замовлення призведе до цього:

mycol anothercol
2    1
1    2

Це схоже на наступне:

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

що є абсолютно зворотним порядком, ніж ви очікували.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.