使用Linq OrderBy对具有相同集合的对象进行分组

7

我有一组包含另一组的对象。

private class Pilot
{
    public string Name;
    public HashSet<string> Skills;
}

以下是一些测试数据:

public void TestSetComparison()
{
    var pilots = new[] 
    {
        new Pilot { Name = "Smith", Skills = new HashSet<string>(new[] { "B-52", "F-14" }) },
        new Pilot { Name = "Higgins", Skills = new HashSet<string>(new[] { "Concorde", "F-14" }) },
        new Pilot { Name = "Jones", Skills = new HashSet<string>(new[] { "F-14", "B-52" }) },
        new Pilot { Name = "Wilson", Skills = new HashSet<string>(new[] { "F-14", "Concorde" }) },
        new Pilot { Name = "Celko", Skills = new HashSet<string>(new[] { "Piper Cub" }) },
    };

我想在Linq中使用OrderBy,以便实现以下功能:
- Smith和Jones一起排序,因为他们驾驶相同的飞机 - Higgins和Wilson一起排序,因为他们驾驶相同的飞机 - 不要紧 Higgins+Wilson排在Smith+Jones前面或后面 - 最好是Smith在Jones之前(稳定排序),但这不太重要
我认为我需要实现一个IComparer来传递给OrderBy,但不知道如何处理上述“doesn't matter”方面和稳定排序。
更新: 我希望输出是相同五个Pilot对象的数组,但顺序不同。

你所描述的是一个GroupBy,而不是OrderBy。你应该将其视为“分组”,而不是“一起排序”。但这有点不寻常,因为您正在尝试对选项列表进行分组,而不是特定属性(在发布答案之前仍在研究)。问题:如果B-52和Concorde有一个额外的飞行员呢?由于没有人具备完全相同的技能,他会被单独留在一个组中吗? - Flater
您不需要一个 IComparer<Pilot>,而是需要一个 IEqualityComparer<HashSet<string>> 来检查两个 Skills 集合是否相等。然后在 GroupBy 调用中使用此比较器按 Skills 分组。据我所知,GroupBy 会保留元素的顺序。 - René Vogt
3个回答

7

当使用GroupBy时,您需要为要分组的类型(在您的情况下是HashSet<string>)实现IEqualityComparer<T>,例如:

private sealed class MyComparer : IEqualityComparer<HashSet<string>> {
  public bool Equals(HashSet<string> x, HashSet<string> y) {
    if (object.ReferenceEquals(x, y))
      return true;
    else if (null == x || null == y)
      return false;

    return x.SetEquals(y);
  }

  public int GetHashCode(HashSet<string> obj) {
    return obj == null ? -1 : obj.Count;
  }
}

接下来使用它:

 IEnumerable<Pilot> result = pilots
    .GroupBy(pilot => pilot.Skills, new MyComparer())
    .Select(chunk => string.Join(", ", chunk
       .Select(item => item.Name)
       .OrderBy(name => name))); // drop OrderBy if you want stable Smith, Jones

 Console.WriteLine(string.Join(Environment.NewLine, result));

结果:

 Jones, Smith
 Higgins, Wilson
 Celko

编辑:如果你想要对一个数组进行重新排序,那么可以添加SelectMany()来将分组扁平化,然后使用ToArray()得到最终结果:

 var result = pilots
    .GroupBy(pilot => pilot.Skills, new MyComparer())
    .SelectMany(chunk => chunk)
    .ToArray();

 Console.WriteLine(string.Join(", ", result.Select(p => p.Name)));

结果:

 Jones, Smith,
 Higgins, Wilson,
 Celko

请注意,string.join将每个组的名称合并在一行中,即Jones, Smith都具有相同的技能集。
作为运行:DotNetFiddle

@onedaywhen:我明白了;如果你想要数组,只需在flattenGroupBy后添加SelectMany和最终的ToArray即可。请看我的编辑。 - Dmitry Bychenko
结果必须是飞行员数组,但按照规范排序,因此我认为我需要一个IComparer在OrderBy中使用。 - petemoloy
1
是的,我现在明白了!我认为你忽略了一个事实,就是可以在“Equals”实现中使用x.SetEquals(y)。这会如何改变“GetHashCode”实现? - petemoloy
@petemoloy:谢谢!x.SetEquals(y);比我的仿真实现更好;无需更改GetHashCode:如果集合相等,则它们具有相同的哈希码(项数)。 - Dmitry Bychenko
有什么理由不将比较器类设为通用的?在实现中没有任何特定于“字符串”的内容。 - petemoloy
1
@petemoloy:嗯,在这种情况下,“private sealed class MyComparer<T> : IEqualityComparer<HashSet<T>>”是相当好的;我没有将其声明为通用类型,因为我认为您正在解决一个非常具体的问题 - Dmitry Bychenko

1
您可以使用以下实现的 HashSetByItemsComparer 来完成您需要的操作:
public class HashSetByItemsComparer<TItem> : IComparer<HashSet<TItem>>
{
    private readonly IComparer<TItem> _itemComparer;

    public HashSetByItemsComparer(IComparer<TItem> itemComparer)
    {
        _itemComparer = itemComparer;
    }

    public int Compare(HashSet<TItem> x, HashSet<TItem> y)
    {
        foreach (var orderedItemPair in Enumerable.Zip(
            x.OrderBy(item => item, _itemComparer),
            y.OrderBy(item => item, _itemComparer), 
            (a, b) => (a, b))) //C# 7 syntax used - Tuples
        {
            var itemCompareResult = _itemComparer.Compare(orderedItemPair.a, orderedItemPair.b);
            if (itemCompareResult != 0)
            {
                return itemCompareResult;
            }
        }

        return 0;
    }
}

这不是最有效的解决方案,因为它会为每个比较单独排序哈希集合。如果需要与数百万个飞行员和许多技能一起使用,则可能需要进行优化,但对于较小的数字,它将完美地工作。
用法示例:
var sortedPilots = pilots.OrderBy(p => p.Skills, new HashSetByItemsComparer<string>(StringComparer.Ordinal));

foreach (var pilot in sortedPilots)
{
    Console.WriteLine(pilot.Name);
}

并且输出为:

Smith
Jones
Higgins
Wilson
Celko

所以它保留相等项目的顺序(OrderBy的默认行为-您不需要担心)。顺便说一句,使用GroupBy的解决方案不允许您恢复项目的顺序,据我所知。

我使用了 HashSet 对象,这样我就可以简单地使用 SetEquals - 它能在这里使用吗? - petemoloy
恐怕SetEquals只会检查相等性,但你需要按OrderBy排序项目... - Sasha

0

这是一个带有orderby的示例

  var groups = from c in GridImage (Your List)
               orderby c.grouping (Your Item inside List)
               group c by c.grouping;

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