首页 > 解决方案 > Linq 按复杂类型的列表分组

问题描述

我正在尝试对列表的某些列表对象执行 Linq GroupBy。

public class ItemDto
{

    public int ItemId { get; set; }
    public string ProductName { get; set; }
    public int Count { get; set; }
    public List<CondimentDto> Condiments { get; set; }
}

public class CondimentDto
{
    public CondimentDto(int ItemId, string productName, decimal originalPrice)
    {
        ItemId = ItemId;
        ProductName = productName;
        OriginalPrice = originalPrice;

    }
    public int PosItemId { get; set; }
    public string ProductName { get; set; }
    public decimal OriginalPrice { get; set; }

}

这是列表示例。

var items = new List<ItemDto>();

items.Add(new ItemDto(){ItemId = 1, ProductName = "Steak", Condiments = new List<CondimentDto>{new CondimentDto(11, "Mayonez", 1)}});
items.Add(new ItemDto() { ItemId = 1, ProductName = "Steak", Condiments = new List<CondimentDto> { new CondimentDto(11, "Mayonez", 1) } });
items.Add(new ItemDto() { ItemId = 1, ProductName = "Steak", Condiments = new List<CondimentDto> { new CondimentDto(11, "Hardal", 1) } });

这是我的 LinQ 查询;

var result = items.GroupBy(item => new {item.ItemId, item.Condiments})
                  .Select(x=>new ItemDto
                  {
                     ItemId = x.Key,
                     ProductName = x.First().ProductName,
                     Condiments = x.First().Condiments,
                     Count = x.Count()

                  })
                  .ToList();

我想要做的是我想按 ItemId 和 List of Condiments 对我的列表进行分组。我期望的结果是

{
  items[
    {
      "ItemId": 1,
      "ProductName": "Steak",
      "Condiments": [
        {
          "ItemId": 11,
          "Name": "Mayonez",
          "Price": 1
        }
      ],
      "Count": 2
    },
    {
      "ItemId": 1,
      "ProductName": "Steak",
      "Condiments": [
        {
          "ItemId": 11,
          "Name": "Hardal",
          "Price": 1
        }
      ],
      "Count": 1
    }
  ]
}

结果,有两个相同的列表项,这就是为什么必须将其分组并且计数为 2 的原因。另一个不同的项目是调味品是 Hardal。它是独立的,计数为 1。

是否可以通过列表对象进行两组?顺便说一句,调味品可以是空的。然后它应该只按 ID 分组。

谢谢

标签: c#linq

解决方案


你可以在这里创建两个IEqualityComparer<T>类。一个用于比较两个CondimentDto对象是否相等,一个用于比较两个对象是否List<CondimentDto>相等。我们通过CondimentComparertoSequenceEqual()来确定两个列表是否相等,我们通过ItemDtoComparerto来对主列表中的对象GroupBy进行分组。我们还必须创造我们自己的并确定平等。ItemDtoitemsEquals()GetHashCode()

public class CondimentComparer : IEqualityComparer<CondimentDto>
{
    public bool Equals(CondimentDto x, CondimentDto y)
    {
        // If all the properties match, then its equal
        return x.PosItemId == y.PosItemId && 
               x.ProductName == y.ProductName && 
               x.OriginalPrice == y.OriginalPrice;
    }

    // Get all hashcodes from object properties using XOR
    // Might be better ways to do this
    public int GetHashCode(CondimentDto obj)
    {
        return obj.PosItemId.GetHashCode() ^ 
               obj.ProductName.GetHashCode() ^
               obj.OriginalPrice.GetHashCode();
    }
}

public class ItemDtoComparer : IEqualityComparer<ItemDto>
{
    public bool Equals(ItemDto x, ItemDto y)
    {

        // If two condiments are null, then just compare ItemId
        if (x.Condiments == null && y.Condiments == null)
        {
            return x.ItemId == y.ItemId;
        }

        // If one is null and the other is not, can't possiblly be equal
        else if (x.Condiments == null && y.Condiments != null ||
                 x.Condiments != null && y.Condiments == null)
        {
            return false;
        }

        // If we get here we have two normal objects
        // Compare two objects ItemId and lists of condiments
        return x.ItemId == y.ItemId &&
               x.Condiments.SequenceEqual(y.Condiments, new CondimentComparer());
    }

    public int GetHashCode(ItemDto obj)
    {

        // If condiments list is null, just get the ItemId hashcode
        if (obj.Condiments == null)
        {
            return obj.ItemId.GetHashCode();
        }

        // Otherwise do the XOR of ItemId and the aggregated XOR of the condiment list properties
        return obj.ItemId.GetHashCode() ^ obj.Condiments.Aggregate(0, (a, c) => a ^ c.PosItemId.GetHashCode() ^ c.ProductName.GetHashCode() ^ c.OriginalPrice.GetHashCode());
    }
}

然后你可以传递ItemDtoComparerGroupby

var result = items
    .GroupBy(item => item, new ItemDtoComparer()) // Pass ItemDtoComparer here
    .Select(grp => new ItemDto
    {
        ItemId = grp.Key.ItemId,
        ProductName = grp.Key.ProductName,
        Condiments = grp.Key.Condiments,
        Count = grp.Count()
    })
    .ToList();

dotnetfiddle.net上的演示

注意:以上GetHashCode()实现是异或哈希码。这被认为不是最佳实践,并且遵循此答案中的建议可以提供更好的替代方案来避免哈希冲突。我只是选择了这种方法,因为它易于记忆和演示。


推荐阅读