What is the best practice to implement equality for readonly structs?

Viewed 835

I've just started to program in C# last year and I'm still learning the language. I have a question regarding readonly struct type and equality comparison methods.

When creating a struct in C#, I know it's usually considered to be a best practice to implement IEquatable as the default reflection-based comparison is very slow. I also learned that in C# 7.2 and later we can define readonly structs and for these types, we can also use in parameter to avoid unnecessary copying.

As structs are usually defined as immutable readonly types, I suppose it's not unusual to define Equals methods for read only structs.

Given the above facts, however, what I'm wondering is if there is a good effective way to implement equality comparison methods for them. My point is that none of these equality methods and operators actually need to modify parameters so I want to utilize in parameters somehow to save unnecessary copying.

Below is my attempt at doing this:

public readonly struct Point : IEquatable<Point>
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    // Explicitly implementing IEquatable<Point> and delegating to an Equals method taking in param.
    bool IEquatable<Point>.Equals(Point other) => Equals(other);

    public bool Equals(in Point other) => X == other.X && Y == other.Y;

    public override bool Equals(object? obj) => obj is Point other && Equals(other);

    public static bool operator ==(in Point left, in Point right) => left.Equals(right);

    public static bool operator !=(in Point left, in Point right) => !left.Equals(right);

    public override int GetHashCode() => HashCode.Combine(X, Y);

    public override string ToString() => $"Point({X}, {Y})";
}

The above certainly works but I think it's not perfect solution as copying is still needed if it's called through the IEquatable interface. Note that I can't just implement IEquatable implicitly with in param as Equal method taking in modifier is considered to have different signature and treated as an overload.

Is there a known best practice to implement this correctly? What I would really like to know is if there is a known best practices and patterns to effectively implement equality for such readonly structs. Especially I'm interested in a way to properly utilize in parameter modifier for implementing Equality comparison methods.

So far I haven't found any satisfactory answer on the Web, and I also checked some source codes of the core library. For example, System.DateTime is now defined as readonly struct and it's rather big, but in parameter isn't utilized t here. (Existing type might well need to keep compatibility, I know they often need to have compromise, though.)

Note that the Point struct defined above is small composing only two 32 bit slot, so copying might not actually be a big problem here, but it's just meant for a simple illustrative example.

Update for .NET6(C# 10)

Now that C#10 has officially been released, the original question has become almost obsolete. We can now create such a read only struct as an readonly record struct. Of course depending of your model, it can also defined as a normal refence type record (class).

https://devblogs.microsoft.com/dotnet/welcome-to-csharp-10/

The point is that optimized equality comparison methods and operators are automatically generated for record types.

    public readonly record struct Person
    {
        public string FirstName { get; init; }
        public string LastName { get; init; }
    }
1 Answers

The above is fine, however: note that the advantages of in on read-only value-types only really kicks in for types that are non-trivial in size; in the case of two integers, you're probably over-thinking things.

You can't remove the need to use pass-by-value on the IEquatable<T> scenario, since that is how the API is defined.

It may also be worth noting that the in usage may make it hard to consume this API from languages other than C#; VB has poor support for this, as an example. Whether this is important depends on your target audience.

Related