我有一些具有不同属性的数据,想要对这些数据进行分层分组。例如:
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>
。每个函数对描述了下一个子分组。但这种选项不太实用,因为每个函数对可能需要与其他函数对不同的 TKey
和 TResult
,这意味着 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
调用的返回类型相同,但如果它没有涉及任何传递的参数中的类型系统,那么类型系统如何推断出该类型?
这个问题超出了我的理解范围,这很棒,但让我感到头疼。