如何通过 Lambda 或 LINQ 从列表中获取不同的实例

Posted

技术标签:

【中文标题】如何通过 Lambda 或 LINQ 从列表中获取不同的实例【英文标题】:How to get distinct instance from a list by Lambda or LINQ 【发布时间】:2010-11-14 01:56:03 【问题描述】:

我有这样的课:

class MyClass<T> 
    public string value1  get; set; 
    public T objT  get; set; 

和这个类的列表。我想使用 .net 3.5 lambda 或 linq 通过不同的 value1 获取 MyClass 列表。我想这是可能的,而且比 .net 2.0 中缓存这样的列表的方式要简单得多:

List<MyClass<T>> list; 
...
List<MyClass<T>> listDistinct = new List<MyClass<T>>();
foreach (MyClass<T> instance in list)

    // some code to check if listDistinct does contain obj with intance.Value1
    // then listDistinct.Add(instance);

什么是 lambda 或 LINQ 方法?

【问题讨论】:

【参考方案1】:

Marcdahlbyk 的答案似乎都非常有效。我有一个更简单的解决方案。您可以使用GroupBy,而不是使用Distinct。它是这样的:

var listDistinct
    = list.GroupBy(
        i => i.value1,
        (key, group) => group.First()
    ).ToArray();

请注意,我已将两个函数传递给 GroupBy()。第一个是键选择器。第二个从每个组中只获得一个项目。根据您的问题,我认为First() 是正确的。如果你愿意,你可以写一个不同的。你可以试试Last() 看看我的意思。

我使用以下输入进行了测试:

var list = new [] 
    new  value1 = "ABC", objT = 0 ,
    new  value1 = "ABC", objT = 1 ,
    new  value1 = "123", objT = 2 ,
    new  value1 = "123", objT = 3 ,
    new  value1 = "FOO", objT = 4 ,
    new  value1 = "BAR", objT = 5 ,
    new  value1 = "BAR", objT = 6 ,
    new  value1 = "BAR", objT = 7 ,
    new  value1 = "UGH", objT = 8 ,
;

结果是:

// value1 = ABC, objT = 0 
// value1 = 123, objT = 2 
// value1 = FOO, objT = 4 
// value1 = BAR, objT = 5 
// value1 = UGH, objT = 8 

我还没有测试它的性能。我相信这个解决方案可能比使用Distinct 的解决方案慢一点。尽管有这个缺点,但有两个很大的优点:简单性和灵活性。通常,优先考虑简单而不是优化更好,但这实际上取决于您要解决的问题。

【讨论】:

非常有趣。实际上,Comparer 方法有一个限制:它只返回第一个找到的不同的。如果我需要灵活性,请在第二个、...或最后一个获得不同,不确定 group.xxx() 是否能够做到? 是的,你会的。只需将First() 替换为Last() 并查看。当然,如果需要,您可以进行任何其他复杂的选择。 @David:你应该考虑接受这个答案。它是解决您问题的灵活而优雅的解决方案。 这让我很头疼。谢谢! @RollRoll 然后你根据匿名类做复合键:data.GroupBy(x => new x.prop1, x.prop2 )。【参考方案2】:

嗯...我可能会写一个自定义的IEqualityComparer&lt;T&gt; 以便我可以使用:

var listDistinct = list.Distinct(comparer).ToList();

并通过 LINQ 编写比较器....

可能有点矫枉过正,但至少可以重复使用:

先使用:

static class Program 
    static void Main() 
        var data = new[] 
            new  Foo = 1,Bar = "a", new  Foo = 2,Bar = "b", new Foo = 1, Bar = "c"
        ;
        foreach (var item in data.DistinctBy(x => x.Foo))
            Console.WriteLine(item.Bar);
        
    

使用实用方法:

public static class ProjectionComparer

    public static IEnumerable<TSource> DistinctBy<TSource,TValue>(
        this IEnumerable<TSource> source,
        Func<TSource, TValue> selector)
    
        var comparer = ProjectionComparer<TSource>.CompareBy<TValue>(
            selector, EqualityComparer<TValue>.Default);
        return new HashSet<TSource>(source, comparer);
    

public static class ProjectionComparer<TSource>

    public static IEqualityComparer<TSource> CompareBy<TValue>(
        Func<TSource, TValue> selector)
    
        return CompareBy<TValue>(selector, EqualityComparer<TValue>.Default);
    
    public static IEqualityComparer<TSource> CompareBy<TValue>(
        Func<TSource, TValue> selector,
        IEqualityComparer<TValue> comparer)
    
        return new ComparerImpl<TValue>(selector, comparer);
    
    sealed class ComparerImpl<TValue> : IEqualityComparer<TSource>
    
        private readonly Func<TSource, TValue> selector;
        private readonly IEqualityComparer<TValue> comparer;
        public ComparerImpl(
            Func<TSource, TValue> selector,
            IEqualityComparer<TValue> comparer)
        
            if (selector == null) throw new ArgumentNullException("selector");
            if (comparer == null) throw new ArgumentNullException("comparer");
            this.selector = selector;
            this.comparer = comparer;
        

        bool IEqualityComparer<TSource>.Equals(TSource x, TSource y)
        
            if (x == null && y == null) return true;
            if (x == null || y == null) return false;
            return comparer.Equals(selector(x), selector(y));
        

        int IEqualityComparer<TSource>.GetHashCode(TSource obj)
        
            return obj == null ? 0 : comparer.GetHashCode(selector(obj));
        
    

【讨论】:

关于代码的一个问题:ProjectionComparer 是什么? .Net 类或 LINQ 或 IEnumerable 相关类,以便您可以自定义扩展? 好的。我认为“ProjectionComparer”是您定义的任何类名,但在该类中您已将扩展方法 DistinctBy() 自定义为 IEnumerable,并且 ProjectionComparer 是另一个帮助器类,对吗? ProjectionComparer 可以是不同的名称,而不是相同的名称吗? 如果我想获取 MyClass 的 value1 列表,我可以像这样使用这个比较器: List listValue1s = list.Distinct(comparer).ToList().Select(y => y.value1);对吗? ProjectionComparer 的名称无关紧要 - 您可以将其称为 EnumerableExtensions。 ProjectionComparer 之所以如此命名,是因为它通过投影提供了一个比较器,这是基于现有值(例如来自 MyClass 的 value1)获取新值的常用术语。对于你的最后一个问题:除非你需要,否则不要调用 ToList() 。如果你不打算使用 MyClass 对象的不同列表,那么你最好让你的 value1 像这样: IEnumerable value1s = list.Select(y => y.value1).Distinct( ); Marc,你有关于 jpbochi 的替代方法的 cmet 吗?似乎不需要写一个Comparer扩展类,而且很灵活。对于 LINQ-to-Object 的情况,似乎已经足够好了。【参考方案3】:

你可以使用这个扩展方法:

    IEnumerable<MyClass> distinctList = sourceList.DistinctBy(x => x.value1);

    public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
        this IEnumerable<TSource> source,
        Func<TSource, TKey> keySelector)
    
        var knownKeys = new HashSet<TKey>();
        return source.Where(element => knownKeys.Add(keySelector(element)));
    

【讨论】:

这似乎可行,但是当调用该方法并且没有立即评估时,例如使用ToList(),返回的可枚举在第一次评估时看起来正确,但在第二次评估后,可枚举为空。比如说,如果我有一个包含 4 个对象和 1 个重复项的列表,请调用此方法来获取一个具有 3 个对象的枚举。调用.Count() 返回3 然后再次调用它返回0。有什么想法吗?【参考方案4】:

查看Enumerable.Distinct(),它可以接受一个 IEqualityComparer:

class MyClassComparer<T> : IEqualityComparer<MyClass<T>>

    // Products are equal if their names and product numbers are equal.
    public bool Equals(MyClass<T> x, MyClass<T>y)
    
        // Check whether the compared objects reference the same data.
        if (Object.ReferenceEquals(x, y)) return true;

        // Check whether any of the compared objects is null.
        if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
            return false;

        // Check whether the products' properties are equal.
        return x.value1 == y.value1;
    

    // If Equals() returns true for a pair of objects,
    // GetHashCode must return the same value for these objects.

    public int GetHashCode(MyClass<T> x)
    
        // Check whether the object is null.
        if (Object.ReferenceEquals(x, null)) return 0;

        // Get the hash code for the Name field if it is not null.
        return (x.value1 ?? "").GetHashCode();
    

您的代码 sn-p 可能如下所示:

List<MyClass<T>> list; 
...
List<MyClass<T>> listDistinct = list.Distinct(new MyClassComparer<T>).ToList();

【讨论】:

我认为每种方法都有优点。 group-by 方法需要最少的代码并且可以更灵活,但具有(轻微的)性能损失,并且乍一看代码的目的并不那么明显。 Marc 的通用解决方案读起来很流畅,但有些人可能会说单个表达​​式做得太多:它既指定了项目的比较方式,又执行了实际的 select-distinct。我的方法更具体,但在等效逻辑和利用它的操作之间提供了清晰的分离。 感谢您完整的 cmets。我同意你的可读性和分离性。然而,就在第二个或最后一个获得 T 实例的灵活性而言,Comparer 只获得第一个,并且它可能会复杂到相同的灵活性,对吧?在 jpbochi 上查看我的 cmets。 确实,Distinct-with-Comparer 方法只会返回“集合”中的第一个。但是,我认为“不同”的语义是,如果对象符合您的标准,则它们应被视为等效。一旦你开始选择第一个或最后一个,你就真的从一个“不同的”计算转移到一个分组上的某种聚合(First、Last、Min 等等)。【参考方案5】:

这样会更简单...

var distinctList = list.GroupBy(l => l.value1, (key, c) => l.FirstOrDefault());

【讨论】:

应该是 c.FirstOrDefault() 而不是 l.FirstOrDefault()。 this 答案中添加了类似的评论。【参考方案6】:

在 linq 中这是更先进的分组

list.GroupBy(li => li.value, (key, grp) => li.FirstOrDefault());

【讨论】:

我相信它实际上应该是:list.GroupBy(li => li.value, (key, grp) => grp.First());【参考方案7】:

我接受了 Marc 的回答,将其修复为使用 TSource 作为值类型(测试 default(TSource) 而不是 null),清理了一些冗余类型规范,并为它编写了一些测试。这是我今天使用的。感谢 Marc 的好主意和实施。

public static class LINQExtensions

    public static IEnumerable<TSource> DistinctBy<TSource, TValue>(
        this IEnumerable<TSource> source,
        Func<TSource, TValue> selector)
    
        var comparer = ProjectionComparer<TSource>.CompareBy(
            selector, EqualityComparer<TValue>.Default);
        return new HashSet<TSource>(source, comparer);
    

public static class ProjectionComparer<TSource>

    public static IEqualityComparer<TSource> CompareBy<TValue>(
        Func<TSource, TValue> selector)
    
        return CompareBy(selector, EqualityComparer<TValue>.Default);
    
    public static IEqualityComparer<TSource> CompareBy<TValue>(
        Func<TSource, TValue> selector,
        IEqualityComparer<TValue> comparer)
    
        return new ComparerImpl<TValue>(selector, comparer);
    
    sealed class ComparerImpl<TValue> : IEqualityComparer<TSource>
    
        private readonly Func<TSource, TValue> _selector;
        private readonly IEqualityComparer<TValue> _comparer;
        public ComparerImpl(
            Func<TSource, TValue> selector,
            IEqualityComparer<TValue> comparer)
        
            if (selector == null) throw new ArgumentNullException("selector");
            if (comparer == null) throw new ArgumentNullException("comparer");
            _selector = selector;
            _comparer = comparer;
        

        bool IEqualityComparer<TSource>.Equals(TSource x, TSource y)
        
            if (x.Equals(default(TSource)) && y.Equals(default(TSource)))
            
                return true;
            

            if (x.Equals(default(TSource)) || y.Equals(default(TSource)))
            
                return false;
            
            return _comparer.Equals(_selector(x), _selector(y));
        

        int IEqualityComparer<TSource>.GetHashCode(TSource obj)
        
            return obj.Equals(default(TSource)) ? 0 : _comparer.GetHashCode(_selector(obj));
        
    

还有测试类:

[TestClass]
public class LINQExtensionsTest

    [TestMethod]
    public void DistinctByTestDate()
    
        var list = Enumerable.Range(0, 200).Select(i => new
        
            Index = i,
            Date = DateTime.Today.AddDays(i%4)
        ).ToList();

        var distinctList = list.DistinctBy(l => l.Date).ToList();

        Assert.AreEqual(4, distinctList.Count);

        Assert.AreEqual(0, distinctList[0].Index);
        Assert.AreEqual(1, distinctList[1].Index);
        Assert.AreEqual(2, distinctList[2].Index);
        Assert.AreEqual(3, distinctList[3].Index);

        Assert.AreEqual(DateTime.Today, distinctList[0].Date);
        Assert.AreEqual(DateTime.Today.AddDays(1), distinctList[1].Date);
        Assert.AreEqual(DateTime.Today.AddDays(2), distinctList[2].Date);
        Assert.AreEqual(DateTime.Today.AddDays(3), distinctList[3].Date);

        Assert.AreEqual(200, list.Count);
    

    [TestMethod]
    public void DistinctByTestInt()
    
        var list = Enumerable.Range(0, 200).Select(i => new
        
            Index = i % 4,
            Date = DateTime.Today.AddDays(i)
        ).ToList();

        var distinctList = list.DistinctBy(l => l.Index).ToList();

        Assert.AreEqual(4, distinctList.Count);

        Assert.AreEqual(0, distinctList[0].Index);
        Assert.AreEqual(1, distinctList[1].Index);
        Assert.AreEqual(2, distinctList[2].Index);
        Assert.AreEqual(3, distinctList[3].Index);

        Assert.AreEqual(DateTime.Today, distinctList[0].Date);
        Assert.AreEqual(DateTime.Today.AddDays(1), distinctList[1].Date);
        Assert.AreEqual(DateTime.Today.AddDays(2), distinctList[2].Date);
        Assert.AreEqual(DateTime.Today.AddDays(3), distinctList[3].Date);

        Assert.AreEqual(200, list.Count);
    

    struct EqualityTester
    
        public readonly int Index;
        public readonly DateTime Date;

        public EqualityTester(int index, DateTime date) : this()
        
            Index = index;
            Date = date;
        
    

    [TestMethod]
    public void TestStruct()
    
        var list = Enumerable.Range(0, 200)
            .Select(i => new EqualityTester(i, DateTime.Today.AddDays(i%4)))
            .ToList();

        var distinctDateList = list.DistinctBy(e => e.Date).ToList();
        var distinctIntList = list.DistinctBy(e => e.Index).ToList();

        Assert.AreEqual(4, distinctDateList.Count);
        Assert.AreEqual(200, distinctIntList.Count);
    

【讨论】:

以上是关于如何通过 Lambda 或 LINQ 从列表中获取不同的实例的主要内容,如果未能解决你的问题,请参考以下文章

如何从 PostgreSQL 查询转换为 LINQ 或 lambda 表达式

如何解决 C# 中 linq 的 lambda 表达式中的对象引用错误?

如何在没有 Linq 的情况下从列表中获取一些对象?

如何使用LINQ从列表中获取重复项?

如何通过 LINQ to Sql 结果上的数据对分组进行 lambda?

在c#中使用lambda或linq查找项目索引[关闭]