Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Equality for .NET PropertyInfos

I have some code that compares 2 PropertyInfos with Equals(). While this normally seems to work, I have run into a strange situation where two reflected property info objects for the same underlying property are not equal:

PropertyInfo prop1, prop2; // both are public and not static
Console.WriteLine(prop1 == prop2); // false ???
Console.WriteLine(Equals(prop1, prop2)); // false ???
Console.WriteLine(prop1.DeclaringType == prop2.DeclaringType); // true
Console.WriteLine(prop1.ReturnType == prop2.ReturnType); // true
Console.WriteLine(prop1.Name == prop2.Name); // true
Console.WriteLine(prop1.DeclaringType.GetProperties().Contains(prop1)); // true
Console.WriteLine(prop2.DeclaringType.GetProperties().Contains(prop2)); // false ???

It looks like PropertyInfo does not actually implement Equals(), but I thought that .NET caches reflected members so that the same instance is always returned. You certainly see a.GetType() == b.GetType() all the time. Is this not the case for PropertyInfos?

Some other notes: -This weirdness happened when running an NUnit test in .NET 4, VS2012, x86 build target -This doesn't even happen for all properties we compare this way, but it fails consistently on one property.

Can anyone explain this behavior?

EDIT: in case anyone is interested, here is the EqualityComparison function I wrote to compare MemberInfos:

public class MemberEqualityComparer : EqualityComparer<MemberInfo> {
    public override bool Equals(MemberInfo @this, MemberInfo that) {
        if (@this == that) { return true; }
        if (@this == null || that == null) { return false; }

                        // handles everything except for generics
                    if (@this.MetadataToken != that.MetadataToken
                        || !Equals(@this.Module, that.Module)
                        || this.Equals(@this.DeclaringType, that.DeclaringType))
                    {
                        return false;
                    }

                    bool areEqual;
                    switch (@this.MemberType)
                    {
                        // constructors and methods can be generic independent of their types,
                        // so they are equal if they're generic arguments are equal
                        case MemberTypes.Constructor:
                        case MemberTypes.Method:
                            var thisMethod = @this as MethodBase;
                            var thatMethod = that as MethodBase;
                                                areEqual = thisMethod.GetGenericArguments().SequenceEqual(thatMethod.GetGenericArguments(), 
this);
                            break;
                        // properties, events, and fields cannot be generic independent of their types,
                        // so if we've reached this point without bailing out we just return true.
                        case MemberTypes.Property:
                        case MemberTypes.Event:
                        case MemberTypes.Field:
                            areEqual = true;
                            break;
                        // the system guarantees reference equality for types, so if we've reached this point
                        // without returning true the two are not equal
                        case MemberTypes.TypeInfo:
                        case MemberTypes.NestedType:
                            areEqual = false;
                            break;
                        default:
                            throw new NotImplementedException(@this.MemberType.ToString());
    }

    public override int GetHashCode(MemberInfo memberInfo) {
        if (memberInfo == null) { return 0; }

    var hash = @this.MetadataToken 
        ^ @this.Module.GetHashCode() 
        ^ this.GetHashCode(@this.DeclaringType);
    return hash;
    }
}
like image 437
ChaseMedallion Avatar asked Nov 28 '12 22:11

ChaseMedallion


2 Answers

I'm guessing they have a different ReflectedType. For example, inheritance:

class A {
   public int Foo {get;set;}
}
class B : A {}

now look at typeof(A).GetProperty("Foo") and typeof(B).GetProperty("Foo").

like image 98
Marc Gravell Avatar answered Oct 15 '22 20:10

Marc Gravell


Object identity is only promised for the Type class, not for the other reflection classes. A possibly sound way to compare for equality is to check that the properties have the same metadata token and came from the same module. So try this:

bool equal = prop1.MetadataToken == prop2.MetadataToken &&
             prop1.Module.Equals(prop2.Module);

Which makes sense as long as ecma 335 applies. I could not test this against your code since you didn't post it. So just try it.

like image 20
Hans Passant Avatar answered Oct 15 '22 22:10

Hans Passant