在 IEqualityComparer 中包装委托

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

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

48461 次浏览

恐怕没有这样的外包装,但是创建一个外包装并不难:

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

我不知道有没有现成的课程,但是大概是这样的:

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

注意: 我实际上还没有编译和运行这个,所以可能有一个打字错误或其他错误。

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

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

当您想要自定义相等性检查时,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();
}
}

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

Sushil

通常情况下,我会通过在答案上注释@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 );
}
}

GetHashCode的重要性

其他人已经对任何定制的 IEqualityComparer<T>实现 应该包含一个 GetHashCode方法这一事实发表了评论; 但是没有人愿意费心解释 为什么的任何细节。

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

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

幸运的是,事实并非如此。Distinct不使用 只是使用 Equals; 它也使用 GetHashCode。事实上,是 如果没有一个提供正确 ABC2的 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的完美用例。因此,让我们使用 阿库的回答中的 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);
}
}

产出:

[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

是啊... 开始有点道理了?

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


原始答案

拓展 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));
}
}

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

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

  1. 依赖于 EqualityComparer<>.Default进行实际的比较,以避免对已实现 IEquatable<>的值类型(struct)进行装箱。

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

实现在(德文文本) < a href = “ http://web.archive ve.org/web/20151126060513/http://flurfunk.sdx-ag.de/2013/06/linq-iequalitycomprer-duch-lambda.html”rel = “ nofollow norefrer”> 实现 IEqualityCompare 与 lambda 表达式比较 关注 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));
}
}
public class DelegateEqualityComparer<T>: IEqualityComparer<T>
{
private readonly Func<T, T, bool> _equalsDelegate;
private readonly Func<T, int>     _getHashCodeDelegate;


public DelegateEqualityComparer(Func<T, T, bool> equalsDelegate, Func<T, int> getHashCodeDelegate)
{
_equalsDelegate      = equalsDelegate      ?? ((tx, ty) => object.Equals(tx, ty));
_getHashCodeDelegate = getHashCodeDelegate ?? (t => t.GetSafeHashCode());
}


public bool Equals(T x, T y) => _equalsDelegate(x, y);


public int GetHashCode(T obj) => _getHashCodeDelegate(obj);
}