Как мне реализовать .Equals() в моей структуре Vector3?

У меня есть неизменяемая структура Vector3, и мне интересно, как лучше всего реализовать метод .Equals(), чтобы он был полезен и по-прежнему удовлетворял Рекомендации по перегрузке Equals().

Вот частичная реализация моей структуры:

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?
    }
}

Изменить: я не хочу напрямую сравнивать каждый X, Y, Z из-за характера операций с плавающей запятой. Например, я могу определить, параллельны ли два вектора vector3, проверив, что u x v == ‹0, 0, 0>; однако с операциями с плавающей запятой это часто не удается, потому что один из нулей на самом деле равен ‹8.205348E-09>.

Я ищу метод Equals(), который немного умнее. Я хочу, чтобы он работал независимо от того, очень большие числа или очень маленькие. я


person Jeff    schedule 09.11.2009    source источник
comment
Что не так с проверкой всех трех элементов на равенство? Есть ли причина сделать его более сложным? (Предполагая, что нет необходимости тестировать VectorN) OTOH, возможно, также существует потребность в почти равном, учитывая, что равенство FP относительной точности может быть ... вводящим в заблуждение. Поскольку у Equals есть контракт с GetHashCode, я бы не рекомендовал использовать логику почти Equals в Equals.   -  person    schedule 09.11.2009


Ответы (5)


В NGenerics http://code.google.com/p/ngenerics/ у нас есть базовый класс 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;
    }

Затем у нас есть Vector3D, который наследуется от VectorBase‹.

class Vector3D : VectorBase<double>

Полный список векторов

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

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

person Simon    schedule 09.11.2009

Если не существует какой-либо минимальной точности между векторами, которая подразумевается с помощью Vector3, я бы сделал Equals простой проверкой равенства FP.

Кроме того, я бы создал метод «Почти равные» (возможно, даже перегруженный метод «Равные», свободный от контракта «Равные»). Я бы не стал вкладывать в Equals "нечеткую логику", потому что Equals является строгим контрактом с GetHashCode.

С другой стороны, если установлено требование минимальной точности, это можно было бы использовать, и это была бы обычная проверка нормализации на равенство. Причина, по которой это нужно сделать после нормализации, заключается в том, что GetHashCode также может использовать нормализованные значения. (Простая дельта-проверка значений не будет работать для GetHashCode). Хотя я не сторонник такого подхода.

Конечно, используемый подход должен учитывать как использовать Vector3 и какую роль он играет. Возможно, есть потребность в более ограниченном типе?

person Community    schedule 09.11.2009
comment
Реализация Xna в Mono имеет строгую проверку на равенство в своем классе Vector3: code.google.com/p/monoxna/source/browse/trunk/src/ - person Luke Quinane; 09.11.2009

Что не так со следующим?

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;
}
person Matthew Scharley    schedule 09.11.2009
comment
вы также должны переопределить ==, !=, Equals (Vector) и gethashcode - person Simon; 09.11.2009
comment
Проблема в том, что выполнение == с плавающей запятой часто дает ложные отрицательные значения (по отношению к рассматриваемой проблеме). У вас может быть два числа, которые должны быть равны 1,0, и одно может быть 0,99999999672434. Следовательно, вместо a == b можно было бы сделать abs(a-b)‹0,00000001 . (Я не знаю, что такое функция абсолютного значения C# для чисел с плавающей запятой; это просто предположение.) - person Joey Adams; 09.11.2009
comment
@Joey: Это проблема реализации равенства поплавков. Это во многом зависит от использования, хотите ли вы это делать или нет. Без дополнительной информации об использовании я бы по-прежнему рекомендовал свой подход. - person Matthew Scharley; 09.11.2009
comment
Вы не можете делать obj как Vector3; поскольку этот оператор работает только со ссылочным типом или типом, допускающим значение NULL, используйте (Vector3)obj; вместо. - person Alastair Pitts; 09.11.2009
comment
@AP: хорошая мысль. Исправлено таким образом, что не генерирует исключений. - person Matthew Scharley; 09.11.2009
comment
Учтите, что векторы имеют координаты (0.0000000000000001, 0.00000000000002, 0.0) и (0.0, 0.00000000000002, 0.00000000000001). Они равны/параллельны/что угодно? Вы должны взять показатели, как я разместил в псевдокоде ниже. - person doc; 09.11.2009
comment
Нет, вы не можете этого сделать, поскольку равенство должно быть транзитивным. т.е. если a ==b и b ==c, то a == c. - person Joe; 09.11.2009
comment
@Joe, тогда используйте подход пола / потолка, а не округление. Вы конечно правы, но в 99% случаев это правильно. - person Matthew Scharley; 09.11.2009
comment
@ Мэтью, я не понимаю, что вы подразумеваете под подходом пола / потолка или как это поможет. Ни твоего утверждения про 99% случаев. - person Joe; 10.11.2009
comment
Вместо округления в большую или меньшую сторону всегда округляйте в большую или меньшую сторону. Это означает, что у вас есть дискретные диапазоны, которые всегда равны одному и тому же, а не скользящая шкала. Это фиксирует транзитивность равенства. Например, если у вас была дисперсия 0,1 в исходном коде, то 0.01 = 0.08 и 0.08 = 0.14, но 0.01 != 0.14. Если вместо этого вы сведете все значения к определенному десятичному разряду, вы получите 0.00 = 0.00 и 0.00 != 0.10, используя одни и те же значения, и транзитивность всегда сохраняется. - person Matthew Scharley; 11.11.2009

Вы не должны реализовывать Equals в Vector3, используя проверки расстояния. Во-первых, это сломает "если (x.Equals(y) && y.Equals(z)) возвращает true, то x.Equals(z) возвращает true". договор. Вам нужен пример для этого?.
Во-вторых, зачем вам перегружать метод Equals? Кто будет клиентом этого API? Хотите сравнить эти объекты самостоятельно или хотите использовать некоторые библиотеки, такие как коллекции или карты, с экземплярами Vector3 в качестве значений и/или ключей? В дальнейшем это может быть очень опасно! Если вы хотите использовать такие векторы в качестве ключей в хэш-картах, вам также потребуется определить тривиальный метод GetHashCode, который будет возвращать константу. Таким образом, лучшим сравнением будет "this._x == vector._x && this._y == vector._y && this._z == vector._z" в этом случае.
Если вы хотите иметь такое сравнение для себя тогда будет понятнее, если вы определите какой-то другой метод, например "IsClose", с пояснением идеи сравнения в описании метода.

person okutane    schedule 09.11.2009

«Будет ли нормализация обоих векторов, а затем сравнение каждого с плавающей запятой в пределах небольшого значения эпсилон?»

Да вполне должно работать. Однако есть лучший (более эффективный) способ сделать это. Извлеките показатели степени сравниваемых чисел, затем сравните вычитаемое значение с eps, увеличенным до максимальной степени степени.

Псевдокод будет выглядеть так:

exp1 = exponentOf(r1)
exp2 = exponentOf(r2)
if absolute_value(r2 - r1) <= epsilon^(max(exp1, exp2)) 
    return equal 
else
    return not_equal
person doc    schedule 09.11.2009