LINQ GroupBy для нескольких полей ссылочного типа; Пользовательский EqualityComparer

Итак, я просмотрел около 20 примеров на SO и в других местах, но не нашел ни одного, который охватил бы то, что я пытаюсь сделать. Это - Могу ли я указать свой явный встроенный компаратор типов? - похоже на то, что мне нужно, но недостаточно далеко (или я не понимаю, как это сделать).

  • У меня есть список LoadData, объект LoadData имеет поля как ссылочного, так и значимого типов.
  • Необходимо сгруппировать по полям ref и value, спроецировать вывод на анонимный тип
  • Необходимо (я думаю) предоставить настраиваемый IEqualityComparer, чтобы указать, как сравнивать поля GroupBy, но они являются анонимным типом

    private class LoadData
    {
        public PeriodEndDto PeriodEnd { get; set; }
        public ComponentDto Component { get; set; }
        public string GroupCode { get; set; }
        public string PortfolioCode { get; set; }
    }
    

Лучший запрос GroupBy, который у меня был до сих пор:

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

Это группы, но есть еще дубликаты.

  1. Как я могу указать собственный код для сравнения полей GroupBy? Например, компоненты можно сравнить по Component.Code.

person Peter    schedule 03.10.2012    source источник


Ответы (2)


Проблема здесь в том, что ваш тип ключа анонимный, что означает, что вы не можете объявить класс, реализующий IEqualityComparer<T> для этого типа ключа. Хотя было бы возможно написать компаратор, который сравнивал бы анонимные типы на предмет равенства специальным образом (с помощью универсального метода, делегатов и вывода типов), это было бы не очень приятно.

Вероятно, есть два самых простых варианта:

  • Сделайте анонимный тип «просто работающим», переопределив Equals / GetHashCode в PeriodEndDto и ComponentDto. Если есть естественное равенство, которое вы хотите использовать везде, это, вероятно, самый разумный вариант. Я бы также порекомендовал реализовать IEquatable<T>
  • Не используйте анонимный тип для группировки - используйте именованный тип, и тогда вы можете либо переопределить для него GetHashCode и Equals, либо написать собственный компаратор равенства обычным способом.

РЕДАКТИРОВАТЬ: ProjectionEqualityComparer действительно не сработает. Хотя было бы возможно написать что-то подобное - своего рода CompositeEqualityComparer, который позволил бы вам создать компаратор равенства из нескольких пар «проекция + компаратор». Хотя это было бы довольно уродливо по сравнению с анонимным типом.

person Jon Skeet    schedule 03.10.2012
comment
Привет @Jon - спасибо, я пошел первым путем, и он решил мою проблему, красиво и легко. Тем не менее, пара вопросов - переопределив Equals / GetHashCode, я, по сути, сообщаю коду, который использует эти объекты, что два разных экземпляра равны в зависимости от данных, которые они хранят. Есть ли в этом риск - т.е. где .NET Framework зависит от этих методов, чтобы знать, что два объекта являются разными экземплярами? А во-вторых, есть ли где-нибудь дополнительные примеры использования ProjectionEqualityComparer? Я хотел бы лучше понять, как это работает. - person Peter; 03.10.2012
comment
Везде, где проверяется равенство - словари, Contains вызовы и т. Д. Если есть естественное представление о равенстве, это, вероятно, будет хорошо для всего кода. - person Jon Skeet; 03.10.2012

РЕДАКТИРОВАТЬ:

Как указывает Джон Скит, это решение кажется лучше, чем оно есть на самом деле, если не задумываться над ним, потому что я забыл реализовать GetHashCode. Необходимость реализации GetHashCode делает этот подход, как сказал Джон в своем ответе, «не очень приятным». Предположительно, это также объяснение (так называемого «необъяснимого») отсутствия EqualityComparer<T>.Create() в структуре. Я оставлю ответ для справки, поскольку примеры того, чего не следует делать, также могут быть поучительными.

ОРИГИНАЛЬНЫЙ ОТВЕТ:

Вы можете использовать подход, предложенный шаблоном Comparer<T>.Create, представленным в .NET 4.5 (но по необъяснимым причинам отсутствующим в EqualityComparer<T>). Для этого создайте DelegateEqualityComparer<T> класс:

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

Затем напишите оболочки для методов GroupBy, чтобы принять делегат Func<TKey, TKey, bool> вместо параметра IEqualityComparer<TKey>. Эти методы заключают делегата в экземпляр DelegateEqualityComparer<T> и передают его соответствующему методу GroupBy. Пример:

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

Наконец, на своем сайте вызова вы должны использовать что-то вроде этого выражения для аргумента equalityComparison:

(a, b) => a.PeriodEnd.Equals(b.PeriodEnd)
    && a.Component.Code.Equals(b.Component.Code)
    && a.GroupCode.Equals(b.GroupCode)
person phoog    schedule 03.10.2012
comment
Вы не реализовали GetHashCode, поэтому он еще не компилируется. - person Jon Skeet; 03.10.2012
comment
@phoog - спасибо, сообщение Джона решило мою непосредственную проблему, но я собираюсь покопаться в этом немного позже, чтобы лучше понять GroupBy. - person Peter; 03.10.2012
comment
@JonSkeet Обратите внимание, что я исхожу из EqualityComparer<T>, а не IEqualityComparer<T>, поэтому мой отказ отменить виртуальный GetHashCode является логической ошибкой, а не ошибкой компиляции. Спасибо, что указали на это! Почему структура даже позволяет разделять EqualityComparer на подклассы? Мне было интересно это, когда я опубликовал вышеупомянутое, но теперь, когда я вижу, как это позволяет легче забыть о реализации GetHashCode (), мне вдвойне любопытно. - person phoog; 03.10.2012
comment
@phoog: EqualityComparer<T>.GetHashCode(T) - это абстрактный метод, поэтому он должен по-прежнему быть ошибкой компиляции. - person Jon Skeet; 03.10.2012
comment
ах, ты прав (краснеет). Я написал пример кода в VS, но, должно быть, сделал это, пока у меня был отключен ReSharper. Я добавил оговорку / опровержение в начале ответа. - person phoog; 03.10.2012