【问题标题】:How to group objects that have the same lists of child objects?如何对具有相同子对象列表的对象进行分组?
【发布时间】:2020-04-24 09:27:31
【问题描述】:

我正在尝试对对象列表进行分组,这些对象具有一个列表属性。使用以下表达式时,它不会将具有列表的对象分组在一起,大概是因为当它检查每个列表的相等性时,它不匹配 - 即使内容相同。

我知道这是问题所在,就好像我停止对列表进行分组一样,代码输出两项而不是三项。

如何调整我的表达方式,以便正确地将前两项(相同的)归为一组?

public class Item {
  public string Id { get; set; }
  public string Name { get; set; }
  public IEnumerable<ChildItem> Children { get; set; }
}

public class ChildItem {
  public string ChildItemName { get; set; }
}
var items = new List<Item> {
  new Item {
    Id = "00001",
    Name = "My Item",
    Children = new List<ChildItem> {
      new ChildItem { ChildItemName = "string 1" },
      new ChildItem { ChildItemName = "string 2" },
    }
  },
  new Item {
    Id = "00001",
    Name = "My Item",
    Children = new List<ChildItem> {
      new ChildItem { ChildItemName = "string 1" },
      new ChildItem { ChildItemName = "string 2" },
    }
  },
  new Item {
    Id = "00002",
    Name = "My Second Item",
    Children = new List<ChildItem> {
      new ChildItem { ChildItemName = "string 3" },
    }
  }
};

var result =
  from c in items
  group c by new
  {
    c.Id,
    c.Name,
    c.Children,
  }
  into gcs
  select new Item()
  {
    Id = gcs.Key.Id,
    Name = gcs.Key.Name,
    Children = gcs.Key.Children
  };

【问题讨论】:

  • 覆盖Equals & GetHashCodeChildItem
  • 不要听@JohnathanBarclay 的建议,除非你 100% 确定你在做什么,这两个覆盖是什么意思,并且 100% 确定这不会与其他任何东西发生冲突或导致问题在代码的其他部分。根据您的域逻辑,您可能希望在不同的地方以不同的方式“比较”相同的事物。那些overrides 不会让你这样做。此外,有很多代码(库)依赖 .Equals 和 .GetHashCode 来处理各种事情,并且覆盖 .Equals 可能会导致细微的错误。使用 Olivier 的回答中的IEqualityComparer&lt;&gt;。更安全。
  • 我已经编辑了标题以使其更清晰一些。我认为“Group .. with IEnumerable Properties”并没有说明您想要实现的目标。另外,除非必要,我们通常不会在标题中包含关键字,因此我从标题中删除了 C# 和 LINQ 标记 (meta.stackexchange.com/questions/19190/…)。当然,您可以随意重新编辑标题,或者根据需要将其回滚。
  • 你为什么不按Id而不是Children比较对象?比较ints 比比较Lists 效率更高。

标签: c# list linq group-by


【解决方案1】:

如果具有相同Id的项目具有相同的名称和集合,您可以按Id分组,然后取出组的第一项

IEnumerable<Item> uniqueItems = items
    .GroupBy(x => x.Id)
    .Select(g => g.First());

您不需要将所有内容都放入组密钥中。


由于根据您的评论,Id 不保证唯一性,因此您必须提供自己对“相等”含义的定义。您可以通过实现自己的相等比较器来做到这一点。

ChildItem 作为单例的实现,因为它将在 ItemEqualityComparer.Equals 中重复使用。

class ChildItemEqualityComparer : IEqualityComparer<ChildItem>
{
    public static readonly ChildItemEqualityComparer Instance =
        new ChildItemEqualityComparer(); // Create singleton.
    private ChildItemEqualityComparer() { } // Hide constructor.

    public bool Equals(ChildItem x, ChildItem y) =>
        String.Equals(x.ChildItemName, y.ChildItemName);
    public int GetHashCode(ChildItem childItem) =>
        childItem.ChildItemName?.GetHashCode() ?? 0;
}

Item 的实现:

class ItemEqualityComparer : IEqualityComparer<Item>
{
    public bool Equals(Item x, Item y)
    {
        return x.Id == y.Id && x.Name == y.Name &&
            Enumerable.SequenceEqual(x.Children, y.Children,
                ChildItemEqualityComparer.Instance);
    }

    public int GetHashCode(Item item)
    {
        int hash = 43;
        unchecked {
            hash = 17 * hash + (item.Id?.GetHashCode() ?? 0);
            hash = 17 * hash + (item.Name?.GetHashCode() ?? 0);
            foreach (ChildItem childItem in item.Children) {
                hash = 17 * hash + ChildItemEqualityComparer.Instance.GetHashCode(childItem);
            }
        }
        return hash;
    }
}

还有查询:

var result = items.Distinct(new ItemEqualityComparer());

这会产生 2 个项目。

【讨论】:

    【解决方案2】:
    1. 如果您必须使用 ChildItem 类(而不是简单的字符串),您 可以考虑为 ChildItem 实现 IEqualityComparer。这使得使用 (System.Linq) SequenceEqual 方法。
    ProductA[] storeA = { new ProductA { Name = "apple", Code = 9 }, 
                           new ProductA { Name = "orange", Code = 4 } };
    
    ProductA[] storeB = { new ProductA { Name = "apple", Code = 9 }, 
                           new ProductA { Name = "orange", Code = 4 } };
    
    bool equalAB = storeA.SequenceEqual(storeB);
    
    1. 那你可以考虑在Item类中实现IEquatable接口。

    IEquatable 接口定义了 Equals 方法,该方法 确定实现类型实例的相等性

    在 Equals 方法中,您可以使用 SequenceEqual 来检查上面描述的子项是否相等。

    3.最后,您可以使用 (System.Linq) Distinct() 方法获取项目的唯一对象。

    IEnumerable<Item> noduplicates = items.Distinct();
    

    请查看:

    【讨论】:

      【解决方案3】:

      如果您只是想消除重复项,您可以将集合转换为 HashSet 并返回。

      items.ToHashSet().ToList();
      

      不过,您需要覆盖GetHashCode 的默认实现,以确保它是使用Item 的字段计算的。

      【讨论】:

      • ToHashSet 将 IEqualityComparer 作为第二个参数。使用它而不是覆盖 GHC 更安全,除非你 100% 确定这种覆盖是安全的(例如,'item' 类是你的私有实现细节,并且不用于任何其他排序/字典/等地点等)
      • 好点@quetzalcoatl。在这种情况下,Item 似乎只是 Tom 的实现,但在其他情况下,使用 IEqualityComparer 更有意义。
      【解决方案4】:

      你可以这样写GroupBy表达式

      items.GroupBy(x => x.Id);
      

      【讨论】:

      • 是的,我可以稍微不同地编写语法,但是关于列表相等的问题仍然存在,我不想按 ID 分组,我想按对象中的所有属性分组。
      • 您是在谈论获取独特元素还是分组?
      • 我在数组中有相同的项目,我想消除重复项,但是由于对象有一个 IEnumerable/List,group by 不会消除重复项。
      • Group by 不应该消除您拥有的任何数据.. 只需将它们组合在一起即可。如果你想摆脱重复,那么你必须做一些不同的事情。
      • @TomMcClean A group by 从不消除任何东西,它检查是否相等,而不是更多。另一方面,如果对象不相等,则没有重复项。你似乎对beeing equal有不同的定义。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-10-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-05-01
      • 2021-02-05
      • 1970-01-01
      相关资源
      最近更新 更多