在.NET中比较GUID时出现意外行为

4
我已经尝试创建一个类似下面这样的扩展方法...
public static IEnumerable<T> Distinct<T>(this IEnumerable<T> value, IEnumerable<T> compareTo, Func<T, object> compareFieldPredicate)
{
    return value.Where(o => !compareTo.Exists(p => compareFieldPredicate.Invoke(p) == compareFieldPredicate.Invoke(o)));
}

这个想法是我可以做像这样的事情...
IEnumerable<MyCollection> distinctValues = MyCollection.Distinct(MyOtherCollection, o => o.ID); //Note that o.ID is a guid

现在我本来期望只返回我不同的项,但是我发现情况并非如此。
进一步研究后,使用以下代码来分解这种方法。
Guid guid1 = Guid.NewGuid();
Guid guid2 = new Guid(guid1.ToString());

Func<MyObject, object> myFunction = o => o.ID;
Func<MyObject, object> myFunction1 = o => o.ID;

bool result = myFunction(MyObject) == myFunction1(MyObject);
//result = false

我发现即使Guid相同,比较结果仍将返回false。
这是什么原因?

一个自定义类,包含一个类型为GUID的ID字段。如果不清楚,请见谅。 - Maxim Gershkovich
也许我漏掉了什么,但如果它是一个类型,为什么要将其传递到 myFunctionmyFunction1 中呢? - reuben
5个回答

7

您遇到的问题是在比较之前将Guids装箱为对象。请考虑以下代码:

Guid g1 = Guid.NewGuid();
var g2 = g1;

Console.WriteLine(g1 == g2);

object o1 = g1;
object o2 = g2;

Console.WriteLine(o1 == o2);

实际输出的结果为:

true
false

由于"o1"和"o2"虽然等于相同的Guid,但它们不是同一个对象。

如果你真的想让你的“Distinct”扩展方法不与特定类型(如Guid)绑定,可以这样做:

public static IEnumerable<TItem> Distinct<TItem, TProp>(this IEnumerable<TItem> value, IEnumerable<TItem> compareTo, Func<TItem, TProp> compareFieldPredicate)
    where TProp : IEquatable<TProp>
{
    return value.Where(o => !compareTo.Any(p => compareFieldPredicate(p).Equals(compareFieldPredicate(o))));
} 

非常感谢您简明扼要的回答。我猜想可能是这样,但并没有完全理解。顺便说一下,Equatable泛型约束真的很不错。 - Maxim Gershkovich

2
bool result = (guid1==guid2); //result -> true

您可以尝试将myfunction和myfunction1中的返回类型Object更改为GUID。
Func<MyObject, Guid> myFunction = o => o.ID;
Func<MyObject, Guid> myFunction1 = o => o.ID;

否则,返回值(true)将被封装为Object,并进行引用相等性检查,结果为false。

1

改用

Func<MyObject, Guid> myFunction = o => o.ID;
Func<MyObject, Guid> myFunction1 = o => o.ID;

这是因为您的函数被定义为

Func<MyObject, object>
myFunctionmyFunction1返回的Guid将被封装在两个不同的对象中。有关.NET中拆箱和装箱功能,请参见此处
因此,在比较时,将比较两个不同的对象。
对象中Equals的默认实现正在进行引用相等性检查。它未检查封装的值。有关如何实现object.Equals的详细信息,请参见此处

这就是我要表达的答案,但由于两个返回的对象是同一引用,所以我不清楚为什么它会返回false。然而,将它们更改为Guid无论如何都可以解决问题。 - Michael
@Michael,myFunctionmyFunction1返回的Guid将被装箱到两个不同的对象中。请参阅此链接http://msdn.microsoft.com/en-us/library/yz2be5wk.aspx。 - Harvey Kwok

1

正如其他人所说,你的compareFieldPredicate返回一个object,它的运算符==使用object.ReferenceEquals而不是object.Equals,因此你的代码总是检查对象的身份而不是相等性。

解决这个问题的方法之一是使用object.Equals方法而不是运算符==

public static IEnumerable<T> Distinct<T>(
    this IEnumerable<T> value, 
    IEnumerable<T> compareTo, 
    Func<T, object> compareFieldPredicate
)
{
    return value.Where(o => !compareTo.Exists(
        p => object.Equals(compareFieldPredicate(p), compareFieldPredicate(o))
    ));
}

更好的解决方案是对于实际键类型使用默认比较器,如果该类型为自身实现了IEquatable接口,则消除装箱:
public static IEnumerable<T> Distinct<T, TKey>(
    this IEnumerable<T> value, 
    IEnumerable<T> compareTo, 
    Func<T, TKey> compareFieldPredicate
)
{
    return value.Where(o => !compareTo.Exists(
        p => EqualityComparer<TKey>.Default.Equals(compareFieldPredicate(p), compareFieldPredicate(o))
    ));
}

然而,您的Distinct方法的大部分功能已经由Enumerable.Except LINQ方法实现。

您可以通过提供IEqualityComparer的实现来将您的实现重写为Enumerable.Except的术语。

private class KeyEqualityComparer<T, TKey> : IEqualityComparer<T>
{
    private readonly Func<T, TKey> _keySelector;

    public KeyEqualityComparer(Func<T, TKey> keySelector)
    { _keySelector = keySelector; }

    public int GetHashCode(T item)
    { return _keySelector(item).GetHashCode(); }

    public bool Equals(T x, T y)
    { return EqualityComparer<TKey>.Default.Equals(_keySelector(x), _keySelector(y)); }
}

public static IEnumerable<T> ExceptBy<T, TKey>(
    this IEnumerable<T> first, 
    IEnumerable<T> second, 
    Func<T, TKey> keySelector
)
{
    return first.Except(second, new KeyEqualityComparer<T, TKey>(keySelector));
}

0

如果你将lambda改为返回Guid,那么它就可以工作了:

Func<MyObject, Guid> myFunction = o => o.ID;
Func<MyObject, Guid> myFunction1 = o => o.ID;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接