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

Jef*_*tes 14 linq grouping group-by c#-3.0

我有一些具有各种属性的数据,我想对这些数据进行分层分组.例如:

public class Data
{
   public string A { get; set; }
   public string B { get; set; }
   public string C { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

我希望这个分组为:

A1
 - B1
    - C1
    - C2
    - C3
    - ...
 - B2
    - ...
A2
 - B1
    - ...
...
Run Code Online (Sandbox Code Playgroud)

目前,我已经能够使用LINQ对其进行分组,使得顶部组将数据除以A,然后每个子组除以B,然后每个B子组包含C的子组等.LINQ看起来像这样(假设一个IEnumerable<Data>序列被调用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
                };
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,这需要更多的子组.有没有更好的方法来执行这种类型的分组?似乎应该有,我只是没有看到它.

更新
到目前为止,我发现通过使用流畅的LINQ API而不是查询语言表达这种分层分组可以说明提高了可读性,但它并不觉得很干.

我有两种方法:一种是使用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)
                    })
                })
        });
Run Code Online (Sandbox Code Playgroud)

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)
                })
            })
        });
Run Code Online (Sandbox Code Playgroud)

我想要的......
我可以设想一些可以表达的方式(假设语言和框架支持它).第一个是GroupBy扩展,它采用一系列功能对进行键选择和结果选择,Func<TElement, TKey>以及Func<TElement, TResult>.每对描述下一个子组.这个选项失败了,因为每一对都可能需要TKeyTResult不同于其他选项,这意味着GroupBy需要有限的参数和复杂的声明.

第二种选择是SubGroupBy可以链接以生成子组的扩展方法.SubGroupBy将是相同的,GroupBy但结果将是先前的分组进一步分区.例如:

var groupings = data
    .GroupBy(x=>x.A)
    .SubGroupBy(y=>y.B)
    .SubGroupBy(z=>z.C)
Run Code Online (Sandbox Code Playgroud)

// 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 { ... })
Run Code Online (Sandbox Code Playgroud)

这方面的难点在于如何有效地实现方法,就像我目前的理解一样,每个级别都会重新创建新对象以扩展先前的对象.第一次迭代将创建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*/ })
Run Code Online (Sandbox Code Playgroud)

要么:

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)
Run Code Online (Sandbox Code Playgroud)

这并不能解决实现效率低下的问题,但它应该解决复杂的嵌套问题.但是,这种分组的返回类型是什么?我需要自己的界面还是可以IGrouping以某种方式使用.我需要定义多少,或者层次结构的变深度是否仍然无法实现?

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

这个问题正在扩展我的理解,这很好,但我的大脑很痛.

Axe*_*ger 9

以下是如何实现分层分组机制的说明.

从这个描述:

结果类:

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); }
}
Run Code Online (Sandbox Code Playgroud)

扩展方法:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

var result = customers.GroupByMany(c => c.Country, c => c.City);
Run Code Online (Sandbox Code Playgroud)

编辑:

这是一个改进的和正确类型的代码版本.

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)