Phương thức C # Distinction () có giữ nguyên thứ tự ban đầu của chuỗi không?


82

Tôi muốn xóa các bản sao khỏi danh sách mà không thay đổi thứ tự của các phần tử duy nhất trong danh sách.

Jon Skeet và những người khác đã đề xuất sử dụng sau

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

xóa các bản sao khỏi danh sách C #

Xóa các bản sao khỏi Danh sách <T> trong C #

Có đảm bảo rằng thứ tự của các phần tử duy nhất sẽ giống như trước không? Nếu có, vui lòng cung cấp tài liệu tham khảo xác nhận điều này vì tôi không thể tìm thấy bất cứ điều gì về nó trong tài liệu.


5
@ColonelPanic - tài liệu chính thức tại đây msdn.microsoft.com/en-us/library/bb348436(v=vs.110).aspx tuyên bố rõ ràng "Phương thức Distinction () trả về một chuỗi không có thứ tự không chứa các giá trị trùng lặp".
Evk

@Evk 'Trình tự không có thứ tự' không giống như 'thứ tự ban đầu của trình tự'.
Nitesh

3
Tôi coi "chưa được kiểm tra" có nghĩa là "không theo thứ tự cụ thể", cũng có nghĩa là "không cần thiết theo thứ tự ban đầu của trình tự".
Evk

Tôi vừa gặp sự cố liên quan đến sự khác biệt với oracle12 Entity Framework 6. Trong trường hợp của tôi, tôi đã đặt hàng trước khi hủy bỏ trong điều khoản linq của mình và đơn đặt hàng đã biến mất. select (). OrderBy (). Distinction (). ToList () không hoạt động khi select (). OrderBy (). Distinction (). ToList () hoạt động.
Karl

2
@Karl, những biểu thức này giống nhau. :)
pvgoran

Câu trả lời:


75

Nó không được đảm bảo, nhưng đó là cách triển khai rõ ràng nhất. Sẽ khó thực hiện theo cách phát trực tuyến (tức là nó trả về kết quả ngay khi có thể, chỉ đọc ít nhất có thể) mà không trả lại theo thứ tự.

Bạn có thể muốn đọc bài đăng trên blog của tôi về việc triển khai Edulinq của Distinction () .

Lưu ý rằng ngay cả khi điều này được đảm bảo cho LINQ to Objects (theo cá nhân tôi nghĩ là nên như vậy) thì điều đó sẽ không có ý nghĩa gì đối với các nhà cung cấp LINQ khác như LINQ to SQL.

Mức độ đảm bảo được cung cấp trong LINQ cho Đối tượng đôi khi hơi mâu thuẫn, IMO. Một số tối ưu hóa được ghi lại, một số khác thì không. Rất tiếc, một số tài liệu đã sai .


Tôi chấp nhận nó vì 1) Nó trả lời rõ ràng mối quan tâm của tôi về việc nó có được đảm bảo hay không 2) Bài đăng được liên kết nghiên cứu sâu hơn về các khía cạnh không có tài liệu của Distinction 3) Bài đăng được liên kết cũng có một triển khai mẫu có thể được sử dụng làm tài liệu tham khảo để triển khai Distinction trên Danh sách với sự đảm bảo đó.
Nitesh

26

Trong .NET Framework 3.5, việc tháo rời CIL của việc triển khai Linq-to-Objects Distinct()cho thấy rằng thứ tự của các phần tử được giữ nguyên - tuy nhiên đây không phải là hành vi được ghi lại.

Tôi đã thực hiện một cuộc điều tra nhỏ với Reflector. Sau khi tháo gỡ System.Core.dll, Version = 3.5.0.0, bạn có thể thấy rằng Distinction () là một phương thức mở rộng, trông giống như sau:

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

Vì vậy, thú vị ở đây là DistinctionIterator, thực hiện IEnumerable và IEnumerator. Đây là cách triển khai đơn giản (goto và lables đã bị loại bỏ) của IEnumerator này:

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

Như bạn có thể thấy - việc liệt kê diễn ra theo thứ tự được cung cấp bởi nguồn có thể liệt kê (danh sách mà chúng ta đang gọi Distinct). Hashsetchỉ được sử dụng để xác định xem chúng tôi đã trả về phần tử đó hay chưa. Nếu không, chúng tôi sẽ trả lại, nếu không - tiếp tục liệt kê trên nguồn.

Vì vậy, nó được đảm bảo, Distinct()sẽ trả về các phần tử chính xác theo cùng một thứ tự , được cung cấp bởi bộ sưu tập mà Distinction đã được áp dụng.


8
Đó có phải là một hành vi được ghi chép tốt không?
abatishchev

4
Câu trả lời được liên kết chứa tham chiếu đến tài liệu cho biết: "Chuỗi kết quả không có thứ tự."
mgronber

4
@lazyberezovsky: Câu hỏi hỏi về sự đảm bảo , không phải cách triển khai phổ biến . (Như tôi đã nói, tôi sẽ ngạc nhiên nếu việc triển khai có thay đổi giữa các nền tảng / phiên bản, nhưng điều đó không có nghĩa là đảm bảo.)
LukeH

5
@lazyberezovsky: Tôi đến từ C \ C ++, nơi có rất nhiều thứ chưa được xác định và rất phổ biến khi yêu cầu xem điều gì đó được đảm bảo. Ngoài ra, tôi đang sử dụng Distinction () trong một ứng dụng Silverlight, trên cả Mac và Windows, đó là lý do tại sao chúng tôi không thể giải quyết 'triển khai chung' nó phải được đảm bảo.
Nitesh

42
@lazyberezovsky: Khi mọi người nói về bảo lãnh, họ thường muốn nói đến hành vi được lập thành văn bản là hợp lý để dựa vào. Ví dụ, các tài liệu cho groupby làm rõ hành vi, nhưng các tài liệu cho biệt không .
Jon Skeet

14

Theo tài liệu , trình tự không có thứ tự.


2
Thông tin bổ sung để tìm nó: Trong liên kết, hãy tham khảo phần "Nhận xét". "Chuỗi kết quả không có thứ tự."
Curtis Yallop

6

Đúng , Enumerable.Distinct duy trì trật tự. Giả sử phương thức là lazy "tạo ra các giá trị khác biệt ngay khi chúng được nhìn thấy", nó sẽ tự động tuân theo. Hãy suy nghĩ về nó.

Nguồn tham chiếu .NET xác nhận. Nó trả về một dãy con, phần tử đầu tiên trong mỗi lớp tương đương.

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

Các thực hiện NET lõi cũng tương tự.

Thật thất vọng, tài liệu cho Enumerable.Distinct bị nhầm lẫn về điểm này:

Chuỗi kết quả không có thứ tự.

Tôi chỉ có thể tưởng tượng họ có nghĩa là "chuỗi kết quả không được sắp xếp." Bạn có thể triển khai Distinction bằng cách đặt trước sau đó so sánh từng phần tử với phần tử trước đó, nhưng điều này sẽ không lười biếng như đã định nghĩa ở trên.


6
Nguồn không phải là đặc tả. Những gì bạn tìm thấy là một sự trùng hợp và có thể không hợp lệ sau lần cập nhật tiếp theo.
Henk Holterman

@HenkHolterman Nói chung, tôi đồng ý, việc triển khai có thể thay đổi. Ví dụ, .NET 4.5 đã thay đổi thuật toán sắp xếp đằng sau Array.Sort. Tuy nhiên, trong trường hợp cụ thể này, bất kỳ triển khai hợp lý nào của Enumerable.Distinct chắc chắn sẽ lười biếng ("tạo ra các giá trị khác biệt ngay khi chúng được nhìn thấy") và thuộc tính bảo toàn thứ tự theo sau từ đó. Đánh giá lười biếng là nguyên lý cốt lõi của LINQ đối với Đối tượng; hủy bỏ nó sẽ là không thể tưởng tượng được.
Colonel Panic vào

1
Tôi đã nhìn thấy triển khai sử dụng .net 4.6 nơi gọi dbQuery.OrderBy(...).Distinct().ToList()không trả lại một danh sách theo thứ tự quy định theo lệnh của vị ngữ - tháo biệt (mà xảy ra là dư thừa) cố định các lỗi trong trường hợp của tôi
Rowland Shaw

1

Theo mặc định khi sử dụng Toán tử linq riêng biệt sử dụng phương thức Equals nhưng bạn có thể sử dụng IEqualityComparer<T>đối tượng của riêng mình để chỉ định khi nào hai đối tượng bằng nhau bằng cách triển khai logic tùy chỉnh GetHashCodeEquals phương thức phương thức . Nhớ lấy:

GetHashCodekhông nên sử dụng so sánh cpu nặng (ví dụ: chỉ sử dụng một số kiểm tra cơ bản rõ ràng) và nó được sử dụng như lần đầu tiên để chỉ ra nếu hai đối tượng chắc chắn khác nhau (nếu trả về mã băm khác nhau) hoặc có khả năng giống nhau (cùng một mã băm). Trong trường hợp mới nhất này khi hai đối tượng có cùng một mã băm, khung công tác sẽ kiểm tra bằng cách sử dụng phương thức Equals như một quyết định cuối cùng về sự bình đẳng của các đối tượng đã cho.

Sau khi bạn có MyTypevà một MyTypeEqualityComparerlớp theo mã không đảm bảo trình tự duy trì thứ tự của nó:

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

Trong thư viện khoa học tiếp theo, tôi đã triển khai một phương thức mở rộng để đảm bảo bộ Vector3D duy trì thứ tự khi sử dụng một phương thức mở rộng cụ thể DistinctKeepOrder:

mã liên quan sau:

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

Trong ngắn hạn, Vector3DWithOrderđóng gói kiểu và một số nguyên thứ tự, trong khi Vector3DWithOrderEqualityComparerđóng gói trình so sánh kiểu gốc.

và đây là công cụ trợ giúp phương pháp để đảm bảo duy trì trật tự

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

Lưu ý : nghiên cứu sâu hơn có thể cho phép tìm ra cách tổng quát hơn (sử dụng các giao diện) và cách tối ưu hóa (không đóng gói đối tượng).


1

Điều này phụ thuộc nhiều vào nhà cung cấp linq của bạn. Trên Linq2Objects, bạn có thể sử dụng mã nguồn nội bộ Distinct, điều này khiến người ta cho rằng thứ tự ban đầu được giữ nguyên.

Tuy nhiên, đối với các nhà cung cấp khác giải quyết một số loại SQL chẳng hạn, đó không phải là trường hợp cần thiết, vì một câu lệnh ORDER BYthường xuất hiện sau bất kỳ tổng hợp nào (chẳng hạn như Distinct). Vì vậy, nếu mã của bạn là:

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

điều này được dịch sang một cái gì đó tương tự như sau trong SQL:

SELECT * FROM mytable GROUP BY mycol ORDER BY anothercol;

Điều này rõ ràng đầu tiên sẽ nhóm dữ liệu của bạn và sắp xếp nó sau đó. Bây giờ bạn đang bị mắc kẹt với logic riêng của DBMS về cách thực thi điều đó. Trên một số DBMS, điều này thậm chí không được phép. Hãy tưởng tượng dữ liệu sau:

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

khi thực hiện myArr.OrderBy(x => x.anothercol).GroupBy(x => x.mycol) chúng tôi giả sử kết quả sau:

mycol anothercol
1     1
2     1

Nhưng DBMS có thể tổng hợp một cột khác để luôn sử dụng giá trị của hàng đầu tiên, dẫn đến dữ liệu sau:

mycol anothercol
1    2
2    1

mà sau khi đặt hàng sẽ dẫn đến điều này:

mycol anothercol
2    1
1    2

Điều này tương tự như sau:

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

đó là thứ tự hoàn toàn ngược lại so với những gì bạn mong đợi.

Bạn thấy kế hoạch thực hiện có thể khác nhau tùy thuộc vào nhà cung cấp cơ bản là gì. Đây là lý do tại sao không có gì đảm bảo về điều đó trong tài liệu.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.