LINQ GroupBy em vários campos do tipo ref;Comparador de igualdade personalizado
-
13-12-2019 - |
Pergunta
Examinei cerca de 20 exemplos sobre isso no SO e em outros lugares, mas não encontrei nenhum que cubra o que estou tentando fazer.Esse - Posso especificar meu comparador de tipo explícito embutido? - parece o que eu preciso, mas não vai longe o suficiente (ou não entendo como ir mais longe).
- Eu tenho uma lista de LoadData, o objeto LoadData possui campos de tipos de referência e de valor
- Precisa agrupar em uma mistura de campos de referência e valor, projetar a saída para um tipo anônimo
Precisa (eu acho) fornecer um IEqualityComparer personalizado para especificar como comparar os campos GroupBy, mas eles são do tipo anônimo
private class LoadData { public PeriodEndDto PeriodEnd { get; set; } public ComponentDto Component { get; set; } public string GroupCode { get; set; } public string PortfolioCode { get; set; } }
A melhor consulta GroupBy que tenho até agora:
var distinctLoads = list.GroupBy(
dl => new { PeriodEnd = dl.PeriodEnd,
Component = dl.Component,
GroupCode = dl.GroupCode },
(key, data) => new {PeriodEnd = key.PeriodEnd,
Component = key.Component,
GroupCode = key.GroupCode,
PortfolioList = data.Select(d=>d.PortfolioCode)
.Aggregate((g1, g2) => g1 + "," + g2)},
null);
Este grupo, mas ainda há duplicatas.
- Como posso especificar um código personalizado para comparar os campos GroupBy?Por exemplo, os Componentes poderiam ser comparados por Component.Code.
Solução
O problema aqui é que seu tipo de chave é anônimo, o que significa que você não pode declarar uma classe que implemente IEqualityComparer<T>
para esse tipo de chave.Embora fosse possível escrever um comparador que comparasse tipos anônimos quanto à igualdade de maneira personalizada (por meio de um método genérico, delegados e inferência de tipo), não seria muito agradável.
As duas opções mais simples são provavelmente:
- Faça o tipo anônimo "simplesmente funcionar" substituindo Equals/GetHashCode em
PeriodEndDto
eComponentDto
.Se houver uma igualdade natural que você gostaria de usar em todos os lugares, esta é provavelmente a opção mais sensata.Eu recomendaria implementarIEquatable<T>
também - Não use um tipo anônimo para agrupamento - use um tipo nomeado e então você pode substituir
GetHashCode
eEquals
sobre isso, ou você pode escrever um comparador de igualdade personalizado da maneira normal.
EDITAR: ProjectionEqualityComparer
realmente não funcionaria.Seria viável escrever algo semelhante - uma espécie de CompositeEqualityComparer
o que permitiu criar um comparador de igualdade a partir de vários pares "projeção + comparador".Seria muito feio comparado com o tipo anônimo.
Outras dicas
EDITAR:
Como aponta Jon Skeet, esta solução parece melhor do que é, se você não pensar muito sobre isso, porque esqueci de implementar GetHashCode.Ter que implementar o GethashCode torna essa abordagem, como Jon diz em sua resposta: "Não terrivelmente agradável". Presumivelmente, essa também é a explicação para a ausência (chamada "inexplicável") de EqualityComparer<T>.Create()
no quadro.Deixarei a resposta para referência, pois exemplos do que não fazer também podem ser instrutivos.
RESPOSTA ORIGINAL:
Você poderia usar a abordagem sugerida pelo Comparer<T>.Create
padrão introduzido no .NET 4.5 (mas inexplicavelmente ausente no EqualityComparer<T>
).Para fazer isso, crie um DelegateEqualityComparer<T>
aula:
class DelegateEqualityComparer<T> : EqualityComparer<T>
{
private readonly Func<T, T, bool> _equalityComparison;
private DelegateEqualityComparer(Func<T, T, bool> equalityComparison)
{
if (equalityComparison == null)
throw new ArgumentNullException("equalityComparison");
_equalityComparison = equalityComparison;
}
public override bool Equals(T x, T y)
{
return _equalityComparison(x, y);
}
public static DelegateEqualityComparer<T> Create(
Func<T, T, bool> equalityComparison)
{
return new DelegateEqualityComparer<T>(equalityComparison);
}
}
Em seguida, escreva wrappers em torno dos métodos GroupBy para aceitar um Func<TKey, TKey, bool>
delegado no lugar do IEqualityComparer<TKey>
parâmetro.Esses métodos envolvem o delegado em um DelegateEqualityComparer<T>
instância e passe-a para o método GroupBy correspondente.Exemplo:
public static class EnumerableExt
{
public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TKey, TKey, bool> equalityComparison)
{
return source.GroupBy(
keySelector,
DelegateEqualityComparer<TKey>.Create(equalityComparison);
}
}
Finalmente, no site da chamada, você usaria algo como esta expressão para o equalityComparison
argumento:
(a, b) => a.PeriodEnd.Equals(b.PeriodEnd)
&& a.Component.Code.Equals(b.Component.Code)
&& a.GroupCode.Equals(b.GroupCode)