Comparing Objects
C# has a number of useful comparison interfaces that we can use and implement, so this would seem to be a redundant post, wouldn’t it. We can use IComparable, IComparable
However, in the context of something like a Unit Test, if we want to compare/assert that two objects are equal, we would ideally be able to output what is different between them- if we have two objects with many properties and merely compare them, then the developer will haveto debug to figure out which properties actually were different between them, whereas we pretty much have access to that information in the Unit Test. At the same time, we don’t want to have to write sophisticated comparison routines for every object type. Instead, it might be reasonable to try a more generic approach. If we want to compare two instances of objects we could merely compare their public, readable properties. While this won’t get everything, it means we can store the actual differences which can be expressed as part of debugging output or the output of a unit test.
An Array of issues
The major issue I encountered was handling of Arrays. I previously wrote about the task of serializing Arrays. The tricky part of dealing with Arrays is the same here, which is how we manage the Rank. another issue in this situation is how do we compare Arrays in a meaningful way if they have different ranks or dimensions? I cannot really think of a good way to do so so What I’ve done is merely ignore when the objects passed in are arrays. This means that, technically, the solution is wrong (since the arrays between two instances of an object may differ) but since I’m actually going to be using this code in a Unit Test consideration and would rather not spend dozens of hours merely working on a way to compare objects I think it will work to get started.
Effectively, we can use reflection to grab each property, then compare the values of the two properties in the two instances being compared. If they are different we can add the property to a list of Strings to return, indicating the properties that differ; otherwise, we don’t. Once returned, we can use another helper function to use the list to construct a more useful set of the differences between the two elements.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
public class ComparisonHelper { /// <summary> /// Accepts a result List of differences from the CompareProperties routine, and creates a nicely (hopefully) formatted listing of the values of the properties that differ between the two objects., /// </summary> /// <typeparam name="T">Type of the objects which were compared.</typeparam> /// <param name="CompareResult">Return value from CompareProperties.</param> /// <param name="FirstObject">First Object used to Compare. Should be the object that was passed in to CompareProperties to build the CompareResult parameter, but it's a free country.</param> /// <param name="SecondObject">Second Object used to Compare. Should be the object that was passed in to Compare Properties to build the CompareResult parameter.</param> /// <returns>Formatted String for display in the test results (or log, or whatever) indicating what property or properties were different.</returns> /// <remarks>The First and Second object can probably be transposed from the call made to CompareProperties, though there isn't much reason to do so.</remarks> public static String ListDifferences<T>(List<String> CompareResult, T FirstObject, T SecondObject) { StringBuilder sb = new StringBuilder(); Type GenType = typeof(T); foreach (String PropertyDiff in CompareResult) { PropertyInfo grabproperty = GenType.GetProperty(PropertyDiff); //retrieve the property value from both objects. Object FirstValue = grabproperty.GetValue(FirstObject); Object SecondValue = grabproperty.GetValue(SecondObject); //now, list this as a difference. We return a Built string intended to output to the Test Console to describe why a failure has occured from a mismatch. sb.Append(grabproperty.Name + " Values:\nFirst:" + FirstValue.ToString() + "\nSecond:" + SecondValue.ToString()); } return sb.ToString(); } /// <summary> /// Accepts the a result list of differences from the CompareFixedArray routine, and constructs a more "friendly" text description of the elements that were different. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="CompareResult"></param> /// <param name="FirstArray"></param> /// <param name="SecondArray"></param> /// <returns></returns> public static String ListDifferences<T>(List<int> CompareResult, Array FirstArray, Array SecondArray) { StringBuilder sb = new StringBuilder(); //first step: if the arrays are different sizes than indicate that. if (FirstArray.Length != SecondArray.Length) return "Arrays are different sizes (" + FirstArray.Length + " Versus " + SecondArray.Length + ")"; else { //same size- OK to proceed. Fairly straightforward algorithm- merely iterate on CompareResult and list the index values. foreach (int IndexDiff in CompareResult) { sb.AppendLine("Index:" + IndexDiff + ":\t" + FirstArray.GetValue(IndexDiff) + "\t" + SecondArray.GetValue(IndexDiff)); } } return sb.ToString(); } //compares the two items and returns a list of properties that are different. The List will be empty if the two instances are equal. public static List<String> CompareProperties<T>(T FirstItem, T SecondItem, IEnumerable<String> IgnorePropertyNames = null) { if (IgnorePropertyNames == null) IgnorePropertyNames = Enumerable.Empty<String>(); if (typeof (T).IsArray) return new List<String>();//presume arrays are identical.... HashSet<String> IgnoreProps = new HashSet<string>(); foreach (String iterateignore in IgnorePropertyNames) { if (!IgnoreProps.Contains(iterateignore)) IgnoreProps.Add(iterateignore); } List<String> CompareResults = new List<string>(); Type elementType = typeof(T); var checkproperties = elementType.GetProperties(); foreach (PropertyInfo lookproperty in checkproperties) { try { //don't compare these properties if the property name is in our ignore list. if (!IgnoreProps.Contains(lookproperty.Name)) { Object firstValue = lookproperty.GetValue(FirstItem, new Object[] { }); Object secondValue = lookproperty.GetValue(SecondItem, new Object[] { }); bool different = false; if (firstValue is IComparable<T>) { different = ((IComparable<T>) firstValue).CompareTo((T) secondValue) != 0; } else if (firstValue is IComparable) { different = ((IComparable) firstValue).CompareTo(secondValue) != 0; } else { different = !firstValue.Equals(secondValue); } if (different) { CompareResults.Add(lookproperty.Name); } } } catch (Exception exx) { //likely a property with indexers, or maybe write-only or somesuch, so we'll ignore it. } } return CompareResults; } public static bool CompareObjects<T>(T FirstItem, T SecondItem) { return CompareProperties(FirstItem, SecondItem, null).Count == 0; //return if no different properties. } /// <summary> /// compares two FixedArrays. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="FirstArray">First Array</param> /// <param name="SecondArray">Second Array</param> /// <returns>A int list indicating indices that are different. If the two arrays are different, then the result will be all the indices of the larger FixedArray that are larger than the size of the Smaller FixedArray.</returns> public static List<int> CompareArray<T>(Array FirstArray, Array SecondArray, Comparison<T> CompareMethod = null) { if (CompareMethod == null) { //if it is null set it to a reasonable default. CompareMethod = (a, b) => { var comparable = a as IComparable<T>; if (comparable != null) { return comparable.CompareTo(b); } var compnongen = a as IComparable; if (compnongen != null) { return compnongen.CompareTo(b); } return 0; //not sure the best way to attack when they aren't comparable at all... }; } //if the arrays are different lengths, then return an array that consists of all the indices beyond the smaller FixedArray. if (FirstArray.Length != SecondArray.Length) { return Enumerable.Range(Math.Min(FirstArray.Length, SecondArray.Length), Math.Max(FirstArray.Length, SecondArray.Length)).ToList(); } else { //same length, so compare each element using the compare method we were given. List<int> Buildresult = new List<int>(); for (int i = 0; i < FirstArray.Length; i++) { if (CompareMethod((T)FirstArray.GetValue(i), (T)SecondArray.GetValue(i)) != 0) { Buildresult.Add(i); } } return Buildresult; } } } |
With this, we can now use a test program to show off the results:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
class Program { static void Main(string[] args) { TestCompareClass c = new TestCompareClass(500,"Element",new MinorElement(45)); TestCompareClass b = new TestCompareClass(500, "Element1", new MinorElement(45)); TestCompareClass a = new TestCompareClass(500, "Element", new MinorElement(30)); var resultentryab = ComparisonHelper.CompareProperties(a, b); if (resultentryab.Count > 0) { Console.WriteLine("a and b are different:\n" + ComparisonHelper.ListDifferences(resultentryab,a,b)); } var resultentryac = ComparisonHelper.CompareProperties(a, c); if (resultentryac.Count > 0) { Console.WriteLine("a and c are different:\n" + ComparisonHelper.ListDifferences(resultentryac, a, c)); } var resultentrybc = ComparisonHelper.CompareProperties(b, c); if (resultentrybc.Count > 0) { Console.WriteLine("a and c are different:\n" + ComparisonHelper.ListDifferences(resultentrybc, b, c)); } Console.ReadKey(); } } public class TestCompareClass { public int IntegerValue { get; set; } public String StringValue { get; set; } public MinorElement Minor { get; set; } public TestCompareClass(int IntValue, String pStringValue, MinorElement pMinorItem) { IntegerValue = IntValue; StringValue = pStringValue; Minor = pMinorItem; } } public class MinorElement :IComparable<MinorElement> { public int AnotherEntry { get; set; } public int CompareTo(MinorElement other) { return this.AnotherEntry.CompareTo(other.AnotherEntry); } public MinorElement(int Entry) { AnotherEntry = Entry; } public override String ToString() { return "MinorElement:" + AnotherEntry; } } |
Which outputs the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
a and b are different: StringValue Values: First:Element Second:Element1Minor Values: First:MinorElement:30 Second:MinorElement:45 a and c are different: Minor Values: First:MinorElement:30 Second:MinorElement:45 a and c are different: StringValue Values: First:Element1 Second:ElementMinor Values: First:MinorElement:45 Second:MinorElement:45 |
Have something to say about this post? Comment!