将委托包装在IEqualityComparer中

时间:2020-03-06 14:24:01  来源:igfitidea点击:

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

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

解决方案

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

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.Keys,然后使用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()方法。

这篇文章对我有帮助:ccompare两个通用值

苏希尔

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

其他人已经评论了这样一个事实,即任何自定义的IEqualityComparer <T>实现都应确实包含一个GetHashCode方法。但是没有人愿意详细解释原因。

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

Distinct为例。如果使用的所有扩展方法都是"等于"方法,请考虑一下此扩展方法的含义。如果我们只有"等于",如何确定某个项目是否已按顺序扫描?我们列举了已经查看过的所有值,并检查是否匹配。这将导致使用最差情况的O(N2)算法而不是O(N)算法来实现"区别"!

幸运的是,事实并非如此。不同的不只是使用等于。它也使用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回答中的Comparer &lt;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 &lt;T>(或者等效方法),对GroupBy使用类似Dictionary &lt;TKey,List &lt;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的答案:

这里可以进行一些改进。

  • 首先,我将使用Func &lt;T,TKey>而不是Func &lt;T,object>;这将防止在实际的" keyExtractor"本身中将值类型键装箱。
  • 其次,我实际上要添加一个where TKey:IEquatable &lt;TKey>约束;这将防止在"等于"调用中装箱(" 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));
    }
}