【问题标题】:Wrap a delegate in an IEqualityComparer在 IEqualityComparer 中包装委托
【发布时间】:2008-09-18 23:34:18
【问题描述】:

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

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

【问题讨论】:

    标签: .net linq delegates


    【解决方案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&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:

    这里可以进行一些改进。

    1. 首先,我会使用Func&lt;T, TKey&gt; 而不是Func&lt;T, object&gt;;这将防止在实际 keyExtractor 本身中对值类型键进行装箱。
    2. 其次,我实际上要添加一个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 );
        }
    }
    

    【讨论】:

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

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

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

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

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

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

      与

      new EqualityComparer<Person, int>(p => p.ID);
      
    4. 添加了一个重载来为键指定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 ) );
        }
    }
    

    【讨论】:

    【解决方案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 与stackoverflow.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=>new{Vchr=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();
                }
              }
              

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

              【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2013-03-02
              • 1970-01-01
              • 2010-09-18
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多