LINQ's Distinc () trên một tài sản cụ thể


1095

Tôi đang chơi với LINQ để tìm hiểu về nó, nhưng tôi không thể tìm ra cách sử dụng Distinctkhi tôi không có một danh sách đơn giản (một danh sách số nguyên đơn giản khá dễ thực hiện, đây không phải là câu hỏi). Điều gì nếu tôi muốn sử dụng Phân biệt trong danh sách Đối tượng trên một hoặc nhiều thuộc tính của đối tượng?

Ví dụ: Nếu một đối tượng là Person, với Thuộc tính Id. Làm thế nào tôi có thể có được tất cả Người và sử dụng Distinctchúng với tài sản Idcủa đối tượng?

Person1: Id=1, Name="Test1"
Person2: Id=1, Name="Test1"
Person3: Id=2, Name="Test2"

Làm thế nào tôi có thể nhận được Person1Person3? Điều đó có thể không?

Nếu không thể với LINQ, cách tốt nhất để có một danh sách Personphụ thuộc vào một số thuộc tính của nó trong .NET 3.5 là gì?

Câu trả lời:


1247

EDIT : Đây là một phần của MoreLINEQ .

Những gì bạn cần là một "khác biệt" một cách hiệu quả. Tôi không tin đó là một phần của LINQ, mặc dù nó khá dễ viết:

public static IEnumerable<TSource> DistinctBy<TSource, TKey>
    (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
    HashSet<TKey> seenKeys = new HashSet<TKey>();
    foreach (TSource element in source)
    {
        if (seenKeys.Add(keySelector(element)))
        {
            yield return element;
        }
    }
}

Vì vậy, để tìm các giá trị riêng biệt chỉ sử dụng thuộc Idtính, bạn có thể sử dụng:

var query = people.DistinctBy(p => p.Id);

Và để sử dụng nhiều thuộc tính, bạn có thể sử dụng các loại ẩn danh, thực hiện bình đẳng một cách thích hợp:

var query = people.DistinctBy(p => new { p.Id, p.Name });

Chưa được kiểm tra, nhưng nó sẽ hoạt động (và bây giờ ít nhất là biên dịch).

Nó giả sử bộ so sánh mặc định cho các khóa mặc dù - nếu bạn muốn chuyển vào một bộ so sánh bằng, chỉ cần chuyển nó cho hàm HashSettạo.



1
@ tro999: Tôi không chắc ý của bạn là gì. Mã có mặt trong câu trả lời trong thư viện - tùy thuộc vào việc bạn có vui lòng nhận phụ thuộc hay không.
Jon Skeet

10
@ tro999: Nếu bạn chỉ làm việc này ở một nơi duy nhất, thì chắc chắn, việc sử dụng GroupBysẽ đơn giản hơn. Nếu bạn cần nó ở nhiều nơi, nó sẽ sạch hơn nhiều (IMO) để gói gọn ý định.
Jon Skeet

5
@MatthewWhited: Cho rằng không có đề cập nào IQueryable<T>ở đây, tôi không thấy nó liên quan như thế nào. Tôi đồng ý rằng điều này sẽ không phù hợp với EF vv, nhưng trong LINQ to Objects Tôi nghĩ đó là hơn phù hợp hơn GroupBy. Bối cảnh của câu hỏi luôn luôn quan trọng.
Jon Skeet

7
Dự án đã chuyển trên github, đây là mã của
DistincBy

1858

Điều gì xảy ra nếu tôi muốn có được một danh sách riêng biệt dựa trên một hoặc nhiều thuộc tính?

Đơn giản! Bạn muốn nhóm chúng và chọn một người chiến thắng trong nhóm.

List<Person> distinctPeople = allPeople
  .GroupBy(p => p.PersonId)
  .Select(g => g.First())
  .ToList();

Nếu bạn muốn xác định các nhóm trên nhiều thuộc tính, đây là cách:

List<Person> distinctPeople = allPeople
  .GroupBy(p => new {p.PersonId, p.FavoriteColor} )
  .Select(g => g.First())
  .ToList();

1
@ErenErsonmez chắc chắn. Với mã được đăng của tôi, nếu muốn thực hiện hoãn lại, hãy bỏ cuộc gọi ToList.
Amy B

5
Câu trả lời rất hay! Thực sự đã giúp tôi trong Linq-to-Thực thể được điều khiển từ chế độ xem sql nơi tôi không thể sửa đổi chế độ xem. Tôi cần sử dụng FirstOrDefault () thay vì First () - tất cả đều tốt.
Alex KeySmith

8
Tôi đã thử nó và nó sẽ thay đổi thành Chọn (g => g.FirstOrDefault ())

26
@ChocapicSz Không. Cả hai Single()SingleOrDefault()mỗi lần ném khi nguồn có nhiều hơn một mục. Trong hoạt động này, chúng tôi hy vọng khả năng mỗi nhóm có thể có nhiều hơn một mục. Đối với vấn đề đó, First()được ưu tiên hơn FirstOrDefault()vì mỗi nhóm phải có ít nhất một thành viên .... trừ khi bạn đang sử dụng EntityFramework, điều này không thể hiểu rằng mỗi nhóm có ít nhất một thành viên và yêu cầu FirstOrDefault().
Amy B

2
Có vẻ như hiện tại không được hỗ trợ trong EF Core, thậm chí sử dụng FirstOrDefault() github.com/dotnet/efcore/issues/12088 Tôi đang ở trên 3.1 và tôi gặp lỗi "không thể dịch".
Collin M. Barrett

78

Sử dụng:

List<Person> pList = new List<Person>();
/* Fill list */

var result = pList.Where(p => p.Name != null).GroupBy(p => p.Id).Select(grp => grp.FirstOrDefault());

Việc wheregiúp bạn lọc các mục (có thể phức tạp hơn) groupbyselectthực hiện chức năng riêng biệt.


1
Hoàn hảo, và hoạt động mà không cần mở rộng Linq hoặc sử dụng một phụ thuộc khác.
DavidScherer

77

Bạn cũng có thể sử dụng cú pháp truy vấn nếu bạn muốn nó trông giống như LINQ:

var uniquePeople = from p in people
                   group p by new {p.ID} //or group by new {p.ID, p.Name, p.Whatever}
                   into mygroup
                   select mygroup.FirstOrDefault();

4
Hmm, suy nghĩ của tôi là cả cú pháp truy vấn và cú pháp API trôi chảy cũng giống như LINQ giống nhau và nó chỉ là sở thích mà mọi người sử dụng. Bản thân tôi thích API thông thạo hơn nên tôi sẽ xem xét điều đó giống như LINK hơn nhưng sau đó tôi đoán đó là chủ quan
Max Carroll

LINQ-Like không liên quan gì đến sở thích, là "giống như LINQ" phải làm giống như một ngôn ngữ truy vấn khác được nhúng vào C #, tôi thích giao diện trôi chảy hơn, đến từ các luồng java, nhưng nó không giống như LINQ.
Ryan The Leach

Thông minh!! Bạn là người hùng của tôi!
Farzin Kanzi

63

Tôi nghĩ thế là đủ:

list.Select(s => s.MyField).Distinct();

43
Điều gì sẽ xảy ra nếu anh ta cần trở lại đối tượng đầy đủ của mình, không chỉ lĩnh vực cụ thể đó?
Festim Cahani

1
Điều gì chính xác đối tượng của một số đối tượng có cùng giá trị thuộc tính?
donRumatta

40

Giải pháp nhóm đầu tiên theo các lĩnh vực của bạn sau đó chọn mục Firstordefault.

    List<Person> distinctPeople = allPeople
   .GroupBy(p => p.PersonId)
   .Select(g => g.FirstOrDefault())
   .ToList();

26

Bạn có thể làm điều này với tiêu chuẩn Linq.ToLookup(). Điều này sẽ tạo ra một tập hợp các giá trị cho mỗi khóa duy nhất. Chỉ cần chọn mục đầu tiên trong bộ sưu tập

Persons.ToLookup(p => p.Id).Select(coll => coll.First());

17

Đoạn mã sau có chức năng tương đương với câu trả lời của Jon Skeet .

Đã thử nghiệm trên .NET 4.5, nên hoạt động trên mọi phiên bản trước đó của LINQ.

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
  HashSet<TKey> seenKeys = new HashSet<TKey>();
  return source.Where(element => seenKeys.Add(keySelector(element)));
}

Ngẫu nhiên, hãy xem phiên bản mới nhất của DistincBy.cs của Jon Skeet trên Google Code .


3
Điều này mang lại cho tôi "chuỗi không có lỗi giá trị", nhưng câu trả lời của Skeet đã tạo ra kết quả chính xác.
Điều gì sẽ tuyệt vời

10

Tôi đã viết một bài viết giải thích cách mở rộng chức năng Phân biệt để bạn có thể làm như sau:

var people = new List<Person>();

people.Add(new Person(1, "a", "b"));
people.Add(new Person(2, "c", "d"));
people.Add(new Person(1, "a", "b"));

foreach (var person in people.Distinct(p => p.ID))
    // Do stuff with unique list here.

Đây là bài viết: Mở rộng LINQ - Chỉ định một thuộc tính trong Hàm phân biệt


3
Bài viết của bạn có lỗi, cần có <T> sau Phân biệt: công khai tĩnh IEnumerable <T> Phân biệt (điều này ... Ngoài ra, có vẻ như nó sẽ không hoạt động (độc đáo) trên một thuộc tính khác, tức là kết hợp đầu tiên và họ.
hàng1

2
+1, một lỗi nhỏ không phải là một lý do đủ để downvote, mà thật ngớ ngẩn, thường gọi là một lỗi đánh máy. Và tôi vẫn chưa thấy một chức năng chung sẽ hoạt động cho bất kỳ số lượng tài sản nào! Tôi hy vọng downvoter đã hạ cấp mọi câu trả lời khác trong chủ đề này. Nhưng này loại thứ hai là đối tượng là gì ?? Tôi phản đối !
nawfal

4
Liên kết của bạn bị hỏng
Tom Lint

7

Cá nhân tôi sử dụng lớp sau:

public class LambdaEqualityComparer<TSource, TDest> : 
    IEqualityComparer<TSource>
{
    private Func<TSource, TDest> _selector;

    public LambdaEqualityComparer(Func<TSource, TDest> selector)
    {
        _selector = selector;
    }

    public bool Equals(TSource obj, TSource other)
    {
        return _selector(obj).Equals(_selector(other));
    }

    public int GetHashCode(TSource obj)
    {
        return _selector(obj).GetHashCode();
    }
}

Sau đó, một phương thức mở rộng:

public static IEnumerable<TSource> Distinct<TSource, TCompare>(
    this IEnumerable<TSource> source, Func<TSource, TCompare> selector)
{
    return source.Distinct(new LambdaEqualityComparer<TSource, TCompare>(selector));
}

Cuối cùng, mục đích sử dụng:

var dates = new List<DateTime>() { /* ... */ }
var distinctYears = dates.Distinct(date => date.Year);

Ưu điểm tôi tìm thấy khi sử dụng phương pháp này là sử dụng lại LambdaEqualityComparerlớp cho các phương thức khác chấp nhận IEqualityComparer. (Ồ, và tôi để lại yieldcông cụ cho việc triển khai LINQ ban đầu ...)


5

Trong trường hợp bạn cần một phương thức riêng biệt trên nhiều thuộc tính, bạn có thể kiểm tra thư viện Mạnh mẽ của tôi . Hiện tại nó đang ở giai đoạn rất trẻ, nhưng bạn đã có thể sử dụng các phương pháp như Phân biệt, Liên minh, Giao lộ, Ngoại trừ bất kỳ số lượng tài sản nào;

Đây là cách bạn sử dụng nó:

using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);

5

Khi chúng tôi đối mặt với một nhiệm vụ như vậy trong dự án của mình, chúng tôi đã xác định một API nhỏ để soạn các bộ so sánh.

Vì vậy, trường hợp sử dụng là như thế này:

var wordComparer = KeyEqualityComparer.Null<Word>().
    ThenBy(item => item.Text).
    ThenBy(item => item.LangID);
...
source.Select(...).Distinct(wordComparer);

Và chính API trông như thế này:

using System;
using System.Collections;
using System.Collections.Generic;

public static class KeyEqualityComparer
{
    public static IEqualityComparer<T> Null<T>()
    {
        return null;
    }

    public static IEqualityComparer<T> EqualityComparerBy<T, K>(
        this IEnumerable<T> source,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc);
    }

    public static KeyEqualityComparer<T, K> ThenBy<T, K>(
        this IEqualityComparer<T> equalityComparer,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc, equalityComparer);
    }
}

public struct KeyEqualityComparer<T, K>: IEqualityComparer<T>
{
    public KeyEqualityComparer(
        Func<T, K> keyFunc,
        IEqualityComparer<T> equalityComparer = null)
    {
        KeyFunc = keyFunc;
        EqualityComparer = equalityComparer;
    }

    public bool Equals(T x, T y)
    {
        return ((EqualityComparer == null) || EqualityComparer.Equals(x, y)) &&
                EqualityComparer<K>.Default.Equals(KeyFunc(x), KeyFunc(y));
    }

    public int GetHashCode(T obj)
    {
        var hash = EqualityComparer<K>.Default.GetHashCode(KeyFunc(obj));

        if (EqualityComparer != null)
        {
            var hash2 = EqualityComparer.GetHashCode(obj);

            hash ^= (hash2 << 5) + hash2;
        }

        return hash;
    }

    public readonly Func<T, K> KeyFunc;
    public readonly IEqualityComparer<T> EqualityComparer;
}

Thông tin chi tiết có trên trang web của chúng tôi: IEqualityComparer trong LINQ .


5

Bạn có thể sử dụng DistincBy () để nhận các bản ghi riêng biệt bởi một thuộc tính đối tượng. Chỉ cần thêm câu lệnh sau trước khi sử dụng nó:

sử dụng Microsoft.Ajax.Utilities;

và sau đó sử dụng nó như sau:

var listToReturn = responseList.DistinctBy(x => x.Index).ToList();

trong đó 'Index' là thuộc tính mà tôi muốn dữ liệu được phân biệt.


4

Bạn có thể làm điều đó (mặc dù không nhanh như chớp) như vậy:

people.Where(p => !people.Any(q => (p != q && p.Id == q.Id)));

Đó là, "chọn tất cả những người không có người khác trong danh sách có cùng ID."

Nhắc bạn, trong ví dụ của bạn, điều đó sẽ chỉ chọn người 3. Tôi không chắc làm thế nào để biết bạn muốn gì, ngoài hai người trước.


4

Nếu bạn không muốn thêm thư viện MoreLinq vào dự án của mình chỉ để có được DistinctBychức năng thì bạn có thể nhận được kết quả cuối cùng tương tự bằng cách sử dụng quá tải Distinctphương thức của Linq trong một IEqualityComparerđối số.

Bạn bắt đầu bằng cách tạo một lớp so sánh đẳng thức tùy chỉnh chung sử dụng cú pháp lambda để thực hiện so sánh tùy chỉnh hai trường hợp của một lớp chung:

public class CustomEqualityComparer<T> : IEqualityComparer<T>
{
    Func<T, T, bool> _comparison;
    Func<T, int> _hashCodeFactory;

    public CustomEqualityComparer(Func<T, T, bool> comparison, Func<T, int> hashCodeFactory)
    {
        _comparison = comparison;
        _hashCodeFactory = hashCodeFactory;
    }

    public bool Equals(T x, T y)
    {
        return _comparison(x, y);
    }

    public int GetHashCode(T obj)
    {
        return _hashCodeFactory(obj);
    }
}

Sau đó, trong mã chính của bạn, bạn sử dụng nó như vậy:

Func<Person, Person, bool> areEqual = (p1, p2) => int.Equals(p1.Id, p2.Id);

Func<Person, int> getHashCode = (p) => p.Id.GetHashCode();

var query = people.Distinct(new CustomEqualityComparer<Person>(areEqual, getHashCode));

Voila! :)

Trên đây giả định như sau:

  • Tài sản Person.Idthuộc loạiint
  • Bộ peoplesưu tập không chứa bất kỳ phần tử null nào

Nếu bộ sưu tập có thể chứa null thì chỉ cần viết lại lambdas để kiểm tra null, ví dụ:

Func<Person, Person, bool> areEqual = (p1, p2) => 
{
    return (p1 != null && p2 != null) ? int.Equals(p1.Id, p2.Id) : false;
};

BIÊN TẬP

Cách tiếp cận này tương tự như trong câu trả lời của Vladimir Nesterovsky nhưng đơn giản hơn.

Nó cũng tương tự như câu trả lời của Joel nhưng cho phép logic so sánh phức tạp liên quan đến nhiều thuộc tính.

Tuy nhiên, nếu các đối tượng của bạn chỉ có thể khác nhau trước Idđó thì một người dùng khác đã đưa ra câu trả lời chính xác rằng tất cả những gì bạn cần làm là ghi đè các cài đặt mặc định GetHashCode()Equals()trong Personlớp của bạn , sau đó chỉ cần sử dụng Distinct()phương thức ngoài luồng của Linq để lọc ra bất kỳ bản sao.


Tôi chỉ muốn nhận các mục duy nhất trong dictonary, Bạn có thể vui lòng giúp tôi không y.SafeField (fldParamValue11, NULL_ID_VALUE))
RSB

2

Cách tốt nhất để làm điều này sẽ tương thích với các phiên bản .NET khác là ghi đè Equals và GetHash để xử lý việc này (xem câu hỏi Stack Overflow Mã này trả về các giá trị riêng biệt. Tuy nhiên, điều tôi muốn là trả về một bộ sưu tập được gõ mạnh thay vì một loại ẩn danh ), nhưng nếu bạn cần một cái gì đó chung chung trong mã của bạn, các giải pháp trong bài viết này là tuyệt vời.


1
List<Person>lst=new List<Person>
        var result1 = lst.OrderByDescending(a => a.ID).Select(a =>new Player {ID=a.ID,Name=a.Name} ).Distinct();

Ý của bạn là Select() new Personthay vì new Player? Tuy nhiên, thực tế là bạn đang đặt hàng bằng IDcách nào đó không thông báo Distinct()để sử dụng tài sản đó để xác định tính duy nhất, do đó, điều này sẽ không hoạt động.
BACON

1

Ghi đè bằng (đối tượng obj)GetHashCode () :

class Person
{
    public int Id { get; set; }
    public int Name { get; set; }

    public override bool Equals(object obj)
    {
        return ((Person)obj).Id == Id;
        // or: 
        // var o = (Person)obj;
        // return o.Id == Id && o.Name == Name;
    }
    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

và sau đó chỉ cần gọi:

List<Person> distinctList = new[] { person1, person2, person3 }.Distinct().ToList();

Tuy nhiên, GetHashCode () nên được nâng cao hơn (để tính cả Tên), theo tôi, câu trả lời này có lẽ là tốt nhất. Trên thực tế, để lưu trữ logic đích, không cần ghi đè GetHashCode (), Equals () là đủ, nhưng nếu chúng ta cần hiệu năng, chúng ta phải ghi đè lên nó. Tất cả các phép so sánh, trước tiên hãy kiểm tra hàm băm và nếu chúng bằng nhau thì hãy gọi Equals ().
Oleg Skripnyak

Ngoài ra, trong Equals () dòng đầu tiên phải là "if (! (Obj is Person)) return false". Nhưng thực tiễn tốt nhất là sử dụng các đối tượng riêng biệt được đúc thành một loại, như "var o = obj as Person; if (o == null) return false;" sau đó kiểm tra sự bình đẳng với o mà không cần đúc
Oleg Skripnyak

1
Ghi đè Bình đẳng như thế này không phải là một ý tưởng hay vì nó có thể gây ra hậu quả không lường trước cho các lập trình viên khác mong muốn Bình đẳng của Người được xác định trên nhiều tài sản.
B2K

0

Bạn sẽ có thể ghi đè Equals trên người để thực sự làm Equals trên Person.id. Điều này nên dẫn đến hành vi bạn sau.


-5

Vui lòng thử với mã dưới đây.

var Item = GetAll().GroupBy(x => x .Id).ToList();

3
Một câu trả lời ngắn được chào đón, tuy nhiên nó sẽ không cung cấp nhiều giá trị cho những người dùng sau, những người đang cố gắng hiểu những gì đang xảy ra đằng sau vấn đề. Xin vui lòng dành chút thời gian để giải thích vấn đề thực sự gây ra vấn đề và cách giải quyết. Cảm ơn bạn ~
Nghe
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.