views:

178

answers:

5

I have an immutable Vector3 structure, and I'm wondering how to best implement the .Equals() method so that it's useful and still satisfies the Guidelines for Overloading Equals().

Here's a partial implementation of my structure:

public struct Vector3
{
    private float _x;
    private float _y;
    private float _z;

    public float X { get { return _x; }} 
    public float Y { get { return _y; }}
    public float Z { get { return _z; } }

    public Vector3(float x, float y, float z)
    {
        _x = x;
        _y = y;
        _z = z;
    }

    public override bool Equals(object obj)
    {
        //What should go here?
    }
}

Edit: I don't want to directly compare each X, Y, Z due to the nature of floating point operations. For example, I can tell if two vector3s are parallel by checking to see if u x v == <0, 0, 0>; however with floating point operations this will often fail because one of the zeros is actually <8.205348E-09>.

I'm looking for an Equals() method that is a bit smarter. I want it to work whether the numbers are very large or very small. I

+1  A: 

What is wrong with the following?

protected const float EqualityVariance = 0.0000001;

public override bool Equals(object obj)
{
    Vector3? vector = obj as Vector3?;
    if (vector == null) return false;

    return Equals((Vector3)vector);
}

public bool Equals(Vector3 vector)
{
    return Math.Abs(this._x - vector._x) < EqualityVariance && 
           Math.Abs(this._y - vector._y) < EqualityVariance && 
           Math.Abs(this._z - vector._z) < EqualityVariance;
}
Matthew Scharley
u should also override ==, !=, Equals(Vector) and gethashcode
Simon
The problem with this is that doing == on floating point often gives false negatives (with respect to the problem at hand). You might have two numbers that are supposed to be 1.0, and one might be 0.99999999672434. Hence, one might do, instead of a == b, abs(a-b)<0.00000001 . (I don't know what C#'s absolute value function for floats is; that's just a guess.)
Joey Adams
Of course, but all those would be based on this anyway.
Matthew Scharley
@Joey: That's an issue of the implementation of equality of floats. It depends vastly on usage whether you want to do that or not. With no additional information about usage, I would still recommend my approach.
Matthew Scharley
You can't do obj as Vector3; as this statement only works with reference type or nullable typeuse (Vector3)obj; instead.
Alastair Pitts
@AP: good point. Fixed in a way that doesn't generate exceptions.
Matthew Scharley
Consider that vectors have coordinates (0.0000000000000001, 0.0000000000002, 0.0) and (0.0, 0.00000000000002, 0.0000000000001). Are they equal/parallel/whatever? You have to take exponents as I posted in pseudocode below.
doc
No, you can't do that as equality must be transitive. I.e. if a ==b and b ==c then a == c.
Joe
@Joe, then use a floor/ceil approach rather than rounding. You are of course correct, but in 99% of cases, this is correct.
Matthew Scharley
@Matthew, I don't understand what you mean by a floor/ceil approach or how it would help. Nor your assertion about 99% of cases.
Joe
Instead of rounding up or down, always round up or always round down. This means you have discrete ranges that always end up equaling the same thing, rather than a sliding scale. This fixes the transitivity of the equality. For instance, if you had a variance of 0.1 in the original code, then `0.01 = 0.08` and `0.08 = 0.14`, but `0.01 != 0.14`. If you instead floor all values to a certain decimal place, you get `0.00 = 0.00` and `0.00 != 0.10` using the same values, and transitivity is always preserved.
Matthew Scharley
+1  A: 

In NGenerics http://code.google.com/p/ngenerics/ we have a base class VectorBase

From VectorBase

 public override bool Equals(object obj)
 {
  if (obj == null)
  {
   return false;
  }

  var vector = obj as IVector<T>;
  return (EqualsInternal(vector));
 }

 public bool Equals(IVector<T> other)
 {
  return other != null && EqualsInternal(other);
 }


 private bool EqualsInternal(IVector<T> other)
 {

  if (dimensionCount != other.DimensionCount)
  {
   return false;
  }
  for (var i = 0; i < dimensionCount; i++)
  {
   if (!Equals(this[i], other[i]))
   {
    return false;
   }
  }
  return true;
 }

 public static bool operator ==(VectorBase<T> left, IVector<T> right)
 {
  // If both are null, or both are same instance, return true.
  if (ReferenceEquals(left, right))
  {
   return true;
  }

  // If one is null, but not both, return <c>false</c>.
  if (((object)left == null) || (right == null))
  {
   return false;
  }

  // Return true if the fields match:
  return left.EqualsInternal(right);
 }

 public static bool operator !=(VectorBase<T> left, IVector<T> right)
 {
  return !(left == right);
 }


 /// <inheritdoc />
 public override int GetHashCode()
 {
  var hashCode = 0;
  for (var index = 0; index < dimensionCount; index++)
  {
   hashCode ^= this[index].GetHashCode();
  }
  return hashCode;
 }

Then we have a Vector3D which inherits from VectorBase<

class Vector3D : VectorBase<double>

The full list of vectors is

* Vector2D - 2 dimensional vector.
* Vector3D - 3 dimensional vector.
* VectorN - A vector with user-defined dimensions.

http://code.google.com/p/ngenerics/wiki/Vectors

Simon
+1  A: 

Unless there is some minimum-precision between vectors that is implied with the Vector3, I would make Equals be a simple FP equality check.

In addition I would create an AlmostEquals method (possibly even an overloaded Equals which is free from the contract of Equals). I would not put "fuzzy logic" into Equals because Equals as a strict contract with GetHashCode.

On the other hand, if a minimum-precision requirement is set, that could be used and would be just a normal equality check of the normalization. The reason to do it after normalization would be so that GetHashCode could also used the normalized values. (Just delta-checks across the values would not work for GetHashCode). I'm not a fan of this approach though.

Of course the approach used should take into account how the Vector3 is to be used and what role it has. Perhaps there is a need for a more restricted type?

pst
Mono's Xna implementation has a strict equals check on its Vector3 class: http://code.google.com/p/monoxna/source/browse/trunk/src/Microsoft.Xna.Framework/Vector3.cs
Luke Quinane
A: 

"Would normalizing both vectors and then comparing each float within a small epsilon value work well?"

Yes it should work quite well. However there is better (more efficient) way to do this. Extract exponents of compared numbers, then compare subtracted value to eps rised to the max exponents power.

Pseudo code would look like this:

exp1 = exponentOf(r1)
exp2 = exponentOf(r2)
if absolute_value(r2 - r1) <= epsilon^(max(exp1, exp2)) 
    return equal 
else
    return not_equal
doc
+1  A: 

You shouldn't implement Equals in Vector3 by using distance checks. First of all this will break "if (x.Equals(y) && y.Equals(z)) returns true, then x.Equals(z) returns true." contract. Do you need an example for this?.
Second, why do you need to overload Equals method? Who will be the client of that API? Do you want to compare these objects by yourself or you want to use some libraries like collections or maps with instances of Vector3 as values and/or keys? In later case this can be very dangerous! If you want to use such vectors as keys in hash maps when you will also need to define trivial GetHashCode method which will return constant. So the best comparison will be "this._x == vector._x && this._y == vector._y && this._z == vector._z" in that case.
If you want to have such comparison for yourself then it will be more clear if you will define some other method like "IsClose" with explanation of comparison idea in method summary.

Dmitriy Matveev