关于c#:你能创建一个简单的’EqualityComparer< T>’

Can you create a simple 'EqualityComparer<T>' using a lambda expression

重要提示:这不是LINQ-to-SQL问题。这是对象的Linq。

简短的问题:

在LinqToObjects中,是否有一种简单的方法可以根据对象的键属性从列表中获取不同的对象列表?

长问题:

我正在尝试对一个对象列表执行Distinct()操作,该对象的属性之一是键。

1
2
3
4
5
6
class GalleryImage {
   public int Key { get;set; }
   public string Caption { get;set; }
   public string Filename { get; set; }
   public string[] Tags {g et; set; }
}

我有一个包含GalleryImage[]Gallery对象列表。

因为Web服务的工作方式,我有GalleryImage对象。我认为使用Distinct()获得一个明确的列表是一件简单的事情。

这是我要使用的LINQ查询:

1
2
3
var allImages = Galleries.SelectMany(x => x.Images);
var distinctImages = allImages.Distinct<GalleryImage>(new
                     EqualityComparer<GalleryImage>((a, b) => a.id == b.id));

问题是EqualityComparer是一个抽象类。

我不想:

  • GalleryImage上实现iequatable,因为它是生成的
  • 必须编写一个单独的类来实现IEqualityComparer,如下所示

我是否遗漏了EqualityComparer的具体实现?

我本以为有一种简单的方法可以从基于键的集合中获得"不同"的对象。


(这里有两个解决方案-见第二个解决方案的结尾):

我的miscUtil库有一个ProjectionEqualityComparer类(和两个支持类来使用类型推断)。

下面是一个使用它的示例:

1
2
EqualityComparer<GalleryImage> comparer =
    ProjectionEqualityComparer<GalleryImage>.Create(x => x.id);

这是代码(删除了注释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// Helper class for construction
public static class ProjectionEqualityComparer
{
    public static ProjectionEqualityComparer<TSource, TKey>
        Create<TSource, TKey>(Func<TSource, TKey> projection)
    {
        return new ProjectionEqualityComparer<TSource, TKey>(projection);
    }

    public static ProjectionEqualityComparer<TSource, TKey>
        Create<TSource, TKey> (TSource ignored,
                               Func<TSource, TKey> projection)
    {
        return new ProjectionEqualityComparer<TSource, TKey>(projection);
    }
}

public static class ProjectionEqualityComparer<TSource>
{
    public static ProjectionEqualityComparer<TSource, TKey>
        Create<TKey>(Func<TSource, TKey> projection)
    {
        return new ProjectionEqualityComparer<TSource, TKey>(projection);
    }
}

public class ProjectionEqualityComparer<TSource, TKey>
    : IEqualityComparer<TSource>
{
    readonly Func<TSource, TKey> projection;
    readonly IEqualityComparer<TKey> comparer;

    public ProjectionEqualityComparer(Func<TSource, TKey> projection)
        : this(projection, null)
    {
    }

    public ProjectionEqualityComparer(
        Func<TSource, TKey> projection,
        IEqualityComparer<TKey> comparer)
    {
        projection.ThrowIfNull("projection");
        this.comparer = comparer ?? EqualityComparer<TKey>.Default;
        this.projection = projection;
    }

    public bool Equals(TSource x, TSource y)
    {
        if (x == null && y == null)
        {
            return true;
        }
        if (x == null || y == null)
        {
            return false;
        }
        return comparer.Equals(projection(x), projection(y));
    }

    public int GetHashCode(TSource obj)
    {
        if (obj == null)
        {
            throw new ArgumentNullException("obj");
        }
        return comparer.GetHashCode(projection(obj));
    }
}

二解

为此,您可以在morelinq中使用DistinctBy扩展名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    public static IEnumerable<TSource> DistinctBy<TSource, TKey>
        (this IEnumerable<TSource> source,
         Func<TSource, TKey> keySelector)
    {
        return source.DistinctBy(keySelector, null);
    }

    public static IEnumerable<TSource> DistinctBy<TSource, TKey>
        (this IEnumerable<TSource> source,
         Func<TSource, TKey> keySelector,
         IEqualityComparer<TKey> comparer)
    {
        source.ThrowIfNull("source");
        keySelector.ThrowIfNull("keySelector");
        return DistinctByImpl(source, keySelector, comparer);
    }

    private static IEnumerable<TSource> DistinctByImpl<TSource, TKey>
        (IEnumerable<TSource> source,
         Func<TSource, TKey> keySelector,
         IEqualityComparer<TKey> comparer)
    {
        HashSet<TKey> knownKeys = new HashSet<TKey>(comparer);
        foreach (TSource element in source)
        {
            if (knownKeys.Add(keySelector(element)))
            {
                yield return element;
            }
        }
    }

在这两种情况下,ThrowIfNull看起来如下:

1
2
3
4
5
6
7
public static void ThrowIfNull<T>(this T data, string name) where T : class
{
    if (data == null)
    {
        throw new ArgumentNullException(name);
    }
}


基于charlie flowers的答案,您可以创建自己的扩展方法来执行您想要的操作,内部使用分组:

1
2
3
4
5
6
7
8
    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();
    }

您还可以创建一个从EqualityComparer派生的泛型类,但听起来您希望避免这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    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();
        }
    }


这是我能想到的解决手头问题的最好办法。尽管如此,仍然好奇是否有一种很好的方法可以在飞行中创建一个EqualityComparer

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

创建查找表并从每个表中选取"top"

注意:这和@charlie建议的是一样的,但是使用iLookup——我认为这是一个团队必须要做的。


您可以按键值分组,然后从每个组中选择最上面的项。这对你有用吗?


这个想法正在这里讨论,虽然我希望.NET核心团队采用一种方法从lambda生成IEqualityComparer,但我建议您对此想法进行投票和评论,并使用以下内容:

用途:

1
2
3
4
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; } }

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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.");
  }
}

这是一篇有趣的文章,它扩展了Linq的功能…http://www.singingeels.com/articles/extending_linq_u在_the_distinct_function.aspx中指定_a_property_

默认的distinct根据对象的哈希代码对其进行比较-为了方便地使用distinct,可以重写gethashcode方法。但您提到您正在从Web服务检索对象,因此在本例中可能无法这样做。


那么一个废弃的IEqualityComparer泛型类呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();
  }
}

所以现在您可以将Distinct与自定义比较器一起使用。

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

您也许可以摆脱,但我不确定编译器是否可以推断出类型(现在没有访问它的权限)。

在另一种扩展方法中:

1
2
3
4
5
6
7
8
9
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...
}


implement IEquatable on GalleryImage because it is generated

另一种方法是将galleryimage生成为分部类,然后使用继承和iequatable、equals和gethash实现的另一个文件。