Khác biệt () với lambda?


746

Phải, vì vậy tôi có một số lượng lớn và mong muốn nhận được các giá trị khác biệt từ nó.

Sử dụng System.Linq, tất nhiên có một phương pháp mở rộng được gọi là Distinct. Trong trường hợp đơn giản, nó có thể được sử dụng mà không có tham số, như:

var distinctValues = myStringList.Distinct();

Tốt và tốt, nhưng nếu tôi có vô số đối tượng mà tôi cần chỉ định bình đẳng, thì quá tải có sẵn duy nhất là:

var distinctValues = myCustomerList.Distinct(someEqualityComparer);

Đối số so sánh bằng phải là một thể hiện của IEqualityComparer<T>. Tôi có thể làm điều này, tất nhiên, nhưng nó hơi dài dòng và, tốt, vụng về.

Những gì tôi mong đợi là một sự quá tải sẽ mất một lambda, giả sử Func <T, T, bool>:

var distinctValues
    = myCustomerList.Distinct((c1, c2) => c1.CustomerId == c2.CustomerId);

Bất cứ ai cũng biết nếu một số phần mở rộng như vậy tồn tại, hoặc một số cách giải quyết tương đương? Hay tôi đang thiếu một cái gì đó?

Ngoài ra, có cách nào để chỉ định nội tuyến IEqualityComparer (embarass me) không?

Cập nhật

Tôi tìm thấy một câu trả lời của Anders Hejlsberg cho một bài đăng trên một diễn đàn MSDN về chủ đề này. Anh ta nói:

Vấn đề bạn sẽ gặp phải là khi hai đối tượng so sánh bằng nhau, chúng phải có cùng giá trị trả về GetHashCode (hoặc nếu không, bảng băm được sử dụng bên trong bởi Distinc sẽ không hoạt động chính xác). Chúng tôi sử dụng IEqualityComparer vì nó gói các triển khai tương đương của Equals và GetHashCode vào một giao diện duy nhất.

Tôi cho rằng nó có ý nghĩa..


2
xem stackoverflow.com/questions/1183403/ cho một giải pháp bằng GroupBy

17
Cảm ơn về bản cập nhật của Anders Hejlsberg!
Tor Haugen

Không, nó không có ý nghĩa - làm thế nào hai đối tượng chứa các giá trị giống hệt nhau có thể trả về hai mã băm khác nhau ??
GY

Nó có thể giúp - giải pháp cho .Distinct(new KeyEqualityComparer<Customer,string>(c1 => c1.CustomerId))và giải thích tại sao GetHashCode () lại quan trọng để hoạt động đúng.
marbel82

Bản sao liên quan / có thể có của: LINQ's
Distinc

Câu trả lời:


1029
IEnumerable<Customer> filteredList = originalList
  .GroupBy(customer => customer.CustomerId)
  .Select(group => group.First());

12
Thông minh! Điều này thực sự dễ dàng gói gọn trong một phương thức mở rộng, như DistinctBy(hoặc thậm chí Distinct, vì chữ ký sẽ là duy nhất).
Tomas Aschan

1
Không làm việc cho tôi! <Phương thức 'Đầu tiên' chỉ có thể được sử dụng làm thao tác truy vấn cuối cùng. Thay vào đó, hãy xem xét sử dụng phương thức 'FirstOrDefault'.> Ngay cả khi tôi đã thử 'FirstOrDefault', nó không hoạt động.
JatSing

63
@TorHaugen: Chỉ cần lưu ý rằng có một chi phí liên quan đến việc tạo ra tất cả các nhóm đó. Điều này không thể truyền phát dữ liệu đầu vào và cuối cùng sẽ đệm tất cả dữ liệu trước khi trả lại mọi thứ. Điều đó có thể không phù hợp với hoàn cảnh của bạn, nhưng tôi thích sự thanh lịch của DistincBy :)
Jon Skeet

2
@JonSkeet: Điều này đủ tốt cho các lập trình viên VB.NET, những người không muốn nhập một thư viện bổ sung cho chỉ một tính năng. Không có ASync CTP, VB.NET không hỗ trợ yieldcâu lệnh nên việc phát trực tuyến là không thể. Cảm ơn câu trả lời của bạn mặc dù. Tôi sẽ sử dụng nó khi mã hóa trong C #. ;-)
Alex Essilfie

2
@BenGripka: Điều đó không hoàn toàn giống nhau. Nó chỉ cung cấp cho bạn id khách hàng. Tôi muốn toàn bộ khách hàng :)
ryanman

496

Có vẻ như tôi muốn DistinctBytừ MoreLINEQ . Sau đó bạn có thể viết:

var distinctValues = myCustomerList.DistinctBy(c => c.CustomerId);

Đây là phiên bản rút gọn của DistinctBy(không kiểm tra vô hiệu và không có tùy chọn để chỉ định bộ so sánh khóa của riêng bạn):

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

14
Tôi biết câu trả lời tốt nhất sẽ được đăng bởi Jon Skeet chỉ bằng cách đọc tiêu đề của bài viết. Nếu nó có bất cứ điều gì để làm với LINQ, Skeet là người đàn ông của bạn. Đọc 'C # In Depth' để đạt được kiến ​​thức linq giống như Chúa.
nocarrier

2
câu trả lời chính xác!!! ngoài ra, đối với tất cả VB_Complainers về yield+ lib thêm, foreach có thể được viết lại thànhreturn source.Where(element => knownKeys.Add(keySelector(element)));
denis morozov

5
@ sudhAnsu63 đây là một hạn chế của LinqToSql (và các nhà cung cấp linq khác). Mục đích của LinqToX là dịch biểu thức lambda C # của bạn sang ngữ cảnh gốc của X. Nghĩa là, LinqToSql chuyển đổi C # của bạn thành SQL và thực thi lệnh đó bất cứ khi nào có thể. Điều này có nghĩa là bất kỳ phương thức nào có trong C # đều không thể được "chuyển qua" một linqProvider nếu không có cách nào để diễn đạt nó bằng SQL (hoặc bất kỳ nhà cung cấp linq nào bạn đang sử dụng). Tôi thấy điều này trong các phương thức mở rộng để chuyển đổi các đối tượng dữ liệu để xem các mô hình. Bạn có thể giải quyết vấn đề này bằng cách "cụ thể hóa" truy vấn, gọi ToList () trước DistincBy ().
Michael Blackburn

1
Và bất cứ khi nào tôi quay lại câu hỏi này, tôi cứ tự hỏi tại sao họ không áp dụng ít nhất một số MoreLinq vào BCL.
Shimmy Weitzhandler

2
@Shimmy: Tôi chắc chắn hoan nghênh rằng ... Tôi không chắc tính khả thi là gì. Tôi có thể nâng nó trong .NET Foundation mặc dù ...
Jon Skeet

39

Để bọc mọi thứ lên . Tôi nghĩ rằng hầu hết những người đến đây như tôi đều muốn giải pháp đơn giản nhất có thể mà không cần sử dụng bất kỳ thư viện nào và với hiệu suất tốt nhất có thể .

(Nhóm được chấp nhận theo phương pháp đối với tôi, tôi nghĩ là quá mức về mặt hiệu suất.)

Đây là một phương thức mở rộng đơn giản sử dụng giao diện IEqualityComparer cũng hoạt động với các giá trị null.

Sử dụng:

var filtered = taskList.DistinctBy(t => t.TaskExternalId).ToArray();

Mã phương thức mở rộng

public static class LinqExtensions
{
    public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> items, Func<T, TKey> property)
    {
        GeneralPropertyComparer<T, TKey> comparer = new GeneralPropertyComparer<T,TKey>(property);
        return items.Distinct(comparer);
    }   
}
public class GeneralPropertyComparer<T,TKey> : IEqualityComparer<T>
{
    private Func<T, TKey> expr { get; set; }
    public GeneralPropertyComparer (Func<T, TKey> expr)
    {
        this.expr = expr;
    }
    public bool Equals(T left, T right)
    {
        var leftProp = expr.Invoke(left);
        var rightProp = expr.Invoke(right);
        if (leftProp == null && rightProp == null)
            return true;
        else if (leftProp == null ^ rightProp == null)
            return false;
        else
            return leftProp.Equals(rightProp);
    }
    public int GetHashCode(T obj)
    {
        var prop = expr.Invoke(obj);
        return (prop==null)? 0:prop.GetHashCode();
    }
}

19

Không có không có quá tải phương pháp mở rộng cho việc này. Tôi đã thấy điều này làm tôi bực bội trong quá khứ và vì vậy tôi thường viết một lớp người trợ giúp để giải quyết vấn đề này. Mục đích là để chuyển đổi một Func<T,T,bool>để IEqualityComparer<T,T>.

Thí dụ

public class EqualityFactory {
  private sealed class Impl<T> : IEqualityComparer<T,T> {
    private Func<T,T,bool> m_del;
    private IEqualityComparer<T> m_comp;
    public Impl(Func<T,T,bool> del) { 
      m_del = del;
      m_comp = EqualityComparer<T>.Default;
    }
    public bool Equals(T left, T right) {
      return m_del(left, right);
    } 
    public int GetHashCode(T value) {
      return m_comp.GetHashCode(value);
    }
  }
  public static IEqualityComparer<T,T> Create<T>(Func<T,T,bool> del) {
    return new Impl<T>(del);
  }
}

Điều này cho phép bạn viết như sau

var distinctValues = myCustomerList
  .Distinct(EqualityFactory.Create((c1, c2) => c1.CustomerId == c2.CustomerId));

8
Điều đó có một triển khai mã băm khó chịu mặc dù. Dễ dàng hơn để tạo một IEqualityComparer<T>từ một phép chiếu: stackoverflow.com/questions/188120/ Kẻ
Jon Skeet

7
(Chỉ để giải thích nhận xét của tôi về mã băm - rất dễ dàng để mã này kết thúc bằng Equals (x, y) == true, nhưng GetHashCode (x)! = GetHashCode (y). Điều đó về cơ bản phá vỡ mọi thứ như hashtable .)
Jon Skeet

Tôi đồng ý với sự phản đối mã băm. Tuy nhiên, +1 cho mẫu.
Tor Haugen

@Jon, vâng, tôi đồng ý rằng việc triển khai GetHashcode ban đầu ít hơn tối ưu (là lười biếng). Tôi đã chuyển nó sang sử dụng ngay bây giờ EqualityComparer <T> .Default.GetHashcode () chuẩn hơn một chút. Mặc dù vậy, thực tế, điều duy nhất được đảm bảo để thực hiện GetHashcode trong kịch bản này là chỉ trả về một giá trị không đổi. Giết tìm kiếm hashtable nhưng được đảm bảo là đúng chức năng.
JaredPar

1
@JaredPar: Chính xác. Mã băm phải phù hợp với chức năng bình đẳng mà bạn đang sử dụng, có lẽ không phải là mặc định nếu không bạn sẽ không bận tâm :) Đó là lý do tại sao tôi thích sử dụng phép chiếu - bạn có thể có cả hàm bằng và hàm băm hợp lý mã theo cách đó. Nó cũng làm cho mã cuộc gọi có ít sự trùng lặp. Phải thừa nhận rằng nó chỉ hoạt động trong trường hợp bạn muốn chiếu hai lần, nhưng đó là mọi trường hợp tôi từng thấy trong thực tế :)
Jon Skeet

18

Giải pháp tốc ký

myCustomerList.GroupBy(c => c.CustomerId, (key, c) => c.FirstOrDefault());

1
Bạn có thể thêm một số giải thích tại sao điều này được cải thiện?
Keith Pinson

Điều này thực sự hiệu quả với tôi khi Konrad không làm thế.
mô tả

13

Điều này sẽ làm những gì bạn muốn nhưng tôi không biết về hiệu suất:

var distinctValues =
    from cust in myCustomerList
    group cust by cust.CustomerId
    into gcust
    select gcust.First();

Ít nhất nó không dài dòng.


12

Đây là một phương thức mở rộng đơn giản thực hiện những gì tôi cần ...

public static class EnumerableExtensions
{
    public static IEnumerable<TKey> Distinct<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector)
    {
        return source.GroupBy(selector).Select(x => x.Key);
    }
}

Thật xấu hổ khi họ không nướng một phương pháp khác biệt như thế này vào khung, nhưng hey ho.


đây là giải pháp tốt nhất mà không cần phải thêm thư viện morelinq.
chập chững

Nhưng, tôi đã phải thay đổi x.Keyđể x.First()và thay đổi giá trị trở lạiIEnumerable<T>
toddmo

@toddmo Cảm ơn phản hồi :-) Vâng, nghe có vẻ hợp lý ... Tôi sẽ cập nhật câu trả lời sau khi điều tra thêm.
David Kirkland

1
Không bao giờ là quá muộn để nói lời cảm ơn về giải pháp, đơn giản và sạch sẽ
Ali

4

Một cái gì đó tôi đã sử dụng mà làm việc tốt cho tôi.

/// <summary>
/// A class to wrap the IEqualityComparer interface into matching functions for simple implementation
/// </summary>
/// <typeparam name="T">The type of object to be compared</typeparam>
public class MyIEqualityComparer<T> : IEqualityComparer<T>
{
    /// <summary>
    /// Create a new comparer based on the given Equals and GetHashCode methods
    /// </summary>
    /// <param name="equals">The method to compute equals of two T instances</param>
    /// <param name="getHashCode">The method to compute a hashcode for a T instance</param>
    public MyIEqualityComparer(Func<T, T, bool> equals, Func<T, int> getHashCode)
    {
        if (equals == null)
            throw new ArgumentNullException("equals", "Equals parameter is required for all MyIEqualityComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = getHashCode;
    }
    /// <summary>
    /// Gets the method used to compute equals
    /// </summary>
    public Func<T, T, bool> EqualsMethod { get; private set; }
    /// <summary>
    /// Gets the method used to compute a hash code
    /// </summary>
    public Func<T, int> GetHashCodeMethod { get; private set; }

    bool IEqualityComparer<T>.Equals(T x, T y)
    {
        return EqualsMethod(x, y);
    }

    int IEqualityComparer<T>.GetHashCode(T obj)
    {
        if (GetHashCodeMethod == null)
            return obj.GetHashCode();
        return GetHashCodeMethod(obj);
    }
}

@Mukus Tôi không chắc tại sao bạn lại hỏi về tên lớp ở đây. Tôi cần đặt tên cho lớp một cái gì đó để triển khai IEqualityComparer vì vậy tôi chỉ cần thêm tiền tố vào My.
Kleinux

4

Tất cả các giải pháp tôi đã thấy ở đây dựa trên việc chọn một trường đã so sánh. Tuy nhiên, nếu người ta cần so sánh theo một cách khác, thì giải pháp này ở đây dường như hoạt động chung, cho một cái gì đó như:

somedoubles.Distinct(new LambdaComparer<double>((x, y) => Math.Abs(x - y) < double.Epsilon)).Count()

LambdaComparer là gì, bạn đang nhập nó từ đâu?
Patrick Graham

@PatrickGraham liên kết trong câu trả lời: brendan.enrick.com/post/iêng
Dmitry Ledentsov

3

Thực hiện một cách khác:

var distinctValues = myCustomerList.
Select(x => x._myCaustomerProperty).Distinct();

Trình tự trả về các phần tử riêng biệt so sánh chúng theo thuộc tính '_myCaustomerProperty'.


1
Đến đây để nói điều này. Đây phải là câu trả lời được chấp nhận
Still.Tony

5
Không, đây không phải là câu trả lời được chấp nhận, trừ khi tất cả những gì bạn muốn là giá trị riêng biệt của thuộc tính tùy chỉnh. Câu hỏi chung của OP là làm thế nào để trả về các đối tượng khác nhau dựa trên một thuộc tính cụ thể của đối tượng.
tomo

2

Bạn có thể sử dụng InlineComparer

public class InlineComparer<T> : IEqualityComparer<T>
{
    //private readonly Func<T, T, bool> equalsMethod;
    //private readonly Func<T, int> getHashCodeMethod;
    public Func<T, T, bool> EqualsMethod { get; private set; }
    public Func<T, int> GetHashCodeMethod { get; private set; }

    public InlineComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        if (equals == null) throw new ArgumentNullException("equals", "Equals parameter is required for all InlineComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = hashCode;
    }

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

    public int GetHashCode(T obj)
    {
        if (GetHashCodeMethod == null) return obj.GetHashCode();
        return GetHashCodeMethod(obj);
    }
}

Mẫu sử dụng :

  var comparer = new InlineComparer<DetalleLog>((i1, i2) => i1.PeticionEV == i2.PeticionEV && i1.Etiqueta == i2.Etiqueta, i => i.PeticionEV.GetHashCode() + i.Etiqueta.GetHashCode());
  var peticionesEV = listaLogs.Distinct(comparer).ToList();
  Assert.IsNotNull(peticionesEV);
  Assert.AreNotEqual(0, peticionesEV.Count);

Nguồn: https://stackoverflow.com/a/5969691/206730
Sử dụng IEqualityComparer cho Union
Tôi có thể chỉ định nội tuyến so sánh loại rõ ràng của mình không?


2

Bạn có thể sử dụng LambdaEqualityComparer:

var distinctValues
    = myCustomerList.Distinct(new LambdaEqualityComparer<OurType>((c1, c2) => c1.CustomerId == c2.CustomerId));


public class LambdaEqualityComparer<T> : IEqualityComparer<T>
    {
        public LambdaEqualityComparer(Func<T, T, bool> equalsFunction)
        {
            _equalsFunction = equalsFunction;
        }

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

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

        private readonly Func<T, T, bool> _equalsFunction;
    }

1

Một cách khéo léo để làm điều này là sử dụng Aggregate()phần mở rộng, sử dụng một từ điển như ắc với key-tài sản giá trị như các phím:

var customers = new List<Customer>();

var distincts = customers.Aggregate(new Dictionary<int, Customer>(), 
                                    (d, e) => { d[e.CustomerId] = e; return d; },
                                    d => d.Values);

Và một giải pháp kiểu GroupBy đang sử dụng ToLookup():

var distincts = customers.ToLookup(c => c.CustomerId).Select(g => g.First());

Đẹp, nhưng tại sao không chỉ tạo ra một Dictionary<int, Customer>thay thế?
ruffin

0

Tôi giả sử bạn có một IEnumerable và trong ví dụ đại biểu của bạn, bạn muốn c1 và c2 đề cập đến hai yếu tố trong danh sách này?

Tôi tin rằng bạn có thể đạt được điều này với một var varResults = từ c1 trong myList tham gia c2 trong myList trên


0

Nếu Distinct()không tạo ra kết quả duy nhất, hãy thử kết quả này:

var filteredWC = tblWorkCenter.GroupBy(cc => cc.WCID_I).Select(grp => grp.First()).Select(cc => new Model.WorkCenter { WCID = cc.WCID_I }).OrderBy(cc => cc.WCID); 

ObservableCollection<Model.WorkCenter> WorkCenter = new ObservableCollection<Model.WorkCenter>(filteredWC);

0

Gói Microsoft System.Interactive có phiên bản Phân biệt có bộ chọn khóa lambda. Đây thực sự giống như giải pháp của Jon Skeet, nhưng nó có thể hữu ích cho mọi người biết và kiểm tra phần còn lại của thư viện.


0

Đây là cách bạn có thể làm điều đó:

public static class Extensions
{
    public static IEnumerable<T> MyDistinct<T, V>(this IEnumerable<T> query,
                                                    Func<T, V> f, 
                                                    Func<IGrouping<V,T>,T> h=null)
    {
        if (h==null) h=(x => x.First());
        return query.GroupBy(f).Select(h);
    }
}

Phương pháp này cho phép bạn sử dụng nó bằng cách chỉ định một tham số như thế nào .MyDistinct(d => d.Name), nhưng nó cũng cho phép bạn chỉ định một điều kiện có tham số thứ hai như vậy:

var myQuery = (from x in _myObject select x).MyDistinct(d => d.Name,
        x => x.FirstOrDefault(y=>y.Name.Contains("1") || y.Name.Contains("2"))
        );

NB Điều này cũng sẽ cho phép bạn chỉ định các chức năng khác như ví dụ .LastOrDefault(...).


Nếu bạn muốn chỉ đưa ra điều kiện, bạn có thể có nó đơn giản hơn bằng cách thực hiện nó như sau:

public static IEnumerable<T> MyDistinct2<T, V>(this IEnumerable<T> query,
                                                Func<T, V> f,
                                                Func<T,bool> h=null
                                                )
{
    if (h == null) h = (y => true);
    return query.GroupBy(f).Select(x=>x.FirstOrDefault(h));
}

Trong trường hợp này, truy vấn sẽ trông giống như:

var myQuery2 = (from x in _myObject select x).MyDistinct2(d => d.Name,
                    y => y.Name.Contains("1") || y.Name.Contains("2")
                    );

NB Ở đây, biểu thức đơn giản hơn, nhưng lưu ý .MyDistinct2sử dụng .FirstOrDefault(...)ngầm.


Lưu ý: Các ví dụ trên đang sử dụng lớp demo sau

class MyObject
{
    public string Name;
    public string Code;
}

private MyObject[] _myObject = {
    new MyObject() { Name = "Test1", Code = "T"},
    new MyObject() { Name = "Test2", Code = "Q"},
    new MyObject() { Name = "Test2", Code = "T"},
    new MyObject() { Name = "Test5", Code = "Q"}
};

0

IEnumerable phần mở rộng lambda:

public static class ListExtensions
{        
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list, Func<T, int> hashCode)
    {
        Dictionary<int, T> hashCodeDic = new Dictionary<int, T>();

        list.ToList().ForEach(t => 
            {   
                var key = hashCode(t);
                if (!hashCodeDic.ContainsKey(key))
                    hashCodeDic.Add(key, t);
            });

        return hashCodeDic.Select(kvp => kvp.Value);
    }
}

Sử dụng:

class Employee
{
    public string Name { get; set; }
    public int EmployeeID { get; set; }
}

//Add 5 employees to List
List<Employee> lst = new List<Employee>();

Employee e = new Employee { Name = "Shantanu", EmployeeID = 123456 };
lst.Add(e);
lst.Add(e);

Employee e1 = new Employee { Name = "Adam Warren", EmployeeID = 823456 };
lst.Add(e1);
//Add a space in the Name
Employee e2 = new Employee { Name = "Adam  Warren", EmployeeID = 823456 };
lst.Add(e2);
//Name is different case
Employee e3 = new Employee { Name = "adam warren", EmployeeID = 823456 };
lst.Add(e3);            

//Distinct (without IEqalityComparer<T>) - Returns 4 employees
var lstDistinct1 = lst.Distinct();

//Lambda Extension - Return 2 employees
var lstDistinct = lst.Distinct(employee => employee.EmployeeID.GetHashCode() ^ employee.Name.ToUpper().Replace(" ", "").GetHashCode()); 
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.