views:

287

answers:

3

Hello folk,

I try to use the except method with a custom equality comparer, but it is not work.

My equality comparer:

public class BusinessObjectGuidEqualityComparer<T> : IEqualityComparer<T> where T : BusinessObject
{
    #region IEqualityComparer<T> Members

    /// <summary>
    /// Determines whether the specified objects are equal.
    /// </summary>
    /// <param name="x">The first object of type <paramref name="T"/> to compare.</param>
    /// <param name="y">The second object of type <paramref name="T"/> to compare.</param>
    /// <returns>
    /// <see langword="true"/> If the specified objects are equal; otherwise, <see langword="false"/>.
    /// </returns>
    public bool Equals(T x, T y)
    {
        return (x == null && y == null) || (x != null && y != null && x.Guid.Equals(y.Guid)); 
    }

    /// <summary>
    /// Returns a hash code for this instance.
    /// </summary>
    /// <param name="obj">The object to get the hash code.</param>
    /// <returns>
    /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 
    /// </returns>
    /// <exception cref="T:System.ArgumentNullException">
    /// The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null.
    /// </exception>
    public int GetHashCode(T obj)
    {
        if (obj == null)
        {
            throw new ArgumentNullException("obj");
        }

        return obj.GetHashCode();
    }

    #endregion
}

My except usage:

BusinessObjectGuidEqualityComparer<Area> comparer = new BusinessObjectGuidEqualityComparer<Area>();
IEnumerable<Area> toRemove = this.Areas.Except(allocatedAreas, comparer);
IEnumerable<Area> toAdd = allocatedAreas.Except(this.Areas, comparer);

The strange thing is, event I provide my custom equality comparer the default one is used, so what do I make wrong?

Thanks for help.

+1  A: 

The methods in your equality comparer doesn't match up. You are comparing the GUID of the objects, but the GetHashCode method uses the default implementation which is based on the reference, not the GUID. As different instances will get different hash codes eventhough they have the same GUID, the Equals method will never be used.

Get the hash code for the GUID in the GetHashCode method, as that is what you are comparing:

public int GetHashCode(T obj) {
    if (obj == null) {
        throw new ArgumentNullException("obj");
    }
    return obj.Guid.GetHashCode();
}
Guffa
Yes you are right, but that does not solve the problem yet, because whether GetHashCode nor Equals get called.
Enyra
+2  A: 

Try:

public int GetHashCode(T obj) {
    return obj == null ? 0 : obj.Guid.GetHashCode();
}

Your hash-code must match equality (or at least, not contradict it); and your equality says "nulls are equal, otherwise compare the guid". Internally, I expect Except uses a HashSet<T>, which explains why getting GetHashCode right is so important.


Here's my test rig (using the above GetHashCode) which works fine:

public abstract class BusinessObject {
    public Guid Guid { get; set; }
}
class Area : BusinessObject {
    public string Name { get; set; }
    static void Main() {
        Guid guid = Guid.NewGuid();
        List<Area> areas = new List<Area> {
            new Area { Name = "a", Guid = Guid.NewGuid() },
            new Area { Name = "b", Guid = guid },
            new Area { Name = "c", Guid = Guid.NewGuid() },
        };
        List<Area> allocatedAreas = new List<Area> {
            new Area { Name = "b", Guid = guid},
            new Area { Name = "d", Guid = Guid.NewGuid()},
        };
        BusinessObjectGuidEqualityComparer<Area> comparer =
             new BusinessObjectGuidEqualityComparer<Area>();
        IEnumerable<Area> toRemove = areas.Except(allocatedAreas, comparer);
        foreach (var row in toRemove) {
            Console.WriteLine(row.Name); // shows a & c, since b is allocated
        }
    }
}

If your version doesn't work, you're going to have to post something about how you're using it, as it works fine for me (above).

Marc Gravell
In the IEqualityComparer documentation is defined, that GetHashCode throws this exception. So I assume I implemented it corrent. But anyway it doe snot solve my problem, the IEqualityComparer members are not called at all.
Enyra
My guess, tripped up by deferred execution.
Sam Saffron
Could be, could be ;-p
Marc Gravell
Your test rig looks fine. If I go thru this with the debugger, toRemove will have a, b and c. And I found out, that the IEqualityComparer members are called when I iterate thru the items, so this deferred execution could be right.
Enyra
+2  A: 

Similar to Marc I just tested this, everything is being called just fine, my guess is that you are caught by the LINQ deferred execution, notice the ToArray in my code.

Note, when tracing through this I noticed GetHashCode is never called on null objects in the comparer.

Keep in mind, MiscUtil has an awesome way for you to do this stuff inline, see: http://stackoverflow.com/questions/188120/can-i-specify-my-explicit-type-comparator-inline/188130#188130

Or you could adapt this to Except: http://stackoverflow.com/questions/742682/distinct-list-of-objects-based-on-an-arbitrary-key-in-linq/742784#742784

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1 {

    public class BusinessObject {
        public Guid Guid { get; set; }
    }

    public class BusinessObjectGuidEqualityComparer<T> : IEqualityComparer<T> where T : BusinessObject {
        #region IEqualityComparer<T> Members

        public bool Equals(T x, T y) {
            return (x == null && y == null) || (x != null && y != null && x.Guid.Equals(y.Guid));
        }

        /// </exception>
        public int GetHashCode(T obj) {
            if (obj == null) {
                throw new ArgumentNullException("obj");
            }

            return obj.GetHashCode();
        }

        #endregion
    }

    class Program {
        static void Main(string[] args) {

            var comparer = new BusinessObjectGuidEqualityComparer<BusinessObject>();

            List<BusinessObject> list1 = new List<BusinessObject>() {
                new BusinessObject() {Guid = Guid.NewGuid()},
                new BusinessObject() {Guid = Guid.NewGuid()}
            };

            List<BusinessObject> list2 = new List<BusinessObject>() {
                new BusinessObject() {Guid = Guid.NewGuid()},
                new BusinessObject() {Guid = Guid.NewGuid()},
                null,
                null,
                list1[0]
            };

            var toRemove = list1.Except(list2, comparer).ToArray();
            var toAdd = list2.Except(list1, comparer).ToArray();

            // toRemove.Length == 1
            // toAdd.Length == 2
            Console.ReadKey();
        }
    }
}
Sam Saffron
Thats it! I recognized, that the IEqualityComparer members are called when I iterate thru it, so I tried toRemove.ToList().ForEach(...); and this works.
Enyra