4

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.

Enyra
  • 17,542
  • 12
  • 35
  • 44

3 Answers3

5

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: Can I specify my explicit type comparator inline?

Or you could adapt this to Except: Distinct list of objects based on an arbitrary key in LINQ

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();
        }
    }
}
Community
  • 1
  • 1
Sam Saffron
  • 128,308
  • 78
  • 326
  • 506
  • 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 Aug 24 '09 at 09:36
  • I still have this issue and doesnt work. I ensured to have the two lists as either .ToList() or .ToArray() , before invoking the Enumerable.Except(list1, list2, new Customcomparer()). but it doent even wink to hit my debugger and does the default list comparison (which considers everything on list1 is different and yields all list1 results. Why? What am i missing here? – Ak777 Jul 12 '21 at 22:56
  • I might have overlooked the other answer for a different Hashcode() to do when writing our own comparer, but i was using default GetHashcode() and after i changed that, my debugger started working to hit my own comparer. – Ak777 Jul 12 '21 at 23:34
3

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).

Community
  • 1
  • 1
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 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 Aug 24 '09 at 08:43
  • My guess, tripped up by deferred execution. – Sam Saffron Aug 24 '09 at 09:07
  • 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 Aug 24 '09 at 09:29
2

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
  • 687,336
  • 108
  • 737
  • 1,005
  • Yes you are right, but that does not solve the problem yet, because whether GetHashCode nor Equals get called. – Enyra Aug 24 '09 at 08:40
  • to what its worth, this should answer should also be given equal high votes. This is the solution worked for me. I might have overlooked the other answer for a different Hashcode() to do when writing our own comparer, but i was using default GetHashcode() and after i changed that, my debugger started working to hit my own comparer. – Ak777 Jul 12 '21 at 23:33