将一个委托包装在IEqualityComparer中

几个Linq.Enumerable函数采用IEqualityComparer<T> 。 有一个方便的包装类,适应delegate(T,T)=>bool实施IEqualityComparer<T> ? 编写一个(如果你忽略了定义一个正确的哈希码的问题)很容易,但我想知道是否有一个开箱即用的解决scheme。

具体来说,我想在Dictionary上进行设置操作,只使用Keys来定义成员资格(同时根据不同的规则保留值)。

通常情况下,我会通过评论@Sam来解决问题(我已经在原始文章上进行了一些编辑,以便在不改变行为的情况下进行一些修改)。

以下是我对@Sam 的回答 ,对默认哈希策略的[IMNSHO]重要修复:

 class FuncEqualityComparer<T> : IEqualityComparer<T> { readonly Func<T, T, bool> _comparer; readonly Func<T, int> _hash; public FuncEqualityComparer( Func<T, T, bool> comparer ) : this( comparer, t => 0 ) // NB Cannot assume anything about how eg, t.GetHashCode() interacts with the comparer's behavior { } public FuncEqualityComparer( Func<T, T, bool> comparer, Func<T, int> hash ) { _comparer = comparer; _hash = hash; } public bool Equals( T x, T y ) { return _comparer( x, y ); } public int GetHashCode( T obj ) { return _hash( obj ); } } 

关于GetHashCode的重要性

其他人已经评论说,任何自定义的IEqualityComparer<T>实现应该包括一个GetHashCode方法 ; 但没有人打算解释为什么在任何细节。

这是为什么。 你的问题特别提到了LINQ扩展方法; 几乎所有这些依靠哈希代码正常工作,因为他们在内部利用哈希表来提高效率。

Distinct ,例如。 考虑这个扩展方法的含义,如果它使用的是一个Equals方法。 如果只有一个项目已经被顺序扫描了,如果你只有一个项目,你如何确定? 您列举了您已经查看的整个值集合并检查匹配。 这将导致Distinct使用最坏情况O(N 2 )algorithm,而不是一个O(N)之一!

幸运的是,事实并非如此。 Distinct只是使用Equals ; 它也使用GetHashCode 。 事实上, 如果没有提供正确的GetHashCodeIEqualityComparer<T>它绝对不能正常工作 。 下面是一个说明这个的人为的例子。

说我有以下types:

 class Value { public string Name { get; private set; } public int Number { get; private set; } public Value(string name, int number) { Name = name; Number = number; } public override string ToString() { return string.Format("{0}: {1}", Name, Number); } } 

现在说我有一个List<Value> ,我想查找具有不同名称的所有元素。 对于使用自定义相等比较器的Distinct这是一个完美的用例。 那么让我们使用Aku的答案中的Comparer<T>类:

 var comparer = new Comparer<Value>((x, y) => x.Name == y.Name); 

现在,如果我们有一堆Value元素具有相同的Name属性,他们都应该崩溃到Distinct返回一个值,对不对? 让我们来看看…

 var values = new List<Value>(); var random = new Random(); for (int i = 0; i < 10; ++i) { values.Add("x", random.Next()); } var distinct = values.Distinct(comparer); foreach (Value x in distinct) { Console.WriteLine(x); } 

输出:

 x:1346013431
 x:1388845717
 x:1576754134
 x:1104067189
 x:1144789201
 x:1862076501
 x:1573781440
 x:646797592
 x:655632802
 x:1206819377

呃,那不行,是吗?

那么GroupBy呢? 我们试试看:

 var grouped = values.GroupBy(x => x, comparer); foreach (IGrouping<Value> g in grouped) { Console.WriteLine("[KEY: '{0}']", g); foreach (Value x in g) { Console.WriteLine(x); } } 

输出:

 [KEY ='x:1346013431']
 x:1346013431
 [KEY ='x:1388845717']
 x:1388845717
 [KEY ='x:1576754134']
 x:1576754134
 [KEY ='x:1104067189']
 x:1104067189
 [KEY ='x:1144789201']
 x:1144789201
 [KEY ='x:1862076501']
 x:1862076501
 [KEY ='x:1573781440']
 x:1573781440
 [KEY ='x:646797592']
 x:646797592
 [KEY ='x:655632802']
 x:655632802
 [KEY ='x:1206819377']
 x:1206819377

再次:没有工作。

如果你仔细想一下, Distinct在内部使用HashSet<T> (或者等效), GroupBy在内部使用类似于Dictionary<TKey, List<T>>东西是有意义的。 这能解释为什么这些方法不起作用吗? 让我们试试这个:

 var uniqueValues = new HashSet<Value>(values, comparer); foreach (Value x in uniqueValues) { Console.WriteLine(x); } 

输出:

 x:1346013431
 x:1388845717
 x:1576754134
 x:1104067189
 x:1144789201
 x:1862076501
 x:1573781440
 x:646797592
 x:655632802
 x:1206819377

是啊…开始有意义?

希望从这些例子中明白为什么在任何IEqualityComparer<T>实现中包含适当的GetHashCode非常重要。


原始答案

扩展orip的答案 :

在这里可以做一些改进。

  1. 首先,我将采用Func<T, TKey>而不是Func<T, object> ; 这将防止在实际的keyExtractor本身中装箱数值types的键。
  2. 其次,我实际上添加了一个where TKey : IEquatable<TKey>约束; 这将阻止Equals调用中的装箱( object.Equals接受一个object参数;您需要一个IEquatable<TKey>实现来获取TKey参数而不用装箱)。 显然这可能会造成太严重的限制,所以你可以创build一个没有约束的基类和派生类。

下面是结果代码的样子:

 public class KeyEqualityComparer<T, TKey> : IEqualityComparer<T> { protected readonly Func<T, TKey> keyExtractor; public KeyEqualityComparer(Func<T, TKey> keyExtractor) { this.keyExtractor = keyExtractor; } public virtual bool Equals(T x, T y) { return this.keyExtractor(x).Equals(this.keyExtractor(y)); } public int GetHashCode(T obj) { return this.keyExtractor(obj).GetHashCode(); } } public class StrictKeyEqualityComparer<T, TKey> : KeyEqualityComparer<T, TKey> where TKey : IEquatable<TKey> { public StrictKeyEqualityComparer(Func<T, TKey> keyExtractor) : base(keyExtractor) { } public override bool Equals(T x, T y) { // This will use the overload that accepts a TKey parameter // instead of an object parameter. return this.keyExtractor(x).Equals(this.keyExtractor(y)); } } 

当你想自定义平等检查时,99%的时间你有兴趣定义的关键要比较,而不是比较本身。

这可能是一个很好的解决scheme(来自Python的列表sorting方法的概念)。

用法:

 var foo = new List<string> { "abc", "de", "DE" }; // case-insensitive distinct var distinct = foo.Distinct(new KeyEqualityComparer<string>( x => x.ToLower() ) ); 

KeyEqualityComparer类:

 public class KeyEqualityComparer<T> : IEqualityComparer<T> { private readonly Func<T, object> keyExtractor; public KeyEqualityComparer(Func<T,object> keyExtractor) { this.keyExtractor = keyExtractor; } public bool Equals(T x, T y) { return this.keyExtractor(x).Equals(this.keyExtractor(y)); } public int GetHashCode(T obj) { return this.keyExtractor(obj).GetHashCode(); } } 

恐怕现在没有这种包装盒了。 然而,创build一个并不困难:

 class Comparer<T>: IEqualityComparer<T> { private readonly Func<T, T, bool> _comparer; public Comparer(Func<T, T, bool> comparer) { if (comparer == null) throw new ArgumentNullException("comparer"); _comparer = comparer; } public bool Equals(T x, T y) { return _comparer(x, y); } public int GetHashCode(T obj) { return obj.ToString().ToLower().GetHashCode(); } } ... Func<int, int, bool> f = (x, y) => x == y; var comparer = new Comparer<int>(f); Console.WriteLine(comparer.Equals(1, 1)); Console.WriteLine(comparer.Equals(1, 2)); 

和丹涛的答案一样,但有一些改进:

  1. 依赖于EqualityComparer<>.Default做实际的比较,以避免实现IEquatable<>值types( struct s)的IEquatable<>

  2. 由于EqualityComparer<>.Default使用它不爆炸null.Equals(something)

  3. IEqualityComparer<>提供了一个静态方法来创build比较器的实例,从而简化了调用。 比较

     Equality<Person>.CreateComparer(p => p.ID); 

     new EqualityComparer<Person, int>(p => p.ID); 
  4. 添加了一个重载指定IEqualityComparer<>的密钥。

class上:

 public static class Equality<T> { public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector) { return CreateComparer(keySelector, null); } public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector, IEqualityComparer<V> comparer) { return new KeyEqualityComparer<V>(keySelector, comparer); } class KeyEqualityComparer<V> : IEqualityComparer<T> { readonly Func<T, V> keySelector; readonly IEqualityComparer<V> comparer; public KeyEqualityComparer(Func<T, V> keySelector, IEqualityComparer<V> comparer) { if (keySelector == null) throw new ArgumentNullException("keySelector"); this.keySelector = keySelector; this.comparer = comparer ?? EqualityComparer<V>.Default; } public bool Equals(T x, T y) { return comparer.Equals(keySelector(x), keySelector(y)); } public int GetHashCode(T obj) { return comparer.GetHashCode(keySelector(obj)); } } } 

你可以像这样使用它:

 var comparer1 = Equality<Person>.CreateComparer(p => p.ID); var comparer2 = Equality<Person>.CreateComparer(p => p.Name); var comparer3 = Equality<Person>.CreateComparer(p => p.Birthday.Year); var comparer4 = Equality<Person>.CreateComparer(p => p.Name, StringComparer.CurrentCultureIgnoreCase); 

人是一个简单的类:

 class Person { public int ID { get; set; } public string Name { get; set; } public DateTime Birthday { get; set; } } 
 public class FuncEqualityComparer<T> : IEqualityComparer<T> { readonly Func<T, T, bool> _comparer; readonly Func<T, int> _hash; public FuncEqualityComparer( Func<T, T, bool> comparer ) : this( comparer, t => t.GetHashCode()) { } public FuncEqualityComparer( Func<T, T, bool> comparer, Func<T, int> hash ) { _comparer = comparer; _hash = hash; } public bool Equals( T x, T y ) { return _comparer( x, y ); } public int GetHashCode( T obj ) { return _hash( obj ); } } 

通过扩展名:

 public static class SequenceExtensions { public static bool SequenceEqual<T>( this IEnumerable<T> first, IEnumerable<T> second, Func<T, T, bool> comparer ) { return first.SequenceEqual( second, new FuncEqualityComparer<T>( comparer ) ); } public static bool SequenceEqual<T>( this IEnumerable<T> first, IEnumerable<T> second, Func<T, T, bool> comparer, Func<T, int> hash ) { return first.SequenceEqual( second, new FuncEqualityComparer<T>( comparer, hash ) ); } } 

奥普的回答非常好。

这里有一个小小的扩展方法使其更加简单:

 public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list, Func<T, object> keyExtractor) { return list.Distinct(new KeyEqualityComparer<T>(keyExtractor)); } var distinct = foo.Distinct(x => x.ToLower()) 

我要回答我自己的问题。 为了将字典视为集合,最简单的方法似乎是将集合运算应用于字典键,然后使用Enumerable.ToDictionary(…)将其转换回字典。

(德文文本)的实现使用lambdaexpression式实现IEqualityCompare关注空值,并使用扩展方法生成IEqualityComparer。

要在Linq中创build一个IEqualityComparer,你只需要写

 persons1.Union(persons2, person => person.LastName) 

比较器:

 public class LambdaEqualityComparer<TSource, TComparable> : IEqualityComparer<TSource> { Func<TSource, TComparable> _keyGetter; public LambdaEqualityComparer(Func<TSource, TComparable> keyGetter) { _keyGetter = keyGetter; } public bool Equals(TSource x, TSource y) { if (x == null || y == null) return (x == null && y == null); return object.Equals(_keyGetter(x), _keyGetter(y)); } public int GetHashCode(TSource obj) { if (obj == null) return int.MinValue; var k = _keyGetter(obj); if (k == null) return int.MaxValue; return k.GetHashCode(); } } 

您还需要添加一个扩展方法来支持types推断

 public static class LambdaEqualityComparer { // source1.Union(source2, lambda) public static IEnumerable<TSource> Union<TSource, TComparable>( this IEnumerable<TSource> source1, IEnumerable<TSource> source2, Func<TSource, TComparable> keySelector) { return source1.Union(source2, new LambdaEqualityComparer<TSource, TComparable>(keySelector)); } } 

只需进行一次优化:我们可以使用开箱即用的EqualityComparer进行值比较,而不是委托它。

这也将使实现更清洁,因为实际的比较逻辑现在停留在你可能已经超载的GetHashCode()和Equals()中。

这里是代码:

 public class MyComparer<T> : IEqualityComparer<T> { public bool Equals(T x, T y) { return EqualityComparer<T>.Default.Equals(x, y); } public int GetHashCode(T obj) { return obj.GetHashCode(); } } 

不要忘记在你的对象上重载GetHashCode()和Equals()方法。

这篇文章帮助了我: c#比较两个通用值

苏希尔

奥普的回答非常好。 扩展orip的答案:

我认为解决scheme的关键是使用“扩展方法”来传输“匿名types”。

  public static class Comparer { public static IEqualityComparer<T> CreateComparerForElements<T>(this IEnumerable<T> enumerable, Func<T, object> keyExtractor) { return new KeyEqualityComparer<T>(keyExtractor); } } 

用法:

 var n = ItemList.Select(s => new { s.Vchr, s.Id, s.Ctr, s.Vendor, s.Description, s.Invoice }).ToList(); n.AddRange(OtherList.Select(s => new { s.Vchr, s.Id, s.Ctr, s.Vendor, s.Description, s.Invoice }).ToList();); n = n.Distinct(x=>new{Vchr=x.Vchr,Id=x.Id}).ToList(); 
 public static Dictionary<TKey, TValue> Distinct<TKey, TValue>(this IEnumerable<TValue> items, Func<TValue, TKey> selector) { Dictionary<TKey, TValue> result = null; ICollection collection = items as ICollection; if (collection != null) result = new Dictionary<TKey, TValue>(collection.Count); else result = new Dictionary<TKey, TValue>(); foreach (TValue item in items) result[selector(item)] = item; return result; } 

这样就可以像这样select一个lambda属性: .Select(y => y.Article).Distinct(x => x.ArticleID);

我不知道现有的类,但是像这样的:

 public class MyComparer<T> : IEqualityComparer<T> { private Func<T, T, bool> _compare; MyComparer(Func<T, T, bool> compare) { _compare = compare; } public bool Equals(T x, Ty) { return _compare(x, y); } public int GetHashCode(T obj) { return obj.GetHashCode(); } } 

注:我还没有真正编译和运行这个,所以可能有一个错字或其他错误。