Do i use IEquatable Equals() on struct comparisons?

Viewed 72

I am making a simple struct for complex numbers and i want to check they are equal by comparing the components.

Currently i have:

public struct Complex
{

    public float Re;
    public float Img;

    public Complex(float real, float imaginary)
    {
        Re = real;
        Img = imaginary;
    }
    public static bool operator ==(Complex a, Complex b)
    {
        return Mathf.Approximately(a.Re - b.Re, 0) && Mathf.Approximately(a.Img - b.Img, 0);
    }
    public static bool operator !=(Complex a, Complex b) => !(a == b);
}

But i've seen other code where people use IEquatable with the Equals function. Am i suppose to use that here or is my code the correct way to do it? I am a bit unsure when to use the interface and when not to at the moment.

2 Answers

This is how to properly implement IEquatable<> and IFormattable for structures:

The following is based on the following answers

Code

public struct Complex : IEquatable<Complex>, IFormattable
{
    public Complex(float real, float imaginary)
    {
        Real=real;
        Imaginary=imaginary;
    }

    public float Real { get; }
    public float Imaginary { get; }

    #region IEquatable Members

    /// <summary>
    /// Equality overrides from <see cref="System.Object"/>
    /// </summary>
    /// <param name="obj">The object to compare this with</param>
    /// <returns>False if object is a different type, otherwise it calls <code>Equals(Complex)</code></returns>
    public override bool Equals(object obj)
    {
        if (obj is Complex item)
        {
            return Equals(item);
        }
        return false;
    }

    /// <summary>
    /// Checks for equality among <see cref="Complex"/> classes
    /// </summary>
    /// <returns>True if equal</returns>
    public bool Equals(Complex other)
    {
        return Real.Equals(other.Real)
            && Imaginary.Equals(other.Imaginary);
    }
    /// <summary>
    /// Calculates the hash code for the <see cref="Complex"/>
    /// </summary>
    /// <returns>The int hash value</returns>
    public override int GetHashCode()
    {
        unchecked
        {
            int hc = -1817952719;
            hc = (-1521134295)*hc + Real.GetHashCode();
            hc = (-1521134295)*hc + Imaginary.GetHashCode();
            return hc;
        }
    }
    public static bool operator ==(Complex target, Complex other) { return target.Equals(other); }
    public static bool operator !=(Complex target, Complex other) { return !target.Equals(other); }

    #endregion

    #region IFormattable Members
    public override string ToString() => ToString("g");
    public string ToString(string formatting) => ToString(formatting, CultureInfo.CurrentCulture.NumberFormat);
    public string ToString(string formatting, IFormatProvider provider)
    {
        if (Imaginary!=0)
        {
            return Imaginary>0
                ? $"{Real.ToString(formatting, provider)}+{Imaginary.ToString(formatting, provider)}i"
                : $"{Real.ToString(formatting, provider)}-{(-Imaginary).ToString(formatting, provider)}i";
        }
        else
        {
            return Real.ToString(formatting, provider);
        }
    }
    #endregion
}

This ensures that == means exact equality. If you want to implement approximately equals, then add a method .ApproxEquals(Complex other, float delta) or similar to implement that functionality.

The minimum delta is 2^(-22) = 0.00000023842 for a value of 1.0

Notice also that .GetHashCode() isn't symmetric, in terms if you exchange the values if Real and Imaginary it will return a different result. This is a key feature that a lot of people mess up by just returning Real.GetHashCode() ^ Imaginary.GetHashCode() which makes it Symmetric since the XOR operator ^ is commutative.

Finally, the conversion to string is handled by .ToString() with various overloads as needed. Best practice is to reference .NumberFormat for the default format provider.

You are better off using IEquatable for performance reasons. That's because the operator requires boxing and unboxing of the struct, whereas the IEquatable Equals doesn't - it's just the function cost itself. For any maths involving lots of numbers and comparisons, you should definitely go with the IEquatable option.

Related