在复杂对象(例如List<T>)上进行分组(GroupBy)

20

使用 GroupBy()Count() > 1,我试图在列表中查找我的类的重复实例。

该类看起来像这样:

public class SampleObject
{
    public string Id;
    public IEnumerable<string> Events;
}

这是我如何实例化和分组列表的方法:

public class Program
{
    private static void Main(string[] args)
    {
        var items = new List<SampleObject>()
        {
            new SampleObject() { Id = "Id", Events = new List<string>() { "ExampleEvent" } },
            new SampleObject() { Id = "Id", Events = new List<string>() { "ExampleEvent" } }
        };

        var duplicates = items.GroupBy(x => new { Token = x.Id, x.Events })
                         .Where(g => g.Count() > 1)
                         .Select(g => g.Key)
                         .ToList();
    }
}

duplicates不包含任何项。我该如何使分组工作?


2
默认情况下,列表不通过其项目的值进行比较。 - Sergey Berezovskiy
1
@SergeyBerezovskiy - 这不是问题所在。问题是缺乏GetHashCodeEquals的重写。 - Enigmativity
3
new { Token = x.Id, x.Events }这段代码没有重写SampleObjectEqualsGetHashCode方法,导致问题完全由x.Events的比较引起。请注意不要改变原意,使内容更易懂。 - Sergey Berezovskiy
2
虽然我不会使用逗号作为分隔符,但是@Farhad提出的解决方案(string.Join)是经典的,并且通常“足够好”。我会使用'\0'作为分隔符。另外一个“事情”...然后我会使用.Select(g => g.First())来获取重复的项目。 - xanatos
1
@SergeyBerezovskiy - 是的,说得对。我不得不使用原始对象的自定义比较器。 - Enigmativity
3个回答

27

为了让LINQ的诸多操作符(如GroupByDistinct)处理对象,你必须要么实现GetHashCodeEquals,要么提供一个自定义比较器。

在你的情况下,如果属性是一个列表,你可能需要一个比较器,除非你将该列表设置为只读。

尝试使用这个比较器:

public class SampleObjectComparer : IEqualityComparer<SampleObject>
{
    public bool Equals(SampleObject x, SampleObject y)
    {
        return x.Id == y.Id && x.Events.SequenceEqual(y.Events);
    }

    public int GetHashCode(SampleObject x)
    {
        return x.Id.GetHashCode() ^ x.Events.Aggregate(0, (a, y) => a ^ y.GetHashCode());
    }
}

现在这段代码可以正常运行:

    var items = new List<SampleObject>()
    {
        new SampleObject() { Id = "Id", Events = new List<string>() { "ExampleEvent"} },
        new SampleObject() { Id = "Id", Events = new List<string>() { "ExampleEvent" } }
    };

    var comparer = new SampleObjectComparer();

    var duplicates = items.GroupBy(x => x, comparer)
                     .Where(g => g.Count() > 1)
                     .Select(g => g.Key)
                     .ToList();

2

GroupBy()会执行默认比较,导致它找不到您的列表相等。

请参见以下代码:

var eventList1 = new List<string>() { "ExampleEvent" };
var eventList2 = new List<string>() { "ExampleEvent" };

Console.WriteLine(eventList1.GetHashCode());
Console.WriteLine(eventList2.GetHashCode());
Console.WriteLine(eventList1.Equals(eventList2));

两个“相等”的列表,对吧?然而,这段代码会输出:

796641852
1064243573
False

所以它们不被视为相等,因此不能分组。

您需要提供一个自定义比较器,用于比较对象的相关属性。请注意,如前所示,List<T>.GetHashCode() 不能正确地表示列表中的项目。

您可以这样做(来自Good GetHashCode() override for List of Foo objects respecting the orderLINQ GroupBy on multiple ref-type fields; Custom EqualityComparer):

public class SampleObjectComparer : IEqualityComparer<SampleObject>
{
    public bool Equals(SampleObject a, SampleObject b)
    {
        return a.Id == b.Id 
            && a.Events.SequenceEqual(b.Events);
    }

    public int GetHashCode(SampleObject a)
    {
        int hash = 17;

        hash = hash * 23 + a.Id.GetHashCode();

        foreach (var evt in a.Events)
        {
            hash = hash * 31 + evt.GetHashCode();
        }           

        return hash;
    }
}

并且像这样使用它:

var eventList1 = new List<string>() { "ExampleEvent" };
var eventList2 = new List<string>() { "ExampleEvent" };

var items = new List<SampleObject>()
{
    new SampleObject() { Id = "Id", Events = eventList1 },
    new SampleObject() { Id = "Id", Events = eventList2 }
};

var duplicates = items.GroupBy(x => x, new SampleObjectComparer())
                 .Where(g => g.Count() > 1)
                 .Select(g => g.Key)
                 .ToList();

Console.WriteLine(duplicates.Count);

1

List<T>没有重写Equals+ GetHashCode,这就是为什么你的GroupBy不能按预期工作的原因。匿名类型的两个属性之一引用了列表,当GroupBy需要比较两个列表时,会使用Object.ReferenceEquals,它只检查两者是否是相同的引用,而不是它们是否包含相同的元素。

您可以提供自定义的IEqualityComparer<T>

public class IdEventComparer : IEqualityComparer<SampleObject>
{
    public bool Equals(SampleObject x, SampleObject y)
    {
        if (object.ReferenceEquals(x, y)) 
            return true;
        if (x == null || y == null) 
            return false;
        if(x.Id != y.Id) 
            return false;
        if (x.Events == null && y.Events == null)
            return true;
        if (x.Events == null || y.Events == null)
            return false;

        return x.Events.SequenceEqual(y.Events);
    }

    public int GetHashCode(SampleObject obj)
    {
        if(obj == null) return 23;
        unchecked
        {
            int hash = 23;
            hash = (hash * 31) + obj.Id == null ? 31 : obj.Id.GetHashCode();

            if (obj.Events == null) return hash;
            foreach (string item in obj.Events)
            {
                hash = (hash * 31) + (item == null ? 0 : item.GetHashCode());
            }
            return hash;
        }
    }
}

然后你可以在许多LINQ方法中使用它,比如GroupBy

var duplicates = items.GroupBy(x => x, new IdEventComparer())
     .Where(g => g.Count() > 1)
     .Select(g => g.Key)
     .ToList();

嗨,Tim。非常好的例子和解释。然而,当我尝试实现它时,使用这行代码:var groupedOrderList = orders.OrderListD.GroupBy(u => u.Services[0].FeatureList, new FeatureAttributeComparer()).Select(grp => grp.First()).ToList(); 我在GroupBy子句下面得到了一个错误,它说:无法从用法中推断类型参数。请明确指定类型参数。你有什么想法吗? - SamyCode

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