2

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.

WDUK
  • 1,412
  • 1
  • 12
  • 29
  • 1
    Always override `Equals` & `GetHashCode` and make your `struct` read-only before implementing `==` and `!=`. – Enigmativity Jun 26 '20 at 05:43
  • [**Mutable structs are evil**](https://stackoverflow.com/q/441309/380384). Do not allow `Re` and `Img` to change, by adding the `readonly` keyword in front of them. Mutable structs can lead to a lot of bugs. Consider the whole `Complex` structure _as a single value_, by not allowing parts of that value to change at runtime. – John Alexiou Jun 26 '20 at 12:06

2 Answers2

1

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.

John Alexiou
  • 28,472
  • 11
  • 77
  • 133
  • How come you do `Real.Equals(other.Real)` is that how float numbers are suppose to be compared? I've always assumed you had to subtract to check if its near epsilon... I don't fully understand why it has to be done this way..And that get hash code i have no idea what that is about.. – WDUK Jun 26 '20 at 05:42
  • There is an inherent assumption that for `Equals()` to return true, the two values must match bit by bit. Otherwise you are changing the meaning of `.Equals()`. This might mean that `b+(a-b) == a` returns `false` based on round-off error. – John Alexiou Jun 26 '20 at 05:46
  • Oh so the equals function will do a bit comparison not a float comparison ? – WDUK Jun 26 '20 at 06:16
  • @WDUK - what's the difference? If the bit representation is equal then the floats are equal. Internally `Single.Equals(float obj)` checks if the bit representation is equal. – John Alexiou Jun 26 '20 at 12:03
  • How come `float1==float2` is generally not a good idea then, normally people compare the difference to epsilon ? Why doesn't C# do a bit comparison as default so we don't have to worry about precision issues. – WDUK Jun 26 '20 at 19:43
  • It is not the job of the language to change the meaning of `==` for floats. I understand that practically close values _could_ be the same, but that is a programmer decision on how close is close for each situation. As far as the language is concerned `Math.Sin(Math.PI) == 0.0` returns `false` due to round-off errors, even though mathematically `sin(π)=0`. – John Alexiou Jun 26 '20 at 20:44
  • But what happens if i did `float zero = 0.0f; bool b = zero.Equals(Math.Sin(Math.PI))` which is kinda what i am trying to say about using .Equals() on floats in general due to precision issues... how come it works with my complex struct as you suggested but generally you wouldn't normally use Equals for float comparisons in general code? – WDUK Jun 26 '20 at 22:37
-1

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.

see sharper
  • 11,505
  • 8
  • 46
  • 65
  • “That's because the operator requires boxing and unboxing of the struct”? I don't get what this sentence means... – Alexei Levenkov Jun 26 '20 at 05:18
  • 1
    Why would my current function lead to boxing if the function takes the same type? Bit confused by that. – WDUK Jun 26 '20 at 05:21