c# - linq extension methods




獨特()與lambda? (12)

對,所以我有一個枚舉並希望從中獲得不同的值。

使用System.Linq ,當然有一個名為Distinct的擴展方法。 在簡單情況下,它可以不帶參數使用,如:

var distinctValues = myStringList.Distinct();

那麼好,但如果我有一個我需要指定相等的對象的枚舉,唯一可用的重載是:

var distinctValues = myCustomerList.Distinct(someEqualityComparer);

相等比較器參數必須是IEqualityComparer<T>一個實例。 當然,我可以做到這一點,但它有點冗長,而且很滑稽。

我所期望的是一個需要lambda的重載,比如Func <T,T,bool>:

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

任何人都知道是否存在這樣的擴展,或者一些等效的解決方法? 或者我錯過了什麼?

另外,有沒有一種方法來指定一個IEqualityComparer內聯(不容我)?

更新

我發現了Anders Hejlsberg在MSDN論壇上post的關於此主題的回复。 他說:

你將要遇到的問題是,當兩個對像比較相等時,它們必須具有相同的GetHashCode返回值(否則Distinct內部使用的哈希表將無法正常工作)。 我們使用IEqualityComparer,因為它將Equals和GetHashCode的兼容實現打包到一個接口中。

我想這是有道理的..


Microsoft System.Interactive包有一個Distinct版本,它需要一個密鑰選擇器lambda。 這與Jon Skeet的解決方案實際上是一樣的,但它可能有助於人們知道並查看圖書館的其餘部分。


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

用法:

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

一個棘手的方法是使用Aggregate()擴展,將字典用作累加器,並將鍵屬性值作為鍵:

var customers = new List<Customer>();

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

GroupBy風格的解決方案使用ToLookup()

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

不,沒有這樣的擴展方法過載。 過去我發現自己很沮喪,因此我通常會寫一個幫助類來處理這個問題。 目標是將Func<T,T,bool>IEqualityComparer<T,T>

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

這使您可以編寫以下內容

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

你可以這樣做:

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

此方法允許您通過指定一個參數(如.MyDistinct(d => d.Name)來使用它,但它也允許您將具有條件指定為第二個參數,如下所示:

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

注意這也可以讓你指定其他函數,例如.LastOrDefault(...)

如果您只想公開條件,可以通過將它實現為更簡單:

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

在這種情況下,查詢將如下所示:

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

注意這裡的表達更簡單,但注意.MyDistinct2隱式使用.FirstOrDefault(...)

注意:上面的示例使用以下演示類

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

如果Distinct()不會產生獨特的結果,請嘗試下面的一個:

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

我假設你有一個IEnumerable,並且在你的示例代表中,你想讓c1和c2指向這個列表中的兩個元素?

我相信你可以通過自我加入var distinctResults =從myList中的c1加入myList中的c2來實現此目的


我在這裡看到的所有解決方案都依賴於選擇一個可比較的領域。 但是,如果需要以不同的方式進行比較,那麼這種解決方案似乎總體上工作起來,例如:

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

換一種方式:

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

序列返回不同的元素通過屬性'_myCaustomerProperty'比較它們。


這將做你想要的,但我不知道性能:

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

至少它不是詳細的。


速記解決方案

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

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




extension-methods