在 IEqualityComparer 中包装委托
Posted
技术标签:
【中文标题】在 IEqualityComparer 中包装委托【英文标题】:Wrap a delegate in an IEqualityComparer 【发布时间】:2008-09-18 23:34:18 【问题描述】:几个 Linq.Enumerable 函数采用 IEqualityComparer<T>
。是否有一个方便的包装类来适应delegate(T,T)=>bool
来实现IEqualityComparer<T>
?编写一个很容易(如果您忽略了定义正确哈希码的问题),但我想知道是否有开箱即用的解决方案。
具体来说,我想对Dictionary
s 进行设置操作,仅使用键来定义成员资格(同时根据不同的规则保留值)。
【问题讨论】:
【参考方案1】:关于GetHashCode
的重要性
其他人已经评论过任何自定义IEqualityComparer<T>
实现应该真正包含GetHashCode
方法;但是没有人会费心去详细解释为什么。
原因如下。您的问题特别提到了 LINQ 扩展方法;几乎所有这些都依赖哈希码才能正常工作,因为它们在内部利用哈希表来提高效率。
以Distinct
为例。如果它使用的只是Equals
方法,请考虑此扩展方法的含义。如果您只有Equals
,如何确定某个项目是否已按顺序扫描?您枚举您已经查看过的整个值集合并检查匹配项。这将导致Distinct
使用最坏情况的 O(N2) 算法而不是 O(N) 算法!
幸运的是,情况并非如此。 Distinct
不只是使用Equals
;它也使用GetHashCode
。事实上,如果没有提供正确 GetHashCode
的 IEqualityComparer<T>
,它绝对不会正常工作。下面是一个人为的例子来说明这一点。
假设我有以下类型:
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's answer 的Comparer<T>
类:
var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);
现在,如果我们有一堆具有相同Name
属性的Value
元素,它们应该都折叠成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);
输出:
[键='x:1346013431'] x: 1346013431 [键='x:1388845717'] x: 1388845717 [键='x:1576754134'] x: 1576754134 [键='x:1104067189'] x: 1104067189 [键='x:1144789201'] x: 1144789201 [键='x:1862076501'] x: 1862076501 [键='x:1573781440'] x: 1573781440 [键='x:646797592'] x: 646797592 [键='x:655632802'] x: 655632802 [键='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's answer:
这里可以进行一些改进。
-
首先,我会使用
Func<T, TKey>
而不是Func<T, object>
;这将防止在实际 keyExtractor
本身中对值类型键进行装箱。
其次,我实际上要添加一个where TKey : IEquatable<TKey>
约束;这将防止在 Equals
调用中装箱(object.Equals
采用 object
参数;您需要 IEquatable<TKey>
实现来采用 TKey
参数而不装箱)。显然,这可能会造成过于严格的限制,因此您可以创建一个没有约束的基类和一个有它的派生类。
生成的代码如下所示:
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));
【讨论】:
您的StrictKeyEqualityComparer.Equals
方法似乎与KeyEqualityComparer.Equals
相同。 TKey : IEquatable<TKey>
约束是否使 TKey.Equals
工作方式不同?
@JustinMorgan: 是的——在第一种情况下,由于TKey
可以是任意类型,编译器将使用虚拟方法Object.Equals
,这将需要对值类型参数进行装箱,例如, int
。然而,在后一种情况下,由于TKey
被限制为实现IEquatable<TKey>
,因此将使用不需要任何装箱的TKey.Equals
方法。
非常有趣,感谢您提供的信息。在看到这些答案之前,我不知道 GetHashCode 有这些 LINQ 含义。很高兴知道以备将来使用。
@JohannesH:可能!也会消除对StringKeyEqualityComparer<T, TKey>
的需求。
+1 @DanTao:迟来的感谢您对为什么在 .Net 中定义相等性时永远不应该忽略哈希码的解释。【参考方案2】:
当您想要自定义相等检查时,99% 的时间您都对定义要比较的键感兴趣,而不是比较本身。
这可能是一个优雅的解决方案(概念来自 Python 的 list sort method)。
用法:
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();
【讨论】:
这比aku 的回答好多。 绝对是正确的方法。在我看来,可以进行一些改进,我在自己的回答中已经提到过。 这是非常优雅的代码,但它没有回答问题,这就是我接受@aku 回答的原因。我想要一个 FuncFunc<T, int>
以提供T
值的哈希码(如已建议,例如, Ruben's answer)。否则,您留下的 IEqualityComparer<T>
实现会很糟糕,尤其是,它在 LINQ 扩展方法中的用处。请参阅我的答案以讨论为什么会这样。
这很好,但如果选择的键是值类型,则会出现不必要的装箱。也许有一个用于定义密钥的 TKey 会更好。【参考方案3】:
恐怕没有这种开箱即用的包装器。但是创建一个并不难:
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));
【讨论】:
但是,请小心 GetHashCode 的实现。如果你真的要在某种哈希表中使用它,你会想要一些更健壮的东西。 这段代码有严重问题!很容易想出一个类,它有两个在此比较器方面相等但具有不同哈希码的对象。 为了解决这个问题,该类需要另一个成员private readonly Func<T, int> _hashCodeResolver
,该成员也必须在构造函数中传递并在 GetHashCode(...)
方法中使用。
我很好奇:你为什么用obj.ToString().ToLower().GetHashCode()
而不是obj.GetHashCode()
?
框架中采用IEqualityComparer<T>
的地方总是在后台使用散列(例如,LINQ 的 GroupBy、Distinct、Except、Join 等),并且在此实现中,MS 与散列有关的合同被破坏.下面是 MS 的文档摘录:“需要实现以确保如果 Equals 方法为两个对象 x 和 y 返回 true,则 GetHashCode 方法为 x 返回的值必须等于为 y 返回的值。” 见:msdn.microsoft.com/en-us/library/ms132155【参考方案4】:
通常,我会通过在答案上评论 @Sam 来解决这个问题(我已经对原始帖子进行了一些编辑,以便在不改变行为的情况下稍微清理一下。)
以下是我对@Sam's answer 的重复片段,对默认散列策略进行了 [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 e.g., 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>
都直接坏掉了。
@Joshua Frank:使用哈希相等来暗示相等是无效的——只有反之才成立。总之,@Dan Tao 说的完全正确,而这个答案只是将这个事实应用到之前不完整的答案中
@Ruben Bartelink:感谢您的澄清。但我仍然不明白你的 t => 0 的散列策略。如果所有对象总是散列到相同的东西(零),那么这不是比使用 obj.GetHashCode 更更多破碎吗,根据@Dan Tao的观点?为什么不总是强制调用者提供一个好的散列函数呢?
因此,假设提供的 Func 中的任意算法不可能返回 true,尽管哈希码不同,这是不合理的。您认为始终返回零并不是散列的观点是正确的。这就是为什么当分析器告诉我们搜索效率不够高时,会出现一个使用散列函数的重载。所有这一切的唯一一点是,如果您要使用默认的哈希算法,它应该是 100% 的工作时间并且没有危险的表面上正确的行为。然后我们就可以开始表演了!
换句话说,由于您使用的是 custom 比较器,因此它与对象的 default 哈希码与 相关默认比较器,因此您不能使用它。【参考方案5】:
与丹涛的回答相同,但有一些改进:
依赖 EqualityComparer<>.Default
进行实际比较,以避免对已实现 IEquatable<>
的值类型 (struct
s) 进行装箱。
自从EqualityComparer<>.Default
使用它不会在null.Equals(something)
上爆炸。
在IEqualityComparer<>
周围提供静态包装器,它将有一个静态方法来创建比较器的实例 - 简化了调用。比较
Equality<Person>.CreateComparer(p => p.ID);
与
new EqualityComparer<Person, int>(p => p.ID);
添加了一个重载来为键指定IEqualityComparer<>
。
班级:
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);
Person 是一个简单的类:
class Person
public int ID get; set;
public string Name get; set;
public DateTime Birthday get; set;
【讨论】:
+1 用于提供一种实现,使您可以为密钥提供比较器。除了提供更大的灵活性之外,这还避免了比较和散列的装箱值类型。 这是这里最充实的答案。我还添加了一个空检查。完成。【参考方案6】: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 ) );
【讨论】:
@Sam(在此评论中不再存在):清理代码而不调整行为(和 +1'd)。在 ***.com/questions/98033/… 添加了 Riff【参考方案7】:orip 的回答很棒。
这里有一个小扩展方法,使它更容易:
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())
【讨论】:
【参考方案8】:我将回答我自己的问题。要将字典视为集合,最简单的方法似乎是将集合操作应用于 dict.Keys,然后使用 Enumerable.ToDictionary(...) 转换回字典。
【讨论】:
【参考方案9】:在(德文)Implementing IEqualityCompare with lambda expression 的实现 关心 null 值并使用扩展方法生成 IEqualityComparer。
要在 Linq 联合中创建 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();
你还需要添加一个扩展方法来支持类型推断
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));
【讨论】:
【参考方案10】:只有一项优化: 我们可以使用开箱即用的 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# compare two generic values
寿司
【讨论】:
NB 与***.com/questions/98033/… 评论中发现的相同问题 - 不能假设 obj.GetHashCode() 有意义 我不明白这个的目的。您创建了一个等效于默认相等比较器的相等比较器。那为什么不直接使用呢?【参考方案11】:orip's answer 很棒。扩展 orip 的答案:
我认为解决方案的关键是使用“扩展方法”来转移“匿名类型”。
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=>newVchr=x.Vchr,Id=x.Id).ToList();
【讨论】:
【参考方案12】: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;
这使得选择带有 lambda 的属性成为可能:.Select(y => y.Article).Distinct(x => x.ArticleID);
【讨论】:
【参考方案13】:我不知道现有的课程,但类似:
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();
注意:我还没有真正编译和运行它,所以可能有错字或其他错误。
【讨论】:
NB 与***.com/questions/98033/… 的评论中发现的相同问题 - 不能假设 obj.GetHashCode() 有意义以上是关于在 IEqualityComparer 中包装委托的主要内容,如果未能解决你的问题,请参考以下文章
如何在不可变的泛型 Pair 结构上实现 IEqualityComparer?
使用带有容差的 IEqualityComparer GetHashCode