Can you create a simple 'EqualityComparer<T>' using a lambda expression
重要提示:这不是LINQ-to-SQL问题。这是对象的Linq。
简短的问题:
在LinqToObjects中,是否有一种简单的方法可以根据对象的键属性从列表中获取不同的对象列表?
长问题:
我正在尝试对一个对象列表执行
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; } } |
我有一个包含
因为Web服务的工作方式,我有
这是我要使用的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)); |
问题是
我不想:
- 在
GalleryImage 上实现iequatable,因为它是生成的 - 必须编写一个单独的类来实现
IEqualityComparer ,如下所示
我是否遗漏了
我本以为有一种简单的方法可以从基于键的集合中获得"不同"的对象。
(这里有两个解决方案-见第二个解决方案的结尾):
我的miscUtil库有一个
下面是一个使用它的示例:
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中使用
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; } } } |
在这两种情况下,
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(); } } |
这是我能想到的解决手头问题的最好办法。尽管如此,仍然好奇是否有一种很好的方法可以在飞行中创建一个
1 | Galleries.SelectMany(x => x.Images).ToLookup(x => x.id).Select(x => x.First()); |
创建查找表并从每个表中选取"top"
注意:这和@charlie建议的是一样的,但是使用iLookup——我认为这是一个团队必须要做的。
您可以按键值分组,然后从每个组中选择最上面的项。这对你有用吗?
这个想法正在这里讨论,虽然我希望.NET核心团队采用一种方法从lambda生成
用途:
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服务检索对象,因此在本例中可能无法这样做。
那么一个废弃的
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(); } } |
所以现在您可以将
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实现的另一个文件。