简体   繁体   中英

Equals Override and LINQ GroupBy

I have two classes, one called ArcPrimitive and the other called CirclePrimitive.

public class ArcPrimitive : Primitive
    {
        public double Angle { get; set; }
        public double Length { get; set; }

        public override bool Equals(object obj)
        {
            if (obj is ArcPrimitive other)
            {
                return EqualsHelpers.EqualAngles(Angle, other.Angle) && EqualsHelpers.EqualLengths(Length, other.Length);
            }
            else if (obj is ArcPrimitiveType otherType)
            {
                return EqualsHelpers.EqualAngles(Angle, otherType.Angle) && EqualsHelpers.EqualLengths(Length, otherType.Length);
            }
            else
            {
                return false;
            }
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }

public class CirclePrimitive : Primitive
    {
        public double Radius { get; set; }

        public override bool Equals(object obj)
        {
            if (obj is CirclePrimitive other)
            {
                return EqualsHelpers.EqualLengths(Radius, other.Radius);
            }
            else if (obj is CirclePrimitiveType otherType)
            {
                return EqualsHelpers.EqualLengths(Radius, otherType.Radius);
            }
            else
            {
                return false;
            }
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }

Now, elsewhere in my project I have collections (List<>) of ArcPrimitive objects and CirclePrimitive objects. I would like to group together ArcPrimitive objects and CirclePrimitive objects that are considered equal when my override Equals methods are used for each class. I have attempted to do this using the GroupBy() extension method on my collections of ArcPrimitive and CirclePrimitive objects. However, the only way that I could get the GroupBy method to correctly group the objects is if I used the following overload and supplied a class that implemented the IEqualityComparer interface:

GroupBy<TSource,TKey>(IEnumerable<TSource>, Func<TSource,TKey>, IEqualityComparer<TKey>)

ArcPrimitiveEqualityComparer apec = new ArcPrimitiveEqualityComparer(arcPrimitives);
CirclePrimitiveEqualityComparer cpec = new CirclePrimitiveEqualityComparer(circlePrimitives);

var arcPrimitiveGroups = arcPrimitives.GroupBy(p => p, apec).ToList();
var circlePrimitiveGroups = circlePrimitives.GroupBy(p => p, cpec).ToList();

My issue is that the EqualityComparer classes that I had to write are incredibly not-DRY (Don't Repeat Yourself), making me wonder if there is a better way.

public class ArcPrimitiveEqualityComparer : IEqualityComparer<ArcPrimitive>
    {
        public Dictionary<ArcPrimitive, int> ArcHashDict { get; set; }

        public ArcPrimitiveEqualityComparer(List<ArcPrimitive> arcPrimitives)
        {
            ArcHashDict = new Dictionary<ArcPrimitive, int>();
            int hCode = 0;

            foreach (ArcPrimitive arcPrimitive in arcPrimitives)
            {
                var keys = ArcHashDict.Keys;
                bool matchFound = false;

                foreach (var key in keys)
                {
                    if (arcPrimitive.Equals(key))
                    {
                        matchFound = true;
                    }
                }

                if (matchFound == false)
                {
                    ArcHashDict.Add(arcPrimitive, hCode);
                    hCode += 1;
                }
            }
        }
        
        public bool Equals(ArcPrimitive ap1, ArcPrimitive ap2)
        {
            return ap1.Equals(ap2);
        }

        public int GetHashCode(ArcPrimitive ap)
        {
            foreach (var key in ArcHashDict.Keys)
            {
                if (ap.Equals(key))
                {
                    return ArcHashDict[key];
                }
            }

            throw new Exception("ArcPrimitive does not have a hash code.");
        }
    }

public class CirclePrimitiveEqualityComparer : IEqualityComparer<CirclePrimitive>
    {
        public Dictionary<CirclePrimitive, int> CircleHashDict { get; set; }

        public CirclePrimitiveEqualityComparer(List<CirclePrimitive> circlePrimitives)
        {
            CircleHashDict = new Dictionary<CirclePrimitive, int>();
            int hCode = 0;

            foreach (CirclePrimitive circlePrimitive in circlePrimitives)
            {
                var keys = CircleHashDict.Keys;
                bool matchFound = false;

                foreach (var key in keys)
                {
                    if (circlePrimitive.Equals(key))
                    {
                        matchFound = true;
                    }
                }

                if (matchFound == false)
                {
                    CircleHashDict.Add(circlePrimitive, hCode);
                    hCode += 1;
                }
            }
        }

        public bool Equals(CirclePrimitive cp1, CirclePrimitive cp2)
        {
            return cp1.Equals(cp2);
        }

        public int GetHashCode(CirclePrimitive cp)
        {
            foreach (var key in CircleHashDict.Keys)
            {
                if (cp.Equals(key))
                {
                    return CircleHashDict[key];
                }
            }

            throw new Exception("CirclePrimitive does not have a hash code.");
        }
    }

I have pursued a couple different ways of fixing this serious repetition. One was to create a generic EqualityComparer class. The issue I faced there was that I could not access the correct Equals override when the object type is not specified in the generic class. The other method I tried was to override not only Equals but GetHashCode in my ArcPrimitive and CirclePrimitive classes in the hopes that GroupBy would just use the override methods to do the grouping. However, I could not figure out how to correctly generate a hash code for these objects, because objects that return true from Equals have to have the same hash code, and I could not figure out how to apply my custom equality methods to the hash code function to get the necessary hash codes.

Sorry for the long post, I just felt it was necessary to add the code to give the details of my issue.

EDIT: response to NetMage comment This is a test to try to use IEquatable to control the equality comparison that GroupBy does for grouping and keys.

class Program
{
    static void Main(string[] args)
    {
        List<Test> testList = new List<Test>
        {
            new Test(1),
            new Test(1),
            new Test(2),
            new Test(3),
            new Test(3)
        };

        var result = testList.GroupBy(p => p);

        foreach (var group in result)
        {
            Console.WriteLine($"Key value: {group.Key.Value}; Group count {group.Count()}");
        }

        Console.ReadLine();
        
        // Output
        // Key value: 1; Group count: 1
        // Key value: 1; Group count: 1
        // Key value: 2; Group count: 1
        // Key value: 3; Group count: 1
        // Key value: 3; Group count: 1

    }
}

class Test : IEquatable<Test>
{
    public int Value { get; set; }

    public Test(int val)
    {
        Value = val;
    }

    public bool Equals(Test other)
    {
        return Value == other.Value;
    }
}

As you can see, each object becomes a key and none are grouped together, even though I wanted objects with the same Value property to be grouped together.

Properly working version of IEquatable<T> example:

void Main() {
    var testList = new[] { 1, 1, 2, 3, 3 }.Select(n => new Test(n)).ToList();

    var result = testList.GroupBy(p => p);

    foreach (var group in result)
        Console.WriteLine($"Key value: {group.Key.Value}; Group count {group.Count()}");

    // Output
    //Key value: 1; Group count 2
    //Key value: 2; Group count 1
    //Key value: 3; Group count 2
}

class Test : IEquatable<Test> {
    public int Value { get; set; }

    public Test(int val) => Value = val;

    public override bool Equals(object obj) => obj is Test otherTest && this.Equals(otherTest);
    public bool Equals(Test other) => other is not null && Value == other.Value;
    public override int GetHashCode() => Value;
    public static bool operator==(Test aTest, Test bTest) => aTest is not null && aTest.Equals(bTest);
    public static bool operator!=(Test aTest, Test bTest) => !(aTest == bTest);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM