Auto-implement Equals and GetHashCode with PostSharp Community

by Petr Hudeček on 05 Jun 2020

Recently we released PostSharp.Community.StructuralEquality. If you add the NuGet package to your project and then add the new attribute [StructuralEquality] to your classes, a field-by-field implementation of Equals and GetHashCode will be added to your classes automatically during compilation.

Fody has had this feature in its Equals.Fody add-in for many years and we’re to a large context copying their code and approach, with only some changes that I describe below.

How to use it

After you add the PostSharp.Community.StructuralEquality NuGet package, you can add the [StructuralEquality] attribute to your code like this:

[StructuralEquality]
class Dog
{
    public string Name { get; set; }
    public int Age { get; set; }

    public static bool operator ==(Dog left, Dog right) => Operator.Weave(left, right);
    public static bool operator !=(Dog left, Dog right) => Operator.Weave(left, right);
}

Then you can write code as though the Dog class had Equals, GetHashCode and equality operators implemented. That’s because at build time, after your code compiles, PostSharp adds synthesized overrides of Equals and GetHashCode to the compiled IL code and it replaces the method bodies of equality operators.

The result will be the same as if you used the following code:

class Dog : IEquatable<Dog>
{
    public string Name { get; set; }
    public int Age { get; set; }
    public static bool operator ==(Dog left, Dog right) => object.Equals(left,right);
    public static bool operator !=(Dog left, Dog right) => !object.Equals(left,right);
    
    public override bool Equals(Dog other)
    {
        bool result = false;
        if (!object.ReferenceEquals(null, other))
        {
            if (object.ReferenceEquals(this, other))
            {
                result = true;
            }
            else if (object.Equals(Name, other.Name) && Age == other.Age)
            {
                result = true;
            }
        }
        return result;
    }
    
    public override bool Equals(object other)
    {
        bool result = false;
        if (!object.ReferenceEquals(null, other))
        {
            if (object.ReferenceEquals(this, other))
            {
                result = true;
            }
            else if (this.GetType() == other.GetType())
            {
                result = Equals((Dog)other);
            }
        }
        return result;
    }
    
    public override int GetHashCode()
    {
        int num = Name?.GetHashCode() ?? 0;
        return (num * 397) ^ Age;
    }
}

 

This is the basic no-configuration example. There are ways to customize the synthesized code, for example by excluding some fields or properties. You can see the advanced case in the GitHub repository readme for details.

Field-by-field comparison

The Equals method in the synthesized code compares the fields of each type, not the properties. Backing fields of auto-implemented properties are still compared.

We feel that both field-by-field and property-by-property comparison are reasonable choices for an Equals implementation, but that fields represent the state of the object better. Fields contain the actual data to be compared, whereas properties could do redundant calculations: you could have several properties with different types or values all based on a single field. It would be wasteful to compare them all.

That’s why we decided to implement field-by-field comparison.

Equality operators

You may have noticed that to have StructuralEquality implement equality operators, you still needed to add the following code to your class:

public static bool operator ==(Dog left, Dog right) => Operator.Weave(left, right);
public static bool operator !=(Dog left, Dog right) => Operator.Weave(left, right);

Obviously, it would be nicer if PostSharp could do without even this code and do it all behind the scenes. However, that is not possible:

Suppose you create a class:

class A { } 

And then, in the same project, use == on it:

bool Test(A one, A two) {
  return one == two;
}

Here’s the IL code that gets emitted:

IL_0001: ldarg.1      // one
IL_0002: ldarg.2      // two
IL_0003: ceq

The instruction ceq means “compare the two operands” and, for reference types, it does reference comparison.

But if you replace the Test method with:

bool Test(A one, A two) {
  return object.ReferenceEquals(one, two);
}

Then the IL code stays the same! PostSharp and Fody run only after compilation ends and cannot access the original source code. We could replace all ceq, beq and bne instructions with calls to op_Equality (which is what’s emitted if the operators actually are in the original source code), but we wouldn’t be able to differentiate between whether “==” or “object.ReferenceEquals” was used.

More discussion on this topic can be found at the Equals.Fody GitHub issue #10.

What about generating Equals into source code?

Tools like ReSharper (and Rider) allow you to generate Equals, GetHashCode and equality members into your source code based on selected fields and properties of the class. 

Compared with Fody and StructuralEquality, generating code the way ReSharper does it keeps your build process cleaner and hides less code from you, but it comes with two downsides:

Conclusion

With this StructuralEquality package, you can have Equals and GetHashCode methods implemented very quickly. The synthesized code takes into account all fields in the class so it’s updated automatically when you add a new field or auto-property. Finally, the actual code that you see in your IDE remains clear and simple.

For more information on StructuralEquality including more options, see the GitHub readme file.

Comments

newsletter

Subscribe to our
newsletter

Stay up to date on all the latest releases, news and discounts.
Follow us on social media: