Can you create a simple 'EqualityComparer' using a lambda expression

前端 未结 8 1463
半阙折子戏
半阙折子戏 2020-12-13 18:20

Short question:

Is there a simple way in LINQ to objects to get a distinct list of objects from a list based on a key property on the objects.

相关标签:
8条回答
  • 2020-12-13 18:41

    This idea is being debated here, and while I'm hoping the .NET Core team adopt a method to generate IEqualityComparer<T>s from lambda, I'd suggest you to please vote and comment on that idea, and use the following:

    Usage:

    IEqualityComparer<Contact> comp1 = EqualityComparerImpl<Contact>.Create(c => c.Name);
    var comp2 = EqualityComparerImpl<Contact>.Create(c => c.Name, c => c.Age);
    
    class Contact { public Name { get; set; } public Age { get; set; } }
    

    Code:

    public class EqualityComparerImpl<T> : IEqualityComparer<T>
    {
      public static EqualityComparerImpl<T> Create(
        params Expression<Func<T, object>>[] properties) =>
        new EqualityComparerImpl<T>(properties);
    
      PropertyInfo[] _properties;
      EqualityComparerImpl(Expression<Func<T, object>>[] properties)
      {
        if (properties == null)
          throw new ArgumentNullException(nameof(properties));
    
        if (properties.Length == 0)
          throw new ArgumentOutOfRangeException(nameof(properties));
    
        var length = properties.Length;
        var extractions = new PropertyInfo[length];
        for (int i = 0; i < length; i++)
        {
          var property = properties[i];
          extractions[i] = ExtractProperty(property);
        }
        _properties = extractions;
      }
    
      public bool Equals(T x, T y)
      {
        if (ReferenceEquals(x, y))
          //covers both are null
          return true;
        if (x == null || y == null)
          return false;
        var len = _properties.Length;
        for (int i = 0; i < _properties.Length; i++)
        {
          var property = _properties[i];
          if (!Equals(property.GetValue(x), property.GetValue(y)))
            return false;
        }
        return true;
      }
    
      public int GetHashCode(T obj)
      {
        if (obj == null)
          return 0;
    
        var hashes = _properties
            .Select(pi => pi.GetValue(obj)?.GetHashCode() ?? 0).ToArray();
        return Combine(hashes);
      }
    
      static int Combine(int[] hashes)
      {
        int result = 0;
        foreach (var hash in hashes)
        {
          uint rol5 = ((uint)result << 5) | ((uint)result >> 27);
          result = ((int)rol5 + result) ^ hash;
        }
        return result;
      }
    
      static PropertyInfo ExtractProperty(Expression<Func<T, object>> property)
      {
        if (property.NodeType != ExpressionType.Lambda)
          throwEx();
    
        var body = property.Body;
        if (body.NodeType == ExpressionType.Convert)
          if (body is UnaryExpression unary)
            body = unary.Operand;
          else
            throwEx();
    
        if (!(body is MemberExpression member))
          throwEx();
    
        if (!(member.Member is PropertyInfo pi))
          throwEx();
    
        return pi;
    
        void throwEx() =>
          throw new NotSupportedException($"The expression '{property}' isn't supported.");
      }
    }
    
    0 讨论(0)
  • 2020-12-13 18:44

    You could group by the key value and then select the top item from each group. Would that work for you?

    0 讨论(0)
  • 2020-12-13 18:48

    This is the best i can come up with for the problem in hand. Still curious whether theres a nice way to create a EqualityComparer on the fly though.

    Galleries.SelectMany(x => x.Images).ToLookup(x => x.id).Select(x => x.First());
    

    Create lookup table and take 'top' from each one

    Note: this is the same as @charlie suggested but using ILookup - which i think is what a group must be anyway.

    0 讨论(0)
  • 2020-12-13 18:53

    implement IEquatable on GalleryImage because it is generated

    A different approach would be to generate GalleryImage as a partial class, and then have another file with the inheritance and IEquatable, Equals, GetHash implementation.

    0 讨论(0)
  • 2020-12-13 18:54

    Building on Charlie Flowers' answer, you can create your own extension method to do what you want which internally uses grouping:

        public static IEnumerable<T> Distinct<T, U>(
            this IEnumerable<T> seq, Func<T, U> getKey)
        {
            return
                from item in seq
                group item by getKey(item) into gp
                select gp.First();
        }
    

    You could also create a generic class deriving from EqualityComparer, but it sounds like you'd like to avoid this:

        public class KeyEqualityComparer<T,U> : IEqualityComparer<T>
        {
            private Func<T,U> GetKey { get; set; }
    
            public KeyEqualityComparer(Func<T,U> getKey) {
                GetKey = getKey;
            }
    
            public bool Equals(T x, T y)
            {
                return GetKey(x).Equals(GetKey(y));
            }
    
            public int GetHashCode(T obj)
            {
                return GetKey(obj).GetHashCode();
            }
        }
    
    0 讨论(0)
  • 2020-12-13 18:59

    What about a throw away IEqualityComparer generic class?

    public class ThrowAwayEqualityComparer<T> : IEqualityComparer<T>
    {
      Func<T, T, bool> comparer;
    
      public ThrowAwayEqualityComparer(Func<T, T, bool> comparer)   
      {
        this.comparer = comparer;
      }
    
      public bool Equals(T a, T b)
      {
        return comparer(a, b);
      }
    
      public int GetHashCode(T a)
      {
        return a.GetHashCode();
      }
    }
    

    So now you can use Distinct with a custom comparer.

    var distinctImages = allImages.Distinct(
       new ThrowAwayEqualityComparer<GalleryImage>((a, b) => a.Key == b.Key));
    

    You might be able to get away with the <GalleryImage>, but I'm not sure if the compiler could infer the type (don't have access to it right now.)

    And in an additional extension method:

    public static class IEnumerableExtensions
    {
      public static IEnumerable<TValue> Distinct<TValue>(this IEnumerable<TValue> @this, Func<TValue, TValue, bool> comparer)
      {
        return @this.Distinct(new ThrowAwayEqualityComparer<TValue>(comparer);
      }
    
      private class ThrowAwayEqualityComparer...
    }
    
    0 讨论(0)
提交回复
热议问题