Tengo algunos datos que tienen varios atributos y quiero agruparlos jerárquicamente. Por ejemplo:¿Cómo puedo agrupar datos jerárquicamente utilizando LINQ?
public class Data
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
}
me gustaría que este agruparse como:
A1
- B1
- C1
- C2
- C3
- ...
- B2
- ...
A2
- B1
- ...
...
Actualmente, he podido grupo esto usando LINQ tal que el grupo superior divide los datos por A, entonces cada divide subgrupo por B, entonces cada subgrupo B contiene subgrupos de C, etc. El LINQ parece a esto (suponiendo una secuencia IEnumerable<Data>
llamado 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
};
Como puede ver, esto se vuelve un tanto desordenado cuanto más subgrupo se necesita. ¿Hay alguna manera más agradable de realizar este tipo de agrupación? Parece que debería haber y yo simplemente no lo estoy viendo.
actualización
Hasta ahora, he encontrado que la expresión de esta agrupación jerárquica mediante el uso de las API de LINQ con fluidez en lugar de lenguaje de consulta podría decirse que mejora la legibilidad, pero que no se siente muy seco.
Hubo dos formas en que hice esto: una usando GroupBy
con un selector de resultados, la otra usando GroupBy
seguido de una llamada Select
. Ambos pueden formatearse para ser más legibles que el lenguaje de consulta, pero aún así no se escalan bien.
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)
})
})
});
Lo que me gustaría ...
puedo imaginar un par de maneras en que esto podría expresarse (suponiendo que el lenguaje y un marco apoyado). La primera sería una extensión GroupBy
que toma una serie de pares de funciones para la selección de teclas y la selección de resultados, Func<TElement, TKey>
y Func<TElement, TResult>
. Cada par describe el siguiente subgrupo. Esta opción se cae porque cada par podría requerir que TKey
y TResult
sean diferentes a los demás, lo que significaría que GroupBy
necesitarían parámetros finitos y una declaración compleja.
La segunda opción sería un método de extensión SubGroupBy
que podría estar encadenado para producir subgrupos. SubGroupBy
sería lo mismo que GroupBy
pero el resultado sería la agrupación anterior más particionada. Por ejemplo:
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 { ... })
La dificultad con esto es cómo poner en práctica los métodos más eficiente con mi comprensión actual, cada nivel sería volver a crear nuevos objetos con el fin de extender los objetos anteriores. La primera iteración crearía agrupaciones de A, la segunda crearía objetos que tienen una clave de A y agrupaciones de B, la tercera rehacería todo eso y agregaría las agrupaciones de C. Esto parece terriblemente ineficiente (aunque sospecho que mis opciones actuales) en realidad hacer esto de todos modos). Sería bueno que las llamadas pasaran alrededor de una meta-descripción de lo que se requería y las instancias solo se crearon en la última pasada, pero eso también suena difícil.Tenga en cuenta que el suyo es similar a lo que se puede hacer con GroupBy
, pero sin las llamadas al método anidado.
Espero que todo eso tenga sentido. Supongo que estoy persiguiendo arcoiris aquí, pero quizás no.
Actualización - otra opción
Otra posibilidad que creo que es más elegante que mis sugerencias anteriores confía en que cada grupo de padres de ser sólo una llave y una secuencia de elementos secundarios (como en los ejemplos), al igual que IGrouping
ofrece ahora. Eso significa que una opción para construir esta agrupación sería una serie de selectores de teclas y un solo selector de resultados.
Si las claves fueron limitadas a un tipo de conjunto, que no es razonable, entonces esto podría ser generado como una secuencia de selectores de llave y un selector de resultados, o un selector de resultados y una params
de selectores de llave. Por supuesto, si las claves tenían que ser de diferentes tipos y niveles diferentes, esto se vuelve difícil de nuevo, excepto por una profundidad de jerarquía finita debido a la forma en que funciona la parametrización de los genéricos.
Éstos son algunos ejemplos ilustrativos de lo que quiero decir:
Por ejemplo:
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*/ })
O:
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)
Esto no resuelve las ineficiencias de implementación, pero debe resolver el complejo anidando Sin embargo, ¿cuál sería el tipo de devolución de esta agrupación? ¿Necesitaría mi propia interfaz o puedo usar IGrouping
de alguna manera? ¿Cuánto debo definir o la profundidad variable de la jerarquía todavía lo hace imposible?
Mi conjetura es que este debe ser el mismo que el tipo de retorno de cualquier IGrouping
llamada, pero ¿cómo el sistema de tipos inferir ese tipo si no participa en ninguno de los parámetros que se pasan?
Este problema me hace entender mucho más, lo cual es genial, pero me duele el cerebro.
@ Jeff: ¿Podría publicar el tipo de código que le quiere * * para escribir (presumiblemente invocar una especie de ayudante) y entonces podemos ver lo que podemos hacer? Sospecho que es una de esas cosas que requerirá una sobrecarga diferente para cada nivel de jerarquía (por ejemplo, uno para 2 niveles, uno para 3, etc.) pero podría ser útil. –
@jon skeet: seguro. Proporcionaré una actualización en breve. Siento que hay una solución más elegante pero no puedo verla. Hice un intento de especular mi llamada ayer, pero no cumple con las reglas de genéricos ya que cada uso de Func requiere diferentes tipos genéricos. –
@Jon Skeet: Correcto, proporcioné algunos detalles sobre las opciones que he considerado (fuera del lenguaje o las restricciones del marco) y mi pensamiento general. –