将委托包装在IEqualityComparer中


127

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

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

Answers:


44

通常,我可以通过在答案上评论@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 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 );
    }
}

5
就我而言,这是正确的答案。任何IEqualityComparer<T>遗漏的东西GetHashCode都是直截了当的。
丹涛

1
@约书亚·弗兰克(Joshua Frank):使用散列等于来暗示等于是无效的-只有反之成立。简而言之,@ Dan Tao所说的完全正确,这个答案只是将这个事实应用于以前不完整的答案
Ruben Bartelink 2010年

2
@Ruben Bartelink:感谢您的澄清。但是我仍然不理解您的t => 0的哈希策略。如果所有对象总是哈希到同一事物(零),那么@Dan Tao的观点是否比使用obj.GetHashCode 糟糕?为什么不总是强迫调用者提供良好的哈希函数?
约书亚·弗兰克

1
因此,不合理地认为,尽管哈希码不同,但在Func中提供的任意算法不可能返回true。您始终返回零就是不散列的观点是正确的。这就是为什么当探查器告诉我们搜索效率不够高时,使用哈希函数Func的重载。所有这一切的唯一要点是,如果您要使用默认的哈希算法,则该算法应该可以在100%的时间内工作并且不具有危险的表面正确行为。然后我们可以进行表演!
Ruben Bartelink 2010年

4
换句话说,由于您使用的是自定义比较器,因此它与与默认比较器相关的对象的默认哈希码无关,因此您无法使用它。
Peet Brits

170

关于重要性 GetHashCode

其他人已经评论了一个事实,即任何定制IEqualityComparer<T>实现都应该真正包含一个GetHashCode方法 ; 但是没有人愿意详细解释原因

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

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

幸运的是,事实并非如此。Distinct只是Equals; 它也使用GetHashCode。实际上,如果没有提供适当的它绝对不能正常工作IEqualityComparer<T>GetHashCode。以下是一个人为设计的示例,说明了这一点。

说我有以下类型:

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

是的...开始有意义了吗?

希望从这些示例中可以清楚地看出,为什么GetHashCode在任何IEqualityComparer<T>实现中都包含适当的内容如此重要。


原始答案

扩展orip的答案

这里可以进行一些改进。

  1. 首先,我将使用Func<T, TKey>而不是Func<T, object>; 这样可以防止实际中将值类型键装箱keyExtractor
  2. 其次,我实际上要添加一个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));
    }
}

1
您的StrictKeyEqualityComparer.Equals方法似乎与相同KeyEqualityComparer.Equals。请问TKey : IEquatable<TKey>约束化妆TKey.Equals工作方式不同?
贾斯汀·摩根

2
@JustinMorgan:是的–在第一种情况下,由于TKey可能是任意类型,因此编译器将使用虚拟方法Object.Equals,该方法需要将值类型参数装箱,例如int。但是,在后一种情况下,由于TKey受限于实施IEquatable<TKey>TKey.Equals将使用不需要任何装箱的方法。
丹涛

2
非常有趣,感谢您的信息。在看到这些答案之前,我不知道GetHashCode具有这些LINQ含义。很高兴知道以备将来使用。
贾斯汀·摩根

1
@JohannesH:可能!本来StringKeyEqualityComparer<T, TKey>也不需要。
丹涛

1
+1 @DanTao:感谢您对为什么在.Net中定义相等时永远不应该忽略哈希码的一个很好的说明。
Marcelo Cantos 2013年

118

当您要自定义相等性检查时,您有99%的时间对定义要比较的键感兴趣,而不是比较本身。

这可能是一个优雅的解决方案(来自Python的list sort方法的概念)。

用法:

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();
    }
}

3
这比阿库的答案好得多。
Slaks 2010年

绝对正确的方法。我认为,我可以在自己的答案中提到一些改进。
丹涛

1
这是非常优雅的代码,但没有回答问题,这就是为什么我接受@aku的回答。我想要一个Func <T,T,bool>的包装器,并且我不需要提取密钥,因为该密钥已经在我的字典中分离出来了。
Marcelo Cantos

6
@Marcelo:很好,你可以做到;但是请注意,如果您要采用@aku的方法,则确实应该添加a Func<T, int>来提供T值的哈希码(如Ruben的答案中所建议)。否则,IEqualityComparer<T>剩下的实现就很不完善,尤其是在LINQ扩展方法中的有效性方面。请参阅我的答案以获取有关为什么的讨论。
丹涛

很好,但是如果选择的键是值类型,则将没有必要的装箱。使用TKey定义密钥可能会更好。
Graham Ambrose

48

恐怕没有现成的包装器。但是创建一个并不难:

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
但是,请谨慎使用GetHashCode的实现。如果实际上要在某种哈希表中使用它,则需要更强大的功能。
thecoop

46
此代码存在严重问题!很容易想出一个类,它具有两个在此比较器方面相等但具有不同哈希码的对象。
empi 2010年

10
为了解决这个问题,该类需要另一个成员,该成员private readonly Func<T, int> _hashCodeResolver也必须在构造函数中传递并在GetHashCode(...)方法中使用。
herzmeister

6
我很好奇:您为什么用obj.ToString().ToLower().GetHashCode()代替obj.GetHashCode()
贾斯汀·摩根

3
在此实现中,框架中IEqualityComparer<T>总是在后台使用哈希的位置(例如LINQ的GroupBy,Distinct,Except,Join等)以及有关哈希的MS合同已被破坏。这是MS的文档摘录:“需要执行一些操作,以确保如果Equals方法对两个对象x和y返回true,则GetHashCode方法为x返回的值必须等于为y返回的值。” 请参阅:msdn.microsoft.com/en-us/library/ms132155
devgeezer 2012年

22

与Dan Tao的答案相同,但有一些改进:

  1. 依靠进行EqualityComparer<>.Default实际比较,从而避免对struct已实现的值类型进行装箱IEquatable<>

  2. 自从EqualityComparer<>.Default使用以来,它不会爆炸null.Equals(something)

  3. 提供的静态包装器IEqualityComparer<>将具有用于创建比较器实例的静态方法-简化了调用。比较

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

    new EqualityComparer<Person, int>(p => p.ID);
  4. 添加了用于指定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);

人是一个简单的类:

class Person
{
    public int ID { get; set; }
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
}

3
+1用于提供一个实现,使您可以为密钥提供一个比较器。除了提供更大的灵活性之外,这还避免了对比较和散列使用装箱值类型。
devgeezer 2012年

2
这是这里最充实的答案。我还添加了一个空检查。完成。
nawfal

11
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)。在stackoverflow.com/questions/98033/…上
Ruben Bartelink

6

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())

2

我要回答我自己的问题。要将字典视为集合,最简单的方法似乎是将集合操作应用于dict.Keys,然后使用Enumerable.ToDictionary(...)转换回Dictionary。


2

的实现(德语文本)使用lambda表达式实现IEqualityCompare 关心空值,并使用扩展方法生成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));
       }
   }

1

只是一项优化:我们可以使用开箱即用的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#比较两个通用值

苏希尔


1
注意:与在stackoverflow.com/questions/98033/上的注释中标识的问题相同-无法假定obj.GetHashCode()有意义
Ruben Bartelink 2010年

4
我没有这个目的。您创建了一个等同于默认相等比较器的相等比较器。那么,为什么不直接使用它呢?
CodesInChaos

1

orip的答案很好。扩展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();

0
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);


-2

我不知道现有的课程,但类似:

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();
  }
}

注意:我尚未真正编译并运行此程序,因此可能存在输入错误或其他错误。


1
注意:与在stackoverflow.com/questions/98033/上的注释中标识的问题相同-无法假定obj.GetHashCode()有意义
Ruben Bartelink 2010年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.