如何使用LINQ对数据进行分层分组?

16

我有一些具有不同属性的数据,想要对这些数据进行分层分组。例如:

public class Data
{
   public string A { get; set; }
   public string B { get; set; }
   public string C { get; set; }
}

我希望将其分组为:

A1
 - B1
    - C1
    - C2
    - C3
    - ...
 - B2
    - ...
A2
 - B1
    - ...
...

目前,我已经使用LINQ对其进行了分组,使得顶级组将数据按A分组,然后每个子组按B分组,然后每个B子组都包含C子组,以此类推。 LINQ如下所示(假设一个名为 data IEnumerable<Data>序列):

var hierarchicalGrouping =
            from x in data
            group x by x.A
                into byA
                let subgroupB = from x in byA
                                group x by x.B
                                    into byB
                                    let subgroupC = from x in byB
                                                    group x by x.C
                                    select new
                                    {
                                        B = byB.Key,
                                        SubgroupC = subgroupC
                                    }
                select new
                {
                    A = byA.Key,
                    SubgroupB = subgroupB
                };

从上面可以看出,需要更多子分组时,这会变得有些混乱。有没有更好的方式来执行此类分组?似乎应该有,但我没有看到。

更新
目前,我发现使用流畅的LINQ API来表达此层次化分组比查询语言提高可读性,但它不够DRY。

我做这件事的两种方法:一种是使用GroupBy和结果选择器,另一种是使用GroupBy后跟一个Select调用。两者都可以格式化以使其比使用查询语言更易读,但仍然无法很好地扩展。

var withResultSelector =
    data.GroupBy(a => a.A, (aKey, aData) =>
        new
        {
            A = aKey,
            SubgroupB = aData.GroupBy(b => b.B, (bKey, bData) =>
                new
                {
                    B = bKey,
                    SubgroupC = bData.GroupBy(c => c.C, (cKey, cData) =>
                    new
                    {
                        C = cKey,
                        SubgroupD = cData.GroupBy(d => d.D)
                    })
                })
        });

var withSelectCall =
    data.GroupBy(a => a.A)
        .Select(aG =>
        new
        {
            A = aG.Key,
            SubgroupB = aG
                .GroupBy(b => b.B)
                .Select(bG =>
            new
            {
                B = bG.Key,
                SubgroupC = bG
                    .GroupBy(c => c.C)
                    .Select(cG =>
                new
                {
                    C = cG.Key,
                    SubgroupD = cG.GroupBy(d => d.D)
                })
            })
        });

我希望的是...
假设语言和框架支持,我可以想象出几种表达方式。第一种方法是使用 GroupBy 扩展方法,它接受一系列函数对作为键选择和结果选择:Func<TElement, TKey>Func<TElement, TResult>。每个函数对描述了下一个子分组。但这种选项不太实用,因为每个函数对可能需要与其他函数对不同的 TKeyTResult,这意味着 GroupBy 需要有限的参数和复杂的声明。

第二种方法是使用 SubGroupBy 扩展方法来进行分组嵌套。 SubGroupByGroupBy 相同,但其结果是前一个分组进一步分区。例如:

var groupings = data
    .GroupBy(x=>x.A)
    .SubGroupBy(y=>y.B)
    .SubGroupBy(z=>z.C)

// This version has a custom result type that would be the grouping data.
// The element data at each stage would be the custom data at this point
// as the original data would be lost when projected to the results type.
var groupingsWithCustomResultType = data
    .GroupBy(a=>a.A, x=>new { ... })
    .SubGroupBy(b=>b.B, y=>new { ... })
    .SubGroupBy(c=>c.C, c=>new { ... })
这个问题的难点在于如何高效地实现这些方法,根据我的理解,每个级别都需要重新创建新的对象来扩展之前的对象。第一次迭代会创建A的分组,第二次则会创建具有A键和B分组的对象,第三次将重复所有操作并添加C分组。这似乎非常低效(尽管我怀疑我的当前选项实际上也是这样做的)。如果调用传递了所需的元描述,并且实例只在最后一个传递中创建,那将是很好的,但这听起来也很困难。请注意,这类似于可以使用 GroupBy 执行的操作,但不需要嵌套的方法调用。
希望这一切都说得通。我想我正在追求幻想,但也许并非如此。
更新-另一种选择 我认为更优雅的另一种可能性比我的以前的建议更可行,它依赖于每个父组仅是一个键和子项序列(如示例中所示),就像 IGrouping 现在提供的一样。这意味着构建此分组的一个选项是一系列键选择器和单个结果选择器。
如果所有键都限于一个集合类型,那么这可以作为一系列键选择器和结果选择器,或者结果选择器和 params 的键选择器生成。当然,如果键必须是不同类型和不同级别的,则再次变得困难,除非由于泛型参数化的方式,这仅适用于有限的层次结构深度。
以下是我所说的一些说明性示例:
例如:
public static /*<grouping type>*/ SubgroupBy(
    IEnumerable<Func<TElement, TKey>> keySelectors,
    this IEnumerable<TElement> sequence,
    Func<TElement, TResult> resultSelector)
{
    ...
}

var hierarchy = data.SubgroupBy(
                    new [] {
                        x => x.A,
                        y => y.B,
                        z => z.C },
                    a => new { /*custom projection here for leaf items*/ })

或者:

public static /*<grouping type>*/ SubgroupBy(
    this IEnumerable<TElement> sequence,
    Func<TElement, TResult> resultSelector,
    params Func<TElement, TKey>[] keySelectors)
{
    ...
}

var hierarchy = data.SubgroupBy(
                    a => new { /*custom projection here for leaf items*/ },
                    x => x.A,
                    y => y.B,
                    z => z.C)

这并不能解决实现效率问题,但它应该可以解决复杂的嵌套。然而,这个分组的返回类型是什么?我需要自己定义一个接口还是可以通过某种方式使用 IGrouping ?我需要定义多少内容,或者层次结构的变量深度是否仍然使这个不可能?

我猜想这应该与任何 IGrouping 调用的返回类型相同,但如果它没有涉及任何传递的参数中的类型系统,那么类型系统如何推断出该类型?

这个问题超出了我的理解范围,这很棒,但让我感到头疼。


@Jeff:你能发一下你想写的代码吗(可能需要调用某种辅助函数),然后我们可以看看我们能做些什么?我怀疑这是那种需要为每个层次结构级别提供不同重载的事情(例如,2级的一个,3级的一个等等),但它仍然可能很有用。 - Jon Skeet
你能找到解决方案吗? - Robert Harvey
@Robert:不,我没有得到一个令人满意的解决方案。看来这是一个相当难以解决的问题。 - Jeff Yates
我以前做过这种工作。如果您需要自定义子分组,您需要递归类定义,就像下面的Obalix的GroupResult类一样。然后,您可以一次一个分组地填充每个类实例中所需的分组。 - Robert Harvey
@Robert:我在想这是否已经是最好的了。谢谢。 - Jeff Yates
显示剩余2条评论
3个回答

10

这里有一个说明,它告诉你如何实现一种分层分组机制。

从这个说明中:

结果类:

public class GroupResult
{
    public object Key { get; set; }
    public int Count { get; set; }
    public IEnumerable Items { get; set; }
    public IEnumerable<GroupResult> SubGroups { get; set; }
    public override string ToString() 
    { return string.Format("{0} ({1})", Key, Count); }
}

扩展方法:

public static class MyEnumerableExtensions
{
    public static IEnumerable<GroupResult> GroupByMany<TElement>(
        this IEnumerable<TElement> elements,
        params Func<TElement, object>[] groupSelectors)
    {
        if (groupSelectors.Length > 0)
        {
            var selector = groupSelectors.First();

            //reduce the list recursively until zero
            var nextSelectors = groupSelectors.Skip(1).ToArray();
            return
                elements.GroupBy(selector).Select(
                    g => new GroupResult
                    {
                        Key = g.Key,
                        Count = g.Count(),
                        Items = g,
                        SubGroups = g.GroupByMany(nextSelectors)
                    });
        }
        else
            return null;
    }
}

使用方法:

var result = customers.GroupByMany(c => c.Country, c => c.City);

编辑:

这是经过改进并正确类型化的代码版本。

public class GroupResult<TItem>
{
    public object Key { get; set; }
    public int Count { get; set; }
    public IEnumerable<TItem> Items { get; set; }
    public IEnumerable<GroupResult<TItem>> SubGroups { get; set; }
    public override string ToString() 
    { return string.Format("{0} ({1})", Key, Count); }
}

public static class MyEnumerableExtensions
{
    public static IEnumerable<GroupResult<TElement>> GroupByMany<TElement>(
        this IEnumerable<TElement> elements,
        params Func<TElement, object>[] groupSelectors)
    {
        if (groupSelectors.Length > 0)
        {
            var selector = groupSelectors.First();

            //reduce the list recursively until zero
            var nextSelectors = groupSelectors.Skip(1).ToArray();
            return
                elements.GroupBy(selector).Select(
                    g => new GroupResult<TElement> {
                        Key = g.Key,
                        Count = g.Count(),
                        Items = g,
                        SubGroups = g.GroupByMany(nextSelectors)
                    });
        } else {
            return null;
        }
    }
}

如果我将“Items = g”和“IEnumerable Items”设置为“IEnumerable<GroupResult> Items”,那么它对我来说无法构建。 - Prisoner ZERO
@囚犯ZERO:这些项目是TElement类型的,而不是GroupResult类型的。我在帖子中添加了一个正确类型的版本。 - AxelEckenberger
1
改进版无法编译。(GroupResult需要1个类型参数。) - Phil Degenhardt

4
你需要一个递归函数。递归函数对树中的每个节点调用自身。
在Linq中,你可以使用Y组合子来实现这一点。

当我分组的属性在每个级别上都发生变化时,这该怎么办? - Jeff Yates
它并不适用。除非您的应用程序设计限制了树层级(嵌套深度),否则最好通过为每个节点添加ParentID来设置自引用关联(以便在每个级别都始终引用ParentID)。 - Robert Harvey
正如所说,问题并不完全相同于树的递归展开。此外,这几乎是一个仅包含链接的答案,一旦链接失效,它就会变成一条评论。 - Gert Arnold

0

这是我尝试创建嵌套分组的方法。也许有人会觉得它有用。

// extension method
public static IEnumerable<TResult> GroupMany<TElement, TResult>(this IEnumerable<TElement> seq, Func<GroupingBuilder<TElement>, IGroupingStage<TElement, TResult>> configure)
{
    var builder = new GroupingBuilder<TElement>();
    return configure(builder).ApplyTo(seq);
}

// builder classes

public class GroupingBuilder<TElement>
{
    public GroupingBuilder<TKeyNext, Group<TKeyNext, TElement>, TElement, TElement> By<TKeyNext>(Func<TElement, TKeyNext> keySelector)
        => By(keySelector, (k, s, nested) => Group.Of(k, nested(s)));

    public new GroupingBuilder<TKeyNext, TElementNext, TElement, TElement> By<TKeyNext, TElementNext>(
        Func<TElement, TKeyNext> keySelector,
        Func<TKeyNext, IEnumerable<TElement>, Func<IEnumerable<TElement>, IEnumerable<TElement>>, TElementNext> elementSelector)
        => new GroupingBuilder<TKeyNext, TElementNext, TElement, TElement>(keySelector, elementSelector, new IdentityStage());


    // preventing writing GroupMany(g => g), i.e. mentioned call will not compile
    private class IdentityStage : IGroupingStage<TElement, TElement>
    {
        public IEnumerable<TElement> ApplyTo(IEnumerable<TElement> seq) => seq;
    }
}

public class GroupingBuilder<TKeyCurrent, TElementCurrent, TElementPrev, TElement> : IGroupingStage<TElement, TElementCurrent>
{
    private Func<TElement, TKeyCurrent> _keySelector;
    private IGroupingStage<TElement, TElementPrev> _prevStage;
    private Func<TKeyCurrent, IEnumerable<TElement>, Func<IEnumerable<TElement>, IEnumerable<TElementPrev>>, TElementCurrent> _elementSelector;

    public GroupingBuilder(
        Func<TElement, TKeyCurrent> keySelector,
        Func<TKeyCurrent, IEnumerable<TElement>, Func<IEnumerable<TElement>, IEnumerable<TElementPrev>>, TElementCurrent> elementSelector,
        IGroupingStage<TElement, TElementPrev> prevStage)
    {
        _keySelector = keySelector;
        _prevStage = prevStage;
        _elementSelector = elementSelector;
    }

    public GroupingBuilder<TKeyNext, Group<TKeyNext, TElementCurrent>, TElementCurrent, TElement> By<TKeyNext>(
        Func<TElement, TKeyNext> keySelector)
        => By(keySelector, (k, s, nested) => Group.Of(k, nested(s)));

    public GroupingBuilder<TKeyNext, TElementNext, TElementCurrent, TElement> By<TKeyNext, TElementNext>(
        Func<TElement, TKeyNext> keySelector,
        Func<TKeyNext, IEnumerable<TElement>, Func<IEnumerable<TElement>, IEnumerable<TElementCurrent>>, TElementNext> elementSelector)
        => new GroupingBuilder<TKeyNext, TElementNext, TElementCurrent, TElement>(keySelector, elementSelector, this);

    IEnumerable<TElementCurrent> IGroupingStage<TElement, TElementCurrent>.ApplyTo(IEnumerable<TElement> seq)
        => seq.GroupBy(_keySelector, (k, s) => _elementSelector(k, s, _prevStage.ApplyTo));
}

public interface IGroupingStage<TElement, TResultElement>
{
    IEnumerable<TResultElement> ApplyTo(IEnumerable<TElement> seq);
}

// Group data structure
public class Group<TKey, TElement>
{
    public TKey Key { get; set; }
    public ICollection<TElement> Items { get; set; }
}

public static class Group
{
    public static Group<TKey, TElement> Of<TKey, TElement>(TKey key, IEnumerable<TElement> elements)
        => new Group<TKey, TElement> { Key = key, Items = elements.ToList() };
}

基本用法:

var items = new[]{
    new SomeEntity{NonUniqueId = 1, Name = "John", Surname = "Doe", DoB = new DateTime(1900, 01, 03)},
    new SomeEntity{NonUniqueId = 1, Name = "John", Surname = "Doe", DoB = new DateTime(1980, 01, 03)},
    new SomeEntity{NonUniqueId = 2, Name = "Jane", Surname = "Doe", DoB = new DateTime(1902, 01, 03)},
    new SomeEntity{NonUniqueId = 1, Name = "Jane", Surname = "Smith", DoB = new DateTime(1999, 01, 03)},
};

IEnumerable<Group<int, Group<DateTime, Group<string, SomeEntity>>>> result = items
    .GroupMany(c => c
        .By(x => x.Surname)
        .By(x => x.DoB)
        .By(x => x.NonUniqueId));

请注意,分组属性必须按相反的顺序指定。这是由于泛型限制引起的 - GroupingBuilder<TKeyCurrent, TElementCurrent, TElementPrev, TElement>将先前的分组类型封装在新类型中,因此只能按相反的顺序进行嵌套。
使用自定义结果选择器:
var result = items
    .GroupMany(c => c
        .By(x => x.Surname, (key, seq, nested) => new { Surname = key, ChildItems = nested(seq).ToList() })
        .By(x => x.DoB, (key, seq, nested) => new { DoB = key, Children = nested(seq).ToList() })
        .By(x => x.NonUniqueId));

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