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

标签 linq c#-3.0 grouping group-by

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

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 看起来像这样(假设一个 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
                };

如您所见,需要的子分组越多,这就会变得有些困惑。有没有更好的方法来执行这种类型的分组?似乎应该有,我只是没有看到。

更新
到目前为止,我发现通过使用流畅的 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)
                    })
                })
        });
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 扩展方法,可以链接以生成子组。 SubGroupBy 将与 GroupBy 相同,但结果将是先前的分组进一步分区。例如:
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 调用的返回类型相同,但是如果类型系统不涉及任何传递的参数,类型系统如何推断该类型?

这个问题扩展了我的理解,这很好,但我的大脑很痛。

最佳答案

Here is a description如何实现分层分组机制。

从这个描述:

结果类:

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;
        }
    }
}

关于linq - 如何使用 LINQ 对数据进行分层分组?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2230202/

相关文章:

python-3.x - 在 python 中对相似但完全相同的单词及其缩写进行分组

linq - * 右 * LINQ 中的外连接

c# - 在 Linq 中计算行数

c# - 有 2 组数据的自连接表的条件 LINQ 查询

variables - 对 Netlogo 中的品种使用 "n-of"命令时出错

mysql - 按小时汇总行(包括空闲时间)?

vb.net - Linq to DataSet Order By 子句错误

c# - 有限状态机应该有一个 "nested"有限状态机吗?

reflection - 如何立即验证Silverlight 3 Datagrid中新插入的行?

c# - 如何在特定时间后停止执行方法?