与lambda不同()?


745

是的,所以我有一个不可估量的并希望从中获得独特的价值。

使用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论坛中有关此主题的帖子进行了回复。他说:

您将遇到的问题是,当两个对象比较相等时,它们必须具有相同的GetHashCode返回值(否则Distinct内部使用的哈希表将无法正常运行)。我们使用IEqualityComparer,因为它会将Equals和GetHashCode的兼容实现打包到单个接口中。

我想这是有道理的。


2
请参阅stackoverflow.com/questions/1183403/…, 了解使用GroupBy的解决方案

17
感谢Anders Hejlsberg更新!
Tor Haugen

不,这没有意义-包含相同值的两个对象如何返回两个不同的哈希码?
2015年

它可以帮助- 解决方案.Distinct(new KeyEqualityComparer<Customer,string>(c1 => c1.CustomerId)),并解释为什么GetHashCode()方法是很重要的工作正常。
marbel82

Answers:


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

12
优秀的!这真的很容易封装在扩展方法中,例如DistinctBy(甚至Distinct签名,因为签名将是唯一的)。
Tomas Aschan 2011年

1
对我不起作用!<方法“ First”只能用作最终查询操作。考虑改为在此实例中使用方法“ FirstOrDefault”。>即使我尝试了“ FirstOrDefault”,它也不起作用。
JatSing 2011年

63
@TorHaugen:请注意,创建所有这些组涉及成本。这不能流输入,并且最终将在返回任何内容之前缓冲所有数据。当然,这可能与您的情况无关,但我更喜欢DistinctBy的优雅:)
Jon Skeet

2
@JonSkeet:这对于不想为一个功能导入其他库的VB.NET编码人员来说已经足够了。如果没有ASync CTP,VB.NET将不支持该yield语句,因此从技术上讲不可能进行流传输。谢谢您的回答。我将在使用C#进行编码时使用它。;-)
Alex Essilfie

2
@BenGripka:不太一样。它只为您提供客户ID。我想要整个客户:)
ryanman

495

在我看来,您想要DistinctBy来自MoreLINQ。然后,您可以编写:

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

这是的简化版本DistinctBy(没有无效检查,也没有指定您自己的密钥比较器的选项):

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
我知道最好的答案将是Jon Skeet通过阅读帖子标题即可发布。如果它与LINQ有关系,Skeet就是您的好人。阅读“ C#In Depth”以获取类似于上帝的linq知识。
nocarrier 2014年

2
好答案!!!同样,对于所有有关yield+ Extra lib的VB_Complainers ,foreach可以重写为return source.Where(element => knownKeys.Add(keySelector(element)));
denis morozov 2014年

5
@ sudhAnsu63这是LinqToSql(和其他linq提供程序)的限制。LinqToX的目的是将您的C#lambda表达式转换为X的本机上下文。也就是说,LinqToSql将您的C#转换为SQL并在可能的情况下本地执行该命令。这意味着,如果无法在SQL(或您使用的任何linq提供程序)中表达它,则无法将驻留在C#中的任何方法“传递”给linqProvider。我在扩展方法中看到了这一点,该方法将数据对象转换为视图模型。您可以通过“具体化”查询,在DistinctBy()之前调用ToList()来解决此问题。
迈克尔·布莱克本

1
每当我回到这个问题时,我都会一直想知道他们为什么不至少将MoreLinq纳入BCL。
Shimmy Weitzhandler,

2
@Shimmy:我当然会欢迎...我不确定可行性。我可以在.NET Foundation中提高它
乔恩·斯基特

39

收拾东西。我认为像我这样来这里的大多数人都希望有最简单的解决方案,而无需使用任何库,并且要获得最佳性能

(我认为按方法分组的表现对我来说是一个过大的杀伤力。)

这是使用IEqualityComparer接口的简单扩展方法,该方法也适用于空值。

用法:

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

扩展方法代码

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

没有,为此没有这种扩展方法重载。过去,我发现这很令人沮丧,因此,我通常编写一个帮助程序类来解决此问题。目标是将转换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));

8
那有一个讨厌的哈希码实现。它更容易创建一个IEqualityComparer<T>从投影:stackoverflow.com/questions/188120/...
乔恩斯基特

7
(只是为了解释我对哈希码的评论-这段代码很容易以Equals(x,y)== true结束,但是GetHashCode(x)!= GetHashCode(y)。这基本上破坏了像哈希表这样的东西。)
乔恩·斯基特

我同意哈希码异议。不过,模式需要+1。
Tor Haugen

@Jon,是的,我同意GetHashcode的原始实现不是最优的(很懒惰)。我将其切换为现在基本上使用EqualityComparer <T> .Default.GetHashcode(),它稍微更标准了。但实际上,在这种情况下,唯一可以保证GetHashcode实现的方法是简单地返回一个常数。终止哈希表查找,但保证在功能上正确。
JaredPar

1
@JaredPar:是的。哈希码必须与您正在使用的均等函数一致,这大概不是默认函数,否则您就不会打扰:)这就是为什么我更喜欢使用投影-您可以同时获得均等和合理的哈希这样编码。这也使调用代码具有较少的重复。诚然,它仅在需要两次相同投影的情况下有效,但这是我在实践中看到的每种情况:)
Jon Skeet

18

速记解决方案

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

1
您能否添加一些解释,以说明为什么它得到了改进?
基思·品森

实际上,当Konrad不在时,这对我来说很有效。
neoscribe'Apr

13

这会做您想要的,但我对性能一无所知:

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

至少它不是冗长的。


12

这是一个简单的扩展方法,可以满足我的需求。

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

他们没有在框架中引入像这样的独特方法真是可惜,但是嘿。


这是最好的解决方案,而无需添加该库morelinq。
toddmo 2014年

但是,我不得不更改x.Keyx.First()并将返回值更改为IEnumerable<T>
toddmo

@toddmo感谢您的反馈:-)是的,听起来合乎逻辑...在进一步调查之后,我将更新答案。
大卫·柯克兰

1
要说谢谢您的解决方案,简单而干净,永远不会太晚
Ali

4

我用过的东西对我来说很有效。

/// <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我不确定您为什么在这里询问班级名称。为了实现IEqualityComparer,我需要给该类命名,以便在My前面加上前缀。
Kleinux 2015年

4

我在这里看到的所有解决方案都依赖于选择一个已经具有可比性的领域。但是,如果需要以不同的方式进行比较,那么此解决方案似乎可以正常工作,例如:

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

什么是LambdaComparer,您是从哪里导入的?
Patrick Graham

@PatrickGraham在答案中链接:brendan.enrick.com/post/…–
德米特里·列登佐夫

3

采取另一种方式:

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

序列返回的不同元素通过属性'_myCaustomerProperty'比较它们。


1
来这里说这个。应该是公认的答案
Still.Tony

5
不,这不是公认的答案,除非您想要的只是自定义属性的不同值。OP的一般问题是如何根据对象的特定属性返回不同的对象。
tomo

2

您可以使用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);
    }
}

用法样本

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

来源:https : //stackoverflow.com/a/5969691/206730
将IEqualityComparer用于Union是否
可以内联指定我的显式类型比较器?


2

您可以使用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

一个复杂的方法是使用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());

不错,但是为什么不创建一个Dictionary<int, Customer>呢?
鲁芬

0

我假设您有一个IEnumerable,并且在示例委托中,您希望c1和c2引用此列表中的两个元素吗?

我相信您可以通过以下方式实现此目的:自我连接vardistinctResults =来自myList中的c1以及myList上的c2


0

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


0

您可以按照以下方法进行操作:

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

注意在这里,表达式更简单,但是note 隐含地.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"}
};

0

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()); 
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.