LINQ GroupBy on multiple ref-type fields; Custom E

2019-02-26 23:54发布

So I've looked through about 20 examples on this on SO and elsewhere, but haven't found one which covers what I'm trying to do. This - Can I specify my explicit type comparator inline? - looks like what I need, but doesn't go far enough (or I don't understand how to take it further).

  • I have a List of LoadData, the LoadData object has fields of both reference and value types
  • Need to group on a mixture of ref and value fields, project the output to an anonymous type
  • Need (I think) to provide a custom IEqualityComparer to specify how to compare the GroupBy fields, but they are an anonymous type

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

The best GroupBy query I have going so far:

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

This groups, but there are still duplicates.

  1. How can I specify custom code to compare the GroupBy fields? For example, the Components could be compared by Component.Code.

2条回答
爷、活的狠高调
2楼-- · 2019-02-27 00:26

The problem here is that your key type is anonymous, which means you can't declare a class that implements IEqualityComparer<T> for that key type. While it would be possible to write a comparator which compared anonymous types for equality in a custom manner (via a generic method, delegates and type inference), it wouldn't be terribly pleasant.

The two simplest options are probably:

  • Make the anonymous type "just work" by overriding Equals/GetHashCode in PeriodEndDto and ComponentDto. If there's a natural equality you'd want to use everywhere, this is probably the sanest option. I'd recommend implementing IEquatable<T> as well
  • Don't use an anonymous type for grouping - use a named type, and then you can either override GetHashCode and Equals on that, or you could write a custom equality comparer in the normal way.

EDIT: ProjectionEqualityComparer wouldn't really work. It would be feasible to write something similar though - a sort of CompositeEqualityComparer which allowed you create an equality comparer from several "projection + comparer" pairs. It would be pretty ugly compared with the anonymous type though.

查看更多
时光不老,我们不散
3楼-- · 2019-02-27 00:27

EDIT:

As Jon Skeet points out, this solution seems better than it is, if you don't think too hard about it, because I have forgotten to implement GetHashCode. Having to implement GetHashCode makes this approach, as Jon says in his answer, "not terribly pleasant." Presumably, this is also the explanation for the (so-called "inexplicable") absence of EqualityComparer<T>.Create() in the framework. I'll leave the answer for reference, as examples of what not to do, can be instructive as well.

ORIGINAL ANSWER:

You could use the approach suggested by the Comparer<T>.Create pattern introduced in .NET 4.5 (but inexplicably absent in EqualityComparer<T>). To do so, create a DelegateEqualityComparer<T> class:

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

Then write wrappers around the GroupBy methods to accept a Func<TKey, TKey, bool> delegate in place of the IEqualityComparer<TKey> parameter. These methods wrap the delegate in a DelegateEqualityComparer<T> instance, and pass that on to the corresponding GroupBy method. Example:

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

Finally, at your call site, you would use something like this expression for the equalityComparison argument:

(a, b) => a.PeriodEnd.Equals(b.PeriodEnd)
    && a.Component.Code.Equals(b.Component.Code)
    && a.GroupCode.Equals(b.GroupCode)
查看更多
登录 后发表回答