views:

7506

answers:

6

This is what I've come up with as a method on a class inherited by many of my other classes. The idea is that it allows the simple comparison between properties of Objects of the same Type.

Now, this does work - but in the interest of improving the quality of my code I thought I'd throw it out for scrutiny. How can it be better/more efficient/etc.?

/// <summary>
/// Compare property values (as strings)
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public bool PropertiesEqual(object comparisonObject)
{

    Type sourceType = this.GetType();
    Type destinationType = comparisonObject.GetType();

    if (sourceType == destinationType)
    {
        PropertyInfo[] sourceProperties = sourceType.GetProperties();
        foreach (PropertyInfo pi in sourceProperties)
        {
            if ((sourceType.GetProperty(pi.Name).GetValue(this, null) == null && destinationType.GetProperty(pi.Name).GetValue(comparisonObject, null) == null))
            {
                // if both are null, don't try to compare  (throws exception)
            }
            else if (!(sourceType.GetProperty(pi.Name).GetValue(this, null).ToString() == destinationType.GetProperty(pi.Name).GetValue(comparisonObject, null).ToString()))
            {
                // only need one property to be different to fail Equals.
                return false;
            }
        }
    }
    else
    {
        throw new ArgumentException("Comparison object must be of the same type.","comparisonObject");
    }

    return true;
}
+1  A: 

make sure objects aren't null.

having obj1 and obj2:

 if( obj1 == null )
      {
         return false;
      }

 return obj1.Equals( obj2 );
Ric Tokyo
what if they're both null? aren't they then equal?
mmr
good point on nulls, in my case using .Equals() doesn't seem to work, which is why i've come up with this solution
nailitdown
Why does it not work ?
Gishu
well, the case i'm testing for is two objects, one newly created, one from the session. comparing the two with .Equals() returns false even though both have identical property values
nailitdown
+2  A: 

Do you override .ToString() on all of your objects that are in the properties? Otherwise, that second comparison could come back with null.

Also, in that second comparison, I'm on the fence about the construct of !( A == B) compared to (A != B), in terms of readability six months/two years from now. The line itself is pretty wide, which is ok if you've got a wide monitor, but might not print out very well. (nitpick)

Are all of your objects always using properties such that this code will work? Could there be some internal, non-propertied data that could be different from one object to another, but all exposed data is the same? I'm thinking of some data which could change over time, like two random number generators that happen to hit the same number at one point, but are going to produce two different sequences of information, or just any data that doesn't get exposed through the property interface.

mmr
good points - != ... agreed, point taken.ToString() was an attempt to workaround .GetValue returning an object (thus the comparison always false, as it's a ref compare).. is there a better way?
nailitdown
If GetValue is returning an object, can you recurse through this function again? ie, call PropertiesEqual on the returned objects?
mmr
+1  A: 

If you are only comparing objects of the same type or further down the inheritance chain, why not specify the parameter as your base type, rather than object ?

Also do null checks on the parameter as well.

Furthermore I'd make use of 'var' just to make the code more readable (if its c#3 code)

Also, if the object has reference types as properties then you are just calling ToString() on them which doesn't really compare values. If ToString isn't overwridden then its just going to return the type name as a string which could return false-positives.

DarkwingDuck
good point on the reference types - in my case it doesn't matter but there's a good chance it would.
nailitdown
+1  A: 

I think it would be best to follow the pattern for Override Object#Equals()
For a better description: Read Bill Wagner's Effective C# - Item 9 I think

public override Equals(object obOther)
{
  if (null == obOther)
    return false;
  if (object.ReferenceEquals(this, obOther)
    return true;
  if (this.GetType() != obOther.GetType())
    return false;
  # private method to compare members.
  return CompareMembers(this, obOther as ThisClass);
}
  • Also in methods that check for equality, you should return either true or false. either they are equal or they are not.. instead of throwing an exception, return false.
  • I'd consider overriding Object#Equals.
  • Even though you must have considered this, using Reflection to compare properties is supposedly slow (I dont have numbers to back this up). This is the default behavior for valueType#Equals in C# and it is recommended that you override Equals for value types and do a member wise compare for performance. (Earlier I speed-read this as you have a collection of custom Property objects... my bad.)
Gishu
I ran into problems with overriding .Equals() because i'm trying to implement this on a base class that gets inherited... because I don't know the keys for the class this'll be run against, i can't implement a decent override for GetHasCode() (req'd when you override Equals()).
nailitdown
The requirement is that if objA.Equals(objB) then objA.GetHashCode() == objB.GetHashCode(). GetHashCode should not be dependent on mutable state/data of a class... I didnt get what you meant by keys for the class.. Seems like something that can be solved. Doesn't the base type have the 'keys'?
Gishu
+1  A: 

The first thing I would suggest would be to split up the actual comparison so that it's a bit more readable (I've also taken out the ToString() - is that needed?):

else {
    object originalProperty = sourceType.GetProperty(pi.Name).GetValue(this, null);
    object comparisonProperty = destinationType.GetProperty(pi.Name).GetValue(comparisonObject, null);

    if (originalProperty != comparisonProperty)
     return false;

The next suggestion would be to minimise the use of reflection as much as possible - it's really slow. I mean, really slow. If you are going to do this, I would suggest caching the property references. I'm not intimately familiar with the Reflection API, so if this is a bit off, just adjust to make it compile:

// elsewhere
Dictionary<object, Property[]> lookupDictionary = new Dictionary<object, Property[]>;

Property[] objectProperties = null;
if (lookupDictionary.ContainsKey(sourceType)) {
  objectProperties = lookupProperties[sourceType];
} else {
  // build array of Property references
  PropertyInfo[] sourcePropertyInfos = sourceType.GetProperties();
  Property[] sourceProperties = new Property[sourcePropertyInfos.length];
  for (int i=0; i < sourcePropertyInfos.length; i++) {
    sourceProperties[i] = sourceType.GetProperty(pi.Name);
  }
  // add to cache
  objectProperties = sourceProperties;
  lookupDictionary[object] = sourceProperties;
}

// loop through and compare against the instances

However, I have to say that I agree with the other posters. This smells lazy and inefficient. You should be implementing IComparable instead :-).

Travis
I was just looking at IComparable but it seemed like it was for sorting and ordering.. is it really useful for comparing the equality of two objects?
nailitdown
Absolutely, because .Equals(object o) is defined as this.CompareTo(o) == 0. So, equals uses ComparesTo() to determine equality. This will be much more efficient (and standard practice) than using reflection.
Travis
I may be mistaken assuming that Equals is implemented (or should be implemented) with reference to CompareTo(). You should consider overriding Equals as described here: http://stackoverflow.com/questions/104158/what-is-best-practice-for-comparing-two-instances-of-a-reference-type
Travis
+6  A: 

I was looking for a snippet of code that would do something similar to help with writing unit test. Here is what I ended up using.

  public static bool PublicInstancePropertiesEqual<T>(T self, T to, params string[] ignore) where T : class 
  {
     if (self != null && to != null)
     {
        Type type = typeof(T);
        List<string> ignoreList = new List<string>(ignore);
        foreach (System.Reflection.PropertyInfo pi in type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
        {
           if (!ignoreList.Contains(pi.Name))
           {
              object selfValue = type.GetProperty(pi.Name).GetValue(self, null);
              object toValue = type.GetProperty(pi.Name).GetValue(to, null);

              if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue)))
              {
                 return false;
              }
           }
        }
        return true;
     }
     return self == to;
  }
Big T