public class Contact { public Guid Id { get; private set; } public string Name { get; set; } public Contact() { Id = Guid.NewGuid(); } } [TestMethod] public void Test() { Contact[] expected = new[] { new Contact { Name = "Bob", }, new Contact { Name = "Abel", }, }; Contact[] actual = new[] { new Contact { Name = "Abel", }, new Contact { Name = "Charlie", }, }; // TODO: test for equivalence? }
I often run into situations like these when testing functions on collections, as presented above. I also see similar situations when comparing collections from a client (that may have modified or net-new entities) to collections from a datastore.
There are many ways to resolve these sorts of scenarios, like in-lining comparison logic, overriding Equals method, or even declaring a custom equivalence class that implements IEqualityComparer<Contact>. The first method is brittle and completely not portable. Overriding Equals is a little more portable, but still fairly brittle - for instance, if in one context I require equality based on Name, in another context equality of Id, and yet another based on both. The third method is portable and is externalized from the class so that it may be clearly swapped depending on context (consider ContactNameEqualityComparer versus ContactIdEqualityComparer).
My only gripe with this last method is that it is always so tedious to implement the boiler-plate comparisons in every instance of a comparer (for instance, writing trivial accept and reject criteria for ContactNameEqualityComparer, and then re-writing that criteria again for ContactIdEqualityComparer, and then again for any other comparers that exist in our system for other types).
FuncComparer
Really I want something that encapsulates basic comparison functionality while being able to supply comparison criteria as lambdas.
public class FuncComparer<T> : IComparer, IComparer<T> { private readonly Comparison<T>[] comparisons = null; private readonly int xIsNotNullComparison = 0; private readonly int yIsNotNullComparison = 0; public FuncComparer( Comparison<T> comparison, bool isNullLessThan = true) : this (new[] { comparison, }, isNullLessThan) { } public FuncComparer( Comparison<T>[] comparisons, bool isNullLessThan = true) { this.comparisons = comparisons; this.xIsNotNullComparison = isNullLessThan ? 1 : -1; this.yIsNotNullComparison = isNullLessThan ? -1 : 1; } // interfaces #region IComparer Members public int Compare(object x, object y) { int comparison = 0; if (!ReferenceEquals(x, y)) { bool xIsNull = true; bool yIsNull = true; T a = default(T); T b = default(T); if (x is T) { xIsNull = false; a = (T)(x); } if (y is T) { yIsNull = false; b = (T)(y); } comparison = Compare(a, xIsNull, b, yIsNull); } return comparison; } #endregion #region IComparer<T> Members public int Compare(T x, T y) { int comparison = 0; if (!ReferenceEquals(x, y)) { comparison = Compare(x, x == null, y, y == null); } return comparison; } #endregion // private methods /// <summary> /// Compare two strong-typed values. Explicit 'is null' /// parameters are required for scenarios where /// <typeparamref name="T"/> is a valuetype and type-less /// compare is used. In such cases, type-less method uses /// default-type value which will telescope nulls to default /// values. For example, when comparing a series of integers, /// any nulls in our inputs will be interpreted as '0'. /// </summary> /// <param name="x"></param> /// <param name="isXNull"></param> /// <param name="y"></param> /// <param name="isYNull"></param> /// <returns></returns> private int Compare(T x, bool isXNull, T y, bool isYNull) { int comparison = 0; if (!isXNull && !isYNull) { for ( int i = 0; i < comparisons.Length && comparison == 0; i++) { comparison = comparisons[i](x, y); } } else if (!isXNull) { comparison = xIsNotNullComparison; } else if (!isYNull) { comparison = yIsNotNullComparison; } // both x and y are null, implies equality, no-op //else //{ //} return comparison; } }
What we may now do is
public class Contact { public Guid Id { get; private set; } public string Name { get; set; } public Contact() { Id = Guid.NewGuid(); } } [TestMethod] public void Test() { FuncComparer<Contact> contactNameComparer = new FuncComparer<Contact>( (a,b) => string.Compare( a.Name, b.Name, StringComparison.InvariantCultureIgnoreCase)); Contact[] expected = new[] { new Contact { Name = "Bob", }, new Contact { Name = "Abel", }, }; Contact[] actual = new[] { new Contact { Name = "Abel", }, new Contact { Name = "Charlie", }, }; // NOTE: successfully demonstrates ease-of-use, esp // when tests are repeated with minor variances in // expectation CollectionAssert.AreNotEqual( expected, actual, contactNameComparer); CollectionAssert.AreEqual( new[] { new Contact { Name = "Abel", }, new Contact { Name = "Charlie", }, }, actual, contactNameComparer); }
Why 'isXNull' and 'isYNull'?
The oddest thing about FuncComparer is how it handles null checks. We perform null checks on the way in, before the private core-implementation executes. This is because the private core-implementation is strongly typed, co-ercing any null inputs into base-type defaults. For reference types, such as any class, this is ok, since default(T) is still null. However, for valuetypes like int or struct, we end up telescoping nulls into default values, which is a loss-full transformation, a reduction in fidelity.
We overcome this through explicit null tests on the way in, and pass that on as state to our core-implementation.
[TestMethod] public void Test() { IComparer typelessComparer = new FuncComparer<int>( (a,b) => a.CompareTo(b), true); // we expect 'null is less-than', so x as null should // return less-than int expectedComparison = -1; int actualComparison = typelessComparer.Compare(null, 0); Assert.AreEqual(expectedComparison, actualComparison); }
One last minor detail, is the ability to configure an ordinal interpretation of null in relation to all other values. Our constructor accepts a boolean that indicates any null values should be considered less-than non-null values, or vice versa. This allows us to tweak things like list ordering.
[TestMethod] public void Test() { IComparer typelessComparer = new FuncComparer<int>( (a,b) => a.CompareTo(b), false); object[] expected = new object[] { 0, 1, 2, 3, 4, null, }; ArrayList actual = new ArrayList { 1, null, 4, 2, 3, 0, }; actual.Sort(typelessComparer); CollectionAssert.AreEqual(expected, actual); }