在 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>?编写一个很容易(如果您忽略了定义正确哈希码的问题),但我想知道是否有开箱即用的解决方案。

具体来说,我想对Dictionarys 进行设置操作,仅使用键来定义成员资格(同时根据不同的规则保留值)。

【问题讨论】:

【参考方案1】:

关于GetHashCode的重要性

其他人已经评论过任何自定义IEqualityComparer<T> 实现应该真正包含GetHashCode 方法;但是没有人会费心去详细解释为什么

原因如下。您的问题特别提到了 LINQ 扩展方法;几乎所有这些都依赖哈希码才能正常工作,因为它们在内部利用哈希表来提高效率。

Distinct 为例。如果它使用的只是Equals 方法,请考虑此扩展方法的含义。如果您只有Equals,如何确定某个项目是否已按顺序扫描?您枚举您已经查看过的整个值集合并检查匹配项。这将导致Distinct 使用最坏情况的 O(N2) 算法而不是 O(N) 算法!

幸运的是,情况并非如此。 Distinct只是使用Equals;它也使用GetHashCode。事实上,如果没有提供正确 GetHashCodeIEqualityComparer<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&lt;T&gt;(或等效)是有意义的,GroupBy 在内部使用类似 Dictionary&lt;TKey, List&lt;T&gt;&gt; 的东西是有意义的。这可以解释为什么这些方法不起作用吗?让我们试试这个:

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&lt;T&gt; 实现中包含适当的 GetHashCode 如此重要。


原答案

扩展至orip's answer:

这里可以进行一些改进。

    首先,我会使用Func&lt;T, TKey&gt; 而不是Func&lt;T, object&gt;;这将防止在实际 keyExtractor 本身中对值类型键进行装箱。 其次,我实际上要添加一个where TKey : IEquatable&lt;TKey&gt; 约束;这将防止在 Equals 调用中装箱(object.Equals 采用 object 参数;您需要 IEquatable&lt;TKey&gt; 实现来采用 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&lt;TKey&gt; 约束是否使 TKey.Equals 工作方式不同? @JustinMorgan: 是的——在第一种情况下,由于TKey 可以是任意类型,编译器将使用虚拟方法Object.Equals,这将需要对值类型参数进行装箱,例如, int。然而,在后一种情况下,由于TKey 被限制为实现IEquatable&lt;TKey&gt;,因此将使用不需要任何装箱的TKey.Equals 方法。 非常有趣,感谢您提供的信息。在看到这些答案之前,我不知道 GetHashCode 有这些 LINQ 含义。很高兴知道以备将来使用。 @JohannesH:可能!也会消除对StringKeyEqualityComparer&lt;T, TKey&gt; 的需求。 +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 回答的原因。我想要一个 Func 的包装器,我不需要提取密钥,因为密钥已经在我的字典中分离出来了。 @Marcelo:没关系,你可以这样做;但请注意,如果您打算采用@aku 的方法,您真的应该添加一个Func&lt;T, int&gt; 以提供T 值的哈希码(如已建议,例如, Ruben's answer)。否则,您留下的 IEqualityComparer&lt;T&gt; 实现会很糟糕,尤其是,它在 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&lt;T, int&gt; _hashCodeResolver,该成员也必须在构造函数中传递并在 GetHashCode(...) 方法中使用。 我很好奇:你为什么用obj.ToString().ToLower().GetHashCode()而不是obj.GetHashCode() 框架中采用IEqualityComparer&lt;T&gt; 的地方总是在后台使用散列(例如,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 );
    

【讨论】:

就我而言,这是正确的答案。任何离开GetHashCodeIEqualityComparer&lt;T&gt; 都直接坏掉了。 @Joshua Frank:使用哈希相等来暗示相等是无效的——只有反之才成立。总之,@Dan Tao 说的完全正确,而这个答案只是将这个事实应用到之前不完整的答案中 @Ruben Bartelink:感谢您的澄清。但我仍然不明白你的 t => 0 的散列策略。如果所有对象总是散列到相同的东西(零),那么这不是比使用 obj.GetHashCode 更更多破碎吗,根据@Dan Tao的观点?为什么不总是强制调用者提供一个好的散列函数呢? 因此,假设提供的 Func 中的任意算法不可能返回 true,尽管哈希码不同,这是不合理的。您认为始终返回零并不是散列的观点是正确的。这就是为什么当分析器告诉我们搜索效率不够高时,会出现一个使用散列函数的重载。所有这一切的唯一一点是,如果您要使用默认的哈希算法,它应该是 100% 的工作时间并且没有危险的表面上正确的行为。然后我们就可以开始表演了! 换句话说,由于您使用的是 custom 比较器,因此它与对象的 default 哈希码与 相关默认比较器,因此您不能使用它。【参考方案5】:

与丹涛的回答相同,但有一些改进:

    依赖 EqualityComparer&lt;&gt;.Default 进行实际比较,以避免对已实现 IEquatable&lt;&gt; 的值类型 (structs) 进行装箱。

    自从EqualityComparer&lt;&gt;.Default 使用它不会在null.Equals(something) 上爆炸。

    IEqualityComparer&lt;&gt; 周围提供静态包装器,它将有一个静态方法来创建比较器的实例 - 简化了调用。比较

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

    new EqualityComparer<Person, int>(p => p.ID);
    

    添加了一个重载来为键指定IEqualityComparer&lt;&gt;

班级:

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 =&gt; y.Article).Distinct(x =&gt; 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

Expression 类的目的是啥?

如何实现 IEqualityComparer 以返回不同的值?

c#List结合IEqualityComparer求交集

有没有办法从 IComparer 派生 IEqualityComparer?