使用LINQ按类属性进行去重

194

我有一个集合:

List<Car> cars = new List<Car>();

汽车通过它们的属性CarCode进行唯一标识。

我有三辆汽车在集合中,其中两辆具有相同的CarCode。

如何使用LINQ将此集合转换为具有唯一CarCodes的汽车?


相关 / 可能是重复的问题:LINQ 在特定属性上使用 Distinct() - Marc.2377
9个回答

351
你可以使用分组,从每个组中获取第一辆汽车:
List<Car> distinct =
  cars
  .GroupBy(car => car.CarCode)
  .Select(g => g.First())
  .ToList();

我认为没有任何开销存在! - Amirhossein Mehrvarzi
8
有一些额外开销,因为要创建组,并且只使用每个组中的一个项目。 - Guffa
18
要获取更多的键,请写:.GroupBy(car =>new{ car.CarCode,car.PID,car.CID}) - Ali Rasouli
5
总的来说,你是正确的,但是由于只有在集合中存在匹配元素时才会创建一个组,每个组至少会有一个元素。因此,在这种情况下使用 First() 完全没有问题。 - Maximilian Ast
8
.NET 6 现在有了 DistinctBy 方法(https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.distinctby?view=net-6.0)。 - peflorencio
显示剩余4条评论

134

使用MoreLINQ,该库中有一个名为DistinctBy的方法 :)

IEnumerable<Car> distinctCars = cars.DistinctBy(car => car.CarCode);

(仅适用于LINQ to Objects。)


4
只提供链接!http://code.google.com/p/morelinq/source/browse/MoreLinq/?r=d4396b9ff63932be0ab07c36452a481d20f96307 - Diogo
1
嗨Jon,我有两个问题。1)为什么你不把库添加到Nuget中?2)关于LINQ to SQL\EF\NH,我们该如何实现它?我们必须使用Guffa版本(如果NO_HASHSET为真,则是您的版本...)吗?非常感谢! - gdoron
2
@gdoron:1)它已经在NuGet中了:http://www.nuget.org/packages/morelinq 2)我怀疑LINQ to SQL等是否足够灵活,能够允许这样做。 - Jon Skeet
哦,原来是预发布版...所以我找不到它。2)我担心将Lib添加到我的项目中,因为我担心有人会使用它与IQueryable<T>一起使用,并尝试对其进行DistinctBy,从而查询整个该死的表格...这不是容易出错吗?再次感谢您非常快速的回复! - gdoron
4
@Shimmy:个人而言,我会感到写 System 下的代码有点紧张,因为这给人一种它是“官方”的错误印象。当然,你的品味可能有所不同 :) - Jon Skeet
显示剩余4条评论

69

与Guffa的方法相同,但作为扩展方法:

public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> items, Func<T, TKey> property)
{
    return items.GroupBy(property).Select(x => x.First());
}

用作:

var uniqueCars = cars.DistinctBy(x => x.CarCode);

3
好的。这种方法也可以在Microsoft.Ajax.Utilities库中找到。 - Savage
1
请注意,在.NET 6和.NET 7预览版中,有一个更适合生产环境的版本可用。请参见: https://github.com/dotnet/runtime/blob/ebba1d4acb7abea5ba15e1f7f69d1d1311465d16/src/libraries/System.Linq/src/System/Linq/Distinct.cs 其中包括延迟执行和正确的错误处理。与可能发生的各种情况(包括错误条件)相比,上面的代码过于简单。 - Tore Aurstad

33

你可以实现一个IEqualityComparer,并在Distinct扩展方法中使用它。

class CarEqualityComparer : IEqualityComparer<Car>
{
    #region IEqualityComparer<Car> Members

    public bool Equals(Car x, Car y)
    {
        return x.CarCode.Equals(y.CarCode);
    }

    public int GetHashCode(Car obj)
    {
        return obj.CarCode.GetHashCode();
    }

    #endregion
}

然后

var uniqueCars = cars.Distinct(new CarEqualityComparer());

我们如何在不编写 "new CarEqualityComparer()" 的情况下使用它? - Parsa
3
@Parsa 你可以创建一个接受lambda表达式的IEqualitiyComparer包装类型。这将使其通用化:cars.Distinct(new GenericEqualityComparer<Car>((a,b) => a.CarCode == b.CarCode, x => x.CarCode.GetHashCode()))。我过去曾经使用过这样的方法,因为在执行一次性Distinct时,它有时会增加价值。 - user2864740

12

另一种用于Linq-to-Objects的扩展方法,不使用GroupBy:

    /// <summary>
    /// Returns the set of items, made distinct by the selected value.
    /// </summary>
    /// <typeparam name="TSource">The type of the source.</typeparam>
    /// <typeparam name="TResult">The type of the result.</typeparam>
    /// <param name="source">The source collection.</param>
    /// <param name="selector">A function that selects a value to determine unique results.</param>
    /// <returns>IEnumerable&lt;TSource&gt;.</returns>
    public static IEnumerable<TSource> Distinct<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
    {
        HashSet<TResult> set = new HashSet<TResult>();

        foreach(var item in source)
        {
            var selectedValue = selector(item);

            if (set.Add(selectedValue))
                yield return item;
        }
    }

7
我认为在性能(或任何方面)上最好的选择是使用IEqualityComparer接口进行去重。
尽管每次为每个类实现新的比较器都很麻烦并且会产生样板代码。
因此,这里有一个扩展方法,可以使用反射为任何类实时生成一个新的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();
    }
}

这里的反射在哪里? - MistyK

1

在没有进行额外的操作的情况下,你不能有效地对一组对象使用Distinct。我将解释原因。

文档中说:

它使用默认的相等比较器Default来比较值。

对于对象来说,这意味着它使用默认的等式方法来比较对象(来源)。也就是说,使用它们的哈希码。由于你的对象没有实现GetHashCode()Equals方法,它将检查对象的引用,而这些引用并不是唯一的。


0

完成相同任务的另一种方法...

List<Car> distinticBy = cars
    .Select(car => car.CarCode)
    .Distinct()
    .Select(code => cars.First(car => car.CarCode == code))
    .ToList();

可以创建一个扩展方法以更通用的方式实现此操作。如果有人能够评估这个“DistinctBy”与“GroupBy”方法的性能,那将是很有趣的。


1
第二个 Select 将是一个 O(n*m) 操作,因此它不会很好地扩展。如果有很多重复项,即如果第一个 Select 的结果是原始集合的非常小的一部分,则它可能会表现得更好。 - Guffa

0
你可以查看我的PowerfulExtensions库。目前它还处于非常年轻的阶段,但是你已经可以在任意数量的属性上使用Distinct、Union、Intersect、Except等方法;
这是如何使用它的:
using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);

如果我有一个对象列表,想要删除所有具有相同ID的对象,那么是不是应该使用 myList.Distinct(x => x.ID) - Thomas

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