当前位置:  开发笔记 > 编程语言 > 正文

在IEqualityComparer中包装委托

如何解决《在IEqualityComparer中包装委托》经验,为你挑选了7个好方法。

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

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



1> Dan Tao..:
关于的重要性 GetHashCode

其他人已经评论过这样一个事实:任何自定义IEqualityComparer实现都应该包含一个GetHashCode方法 ; 但是没有人愿意在任何细节上解释原因.

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

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

幸运的是,事实并非如此.Distinct只是使用Equals; 它也用GetHashCode.事实上,如果没有提供适当的产品,它绝对不能正常工作IEqualityComparerGetHashCode.下面是一个说明这一点的人为例子.

说我有以下类型:

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,我想找到所有具有不同名称的元素.这是Distinct使用自定义相等比较器的完美用例.因此,让我们使用Comparer类从阿库的回答:

var comparer = new Comparer((x, y) => x.Name == y.Name);

现在,如果我们有一堆Value具有相同Name属性的元素,它们应该全部折叠成一个返回的值Distinct,对吧?让我们来看看...

var values = new List();

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 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(或等效的),以及在内部GroupBy使用类似的东西是有意义的Dictionary>.这可以解释为什么这些方法不起作用?我们试试这个:

var uniqueValues = new HashSet(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实现中包含一个适当的实例是如此重要.


原始答案

扩展orip的答案:

这里可以做一些改进.

    首先,我会采取一个Func而不是Func; 这将防止在实际keyExtractor本身中装入值类型键.

    其次,我实际上添加了一个where TKey : IEquatable约束; 这将阻止在Equals调用中装箱(object.Equals接受一个object参数;你需要一个IEquatable实现来获取TKey参数而不用装箱).显然,这可能会造成太严格的限制,因此您可以创建没有约束的基类和带有它的派生类.

以下是生成的代码的外观:

public class KeyEqualityComparer : IEqualityComparer
{
    protected readonly Func keyExtractor;

    public KeyEqualityComparer(Func 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 : KeyEqualityComparer
    where TKey : IEquatable
{
    public StrictKeyEqualityComparer(Func 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));
    }
}


@Bruno:我的打字很快.
@JustinMorgan:是的 - 在第一种情况下,由于`TKey`可能是任意类型,编译器将使用虚拟方法`Object.Equals`,这将需要装箱值类型参数,例如`int`.然而,在后一种情况下,由于`TKey`被约束为实现`IEquatable `,因此将使用不需要任何装箱的`TKey.Equals`方法.
非常有趣,谢谢你的信息.在看到这些答案之前,我不知道GetHashCode有这些LINQ含义.很高兴知道将来使用.

2> orip..:

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

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

用法:

var foo = new List { "abc", "de", "DE" };

// case-insensitive distinct
var distinct = foo.Distinct(new KeyEqualityComparer( x => x.ToLower() ) );

KeyEqualityComparer类:

public class KeyEqualityComparer : IEqualityComparer
{
    private readonly Func keyExtractor;

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


@Marcelo:没关系,你可以这样做; 但要注意,如果你要采用@ aku的方法,你真的应该**添加一个`Func `来提供一个`T`值的哈希码(正如我所建议的那样)例如,[鲁本的回答](http://stackoverflow.com/questions/98033/wrap-a-delegate-in-an-iequalitycomparer/3719617#3719617)).否则,你留下的`IEqualityComparer `实现是非常破碎的,*特别是*关于它在LINQ扩展方法中的用处.请参阅我的回答,讨论为什么会这样.
这比aku的回答好多了.

3> aku..:

我担心没有这样的包装盒开箱即用.然而,创建一个并不难:

class Comparer: IEqualityComparer
{
    private readonly Func _comparer;

    public Comparer(Func 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 f = (x, y) => x == y;
var comparer = new Comparer(f);
Console.WriteLine(comparer.Equals(1, 1));
Console.WriteLine(comparer.Equals(1, 2));


这段代码有一个严重的问题!很容易想出一个类,它有两个在这个比较器方面相同但具有不同哈希码的对象.
为了解决这个问题,该类需要另一个成员`private readonly Func _hashCodeResolver`,它也必须在构造函数中传递并在`GetHashCode(...)`方法中使用.
我很好奇:你为什么使用`obj.ToString().ToLower().GetHashCode()`而不是`obj.GetHashCode()`?
采用"IEqualityComparer "的框架中的地方总是在幕后使用散列(例如,LINQ的GroupBy,Distinct,Except,Join等),并且在此实现中打破了关于散列的MS合同.这里是MS的文档摘录:*"需要实现以确保如果Equals方法对于两个对象x和y返回true,则x的GetHashCode方法返回的值必须等于为y返回的值."*请参阅:http: //msdn.microsoft.com/en-us/library/ms132155

4> Ruben Bartel..:

通常情况下,我会通过在答案上评论@Sam来解决这个问题(我已经对原始帖子进行了一些编辑,以便在不改变行为的情况下对其进行清理.)

以下是我对@Sam 的答案的重复,对[IMNSHO]关键修复默认的散列策略: -

class FuncEqualityComparer : IEqualityComparer
{
    readonly Func _comparer;
    readonly Func _hash;

    public FuncEqualityComparer( Func comparer )
        : this( comparer, t => 0 ) // NB Cannot assume anything about how e.g., t.GetHashCode() interacts with the comparer's behavior
    {
    }

    public FuncEqualityComparer( Func comparer, Func 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 `都是直接断开的.
换句话说,由于您使用的是**自定义**比较器,因此它与**默认**比较器相关的对象的**默认**哈希码无关,因此您无法使用它.
@Ruben Bartelink:谢谢你的澄清.但是我仍然不明白你的散列策略是t => 0.如果所有对象总是散列到同一个东西(零),那么就不比使用obj.GetHashCode更糟糕了,每个@Dan Tao的观点?为什么不总是强制调用者提供良好的哈希函数?

5> ldp615..:

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

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

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

    提供静态包装器IEqualityComparer<>,它将有一个静态方法来创建比较器实例 - 简化调用.相比

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

    new EqualityComparer(p => p.ID);
    

    添加了指定IEqualityComparer<>密钥的重载.

班级:

public static class Equality
{
    public static IEqualityComparer CreateComparer(Func keySelector)
    {
        return CreateComparer(keySelector, null);
    }

    public static IEqualityComparer CreateComparer(Func keySelector, 
                                                         IEqualityComparer comparer)
    {
        return new KeyEqualityComparer(keySelector, comparer);
    }

    class KeyEqualityComparer : IEqualityComparer
    {
        readonly Func keySelector;
        readonly IEqualityComparer comparer;

        public KeyEqualityComparer(Func keySelector, 
                                   IEqualityComparer comparer)
        {
            if (keySelector == null)
                throw new ArgumentNullException("keySelector");

            this.keySelector = keySelector;
            this.comparer = comparer ?? EqualityComparer.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.CreateComparer(p => p.ID);
var comparer2 = Equality.CreateComparer(p => p.Name);
var comparer3 = Equality.CreateComparer(p => p.Birthday.Year);
var comparer4 = Equality.CreateComparer(p => p.Name, StringComparer.CurrentCultureIgnoreCase);

人是一个简单的类:

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


+1用于提供一个实现,使您可以为密钥提供一个比较器。除了提供更大的灵活性之外,这还避免了对比较和哈希进行装箱的值类型。
这是这里最充实的答案。我还添加了一个空检查。完成。

6> 小智..:
public class FuncEqualityComparer : IEqualityComparer
{
    readonly Func _comparer;
    readonly Func _hash;

    public FuncEqualityComparer( Func comparer )
        : this( comparer, t => t.GetHashCode())
    {
    }

    public FuncEqualityComparer( Func comparer, Func 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( this IEnumerable first, IEnumerable second, Func comparer )
    {
        return first.SequenceEqual( second, new FuncEqualityComparer( comparer ) );
    }

    public static bool SequenceEqual( this IEnumerable first, IEnumerable second, Func comparer, Func hash )
    {
        return first.SequenceEqual( second, new FuncEqualityComparer( comparer, hash ) );
    }
}


哇,山姆怎么了?!

7> Bruno..:

orip的答案很棒.

这里有一个小扩展方法,使它更容易:

public static IEnumerable Distinct(this IEnumerable list, Func    keyExtractor)
{
    return list.Distinct(new KeyEqualityComparer(keyExtractor));
}
var distinct = foo.Distinct(x => x.ToLower())

推荐阅读
乐韵答题
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有