128

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

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

4

13 回答 13

174

关于重要性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);
    }
}

输出:

[键='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如果您考虑一下,在内部使用 a HashSet<T>(或等效的)以及在内部GroupBy使用类似 a 的东西是有意义的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. 首先,我会用 aFunc<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));
    }
}
于 2010-09-15T16:39:50.217 回答
119

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

这可能是一个优雅的解决方案(来自 Python 的列表排序方法的概念)。

用法:

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();
    }
}
于 2009-08-06T14:41:23.787 回答
47

恐怕没有这种开箱即用的包装器。但是创建一个并不难:

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));
于 2008-09-18T23:52:01.247 回答
46

通常,我会通过在答案上评论 @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 );
    }
}
于 2010-09-15T16:13:49.633 回答
24

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

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

Person 是一个简单的类:

class Person
{
    public int ID { get; set; }
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
}
于 2011-08-02T14:25:17.287 回答
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 ) );
    }
}
于 2008-11-06T20:44:50.210 回答
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())
于 2011-05-27T09:44:57.567 回答
2

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

于 2008-09-19T00:09:28.773 回答
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));
       }
   }
于 2013-06-29T11:26:55.483 回答
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#比较两个通用值

寿司

于 2010-06-29T15:17:21.183 回答
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();
于 2012-04-06T06:52:59.073 回答
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);

于 2011-05-30T13:10:48.653 回答
-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();
  }
}

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

于 2008-09-18T23:55:44.873 回答