使用lambda的Distinct()函数?

833

好的,所以我有一个可枚举对象,希望从中获取不同的值。

使用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的方法(让我尴尬)?

更新

我在MSDN论坛上找到了Anders Hejlsberg对此主题的post的回复。他说:

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

我想这很有道理。


2
请参考使用 GroupBy 的解决方案,从列表中获取不同的实例:https://dev59.com/BnM_5IYBdhLWcg3w4njb - user943105
不,这没有意义 - 包含相同值的两个对象如何返回两个不同的哈希码呢?? - G.Y
1
它可以帮助- 解决方案 用于 .Distinct(new KeyEqualityComparer<Customer,string>(c1 => c1.CustomerId)),并解释为什么 GetHashCode() 的正常工作非常重要。 - marbel82
1
相关 / 可能是重复的问题:LINQ 在特定属性上使用 Distinct() - Marc.2377
3
现在您可以像**DistinctBy(x => x.CustomerId)**一样使用 .Net 6 内置的 DistinctBy 方法。 - Furkan Öztürk
显示剩余2条评论
20个回答

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

18
太棒了!这个可以很容易地封装在扩展方法中,就像 DistinctBy(甚至可以是 Distinct,因为签名将是唯一的)一样。 - Tomas Aschan
3
对我来说不起作用!<方法“First”只能用作最终查询操作。请考虑在此实例中使用方法“FirstOrDefault”代替。>即使我尝试了“FirstOrDefault”,它也没有起作用。 - JatSing
74
请注意,创建所有这些组需要成本。此方法无法实时传输输入数据,必须在返回任何内容之前将所有数据缓存。当然,这可能与您的情况无关,但我更喜欢DistinctBy的优雅。 - Jon Skeet
2
@JonSkeet:这对于不想为了一个功能导入额外库的VB.NET编程人员来说已经足够好了。没有ASync CTP,VB.NET不支持yield语句,因此技术上无法进行流式处理。不过还是谢谢你的回答。当我使用C#编码时,我会用到它的;-) - Alex Essilfie
4
不完全相同,它只给您客户ID。我想要整个顾客 :) - ryanman
显示剩余8条评论

547

我认为你想要使用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;
        }
    }
}

17
仅凭帖子标题,我就知道Jon Skeet会发布最好的答案。如果与LINQ有关,Skeet是你的专家。阅读《C#深入》可获得神一般的LINQ知识。 - Shawn J. Molloy
2
太棒了!此外,对于所有抱怨 yield 和额外库的 VB 用户们,可以将 foreach 重写为 return source.Where(element => knownKeys.Add(keySelector(element))); - denis morozov
6
这是 LinqToSql(以及其他Linq提供程序)的一种限制。LinqToX 的意图是将您的 C# lambda 表达式转换为 X 本地上下文。也就是说,LinqToSql 将您的 C# 转换为 SQL,并在可能的情况下本地执行该命令。这意味着,如果没有办法将任何存在于 C# 中的方法用 SQL (或者您使用的任何 Linq 提供程序) 表达,那么它就无法 "通过" linqProvider 进行传递。我在扩展方法中看到了这点,用于将数据对象转换为视图模型。您可以通过在 DistinctBy() 之前调用 ToList() 来解决这个问题,从而“实现”查询。 - Michael Blackburn
3
@Shimmy: 我肯定欢迎这样做……但我不确定可行性如何。不过我可以在 .NET 基金会提出这个想法…… - Jon Skeet
2
@Shimmy:Carlo的答案可能适用于LINQ to SQL...我不确定。 - Jon Skeet
显示剩余9条评论

47
概括一下:我认为大多数像我一样来到这里的人都希望找到最简单的解决方案,不使用任何库并且具有最佳的性能

(对我来说,接受的分组方法在性能方面有点过头了。)

以下是一个简单的扩展方法,它使用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();
    }
}

23

简写解决方案

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

1
你能添加一些为什么这是改进的解释吗? - Keith Pinson

21

没有这样的扩展方法重载。我自己以前也感到过沮丧,因此通常编写帮助类来解决这个问题。目标是将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));

9
虽然这个哈希代码实现很糟糕,但从一个映射中创建一个 IEqualityComparer<T> 更容易:https://dev59.com/fnVC5IYBdhLWcg3wxEN1 - Jon Skeet
7
关于我的哈希码评论,使用这个代码很容易导致Equals(x, y) == true,但GetHashCode(x) != GetHashCode(y)。这基本上会破坏任何像哈希表这样的东西。 - Jon Skeet
1
@JaredPar:没错。哈希码必须与你使用的相等性函数保持一致,否则你就不会费心了:) 这就是为什么我更喜欢使用投影 - 这样可以同时获得相等性和合理的哈希码。它还可以减少调用代码的重复。诚然,它只适用于你想要两次相同投影的情况,但这是我在实践中见过的每种情况 :) - Jon Skeet
我只有在把 <T,T> 替换成 <T> 时才能让它运行。否则会出现编译错误。我是不是漏掉了什么? - Uwe Keim
如果你只比较对象的一个成员,哈希码将完全忽略它,这样是行不通的。最好强制提供一个lambda表达式,或者使用lambda表达式和反射来获取成员访问器。 - UberFace
显示剩余2条评论

20

从.NET 6或更高版本开始,有一个新的内置方法Enumerable.DistinctBy可以实现这一点。

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

// With IEqualityComparer
var distinctValues = myCustomerList.DistinctBy(c => c.CustomerId, someEqualityComparer);

4
可以翻译为:“这应该是一个新的被接受的答案。” - TKharaishvili
@TKharaishvili 这个问题标记为 c#-3.0,所以虽然这个答案肯定是相关的,但它是否应该被接受还有待商榷。 - Guru Stron
抱歉,现在已经快到2023年了,如果你还在使用.NET 3.0,那么你已经超过了支持日期约10年左右,这应该是新的答案。 - Lawrence Thurman

14
这里有一个简单的扩展方法,可以满足我的需求...
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);
    }
}

很遗憾他们没有将这样的方法置入框架中,但是无论如何。


1
但是,我必须将 x.Key 更改为 x.First() 并将返回值更改为 IEnumerable<T> - toddmo
@toddmo 感谢您的反馈 :-) 是的,听起来很合理... 我会在进一步调查后更新答案。 - David Kirkland

13

这个会实现你想要的功能,但我不确定性能如何:

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

至少它不啰嗦。


4

我看到的所有解决方案都依赖于选择一个已经可比较的字段。然而,如果有人需要以不同的方式进行比较,这个解决方案似乎通常可以工作,例如:

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

LambdaComparer是什么,你从哪里导入它的? - Patrick Graham
@PatrickGraham 在答案中提供了链接:http://brendan.enrick.com/post/LINQ-Your-Collections-with-IEqualityComparer-and-Lambda-Expressions - Dmitry Ledentsov

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接