Внедрение географического координатного класса: сравнение равенства

Я интегрирую географический координатный класс из CodePlex в свою личную библиотеку инструментов. Этот класс использует поля float для хранения широты и долготы.

Так как класс GeoCoordinate реализует IEquatable<GeoCoordinate>, я обычно написал метод Equals следующим образом:

public bool Equals(GeoCoordinate other)
{
    if (other == null) {
        return false;
    }

    return this.latitude == other.latitude && this.longitude == other.longitude;
}

В этот момент я остановился и подумал, что я сравниваю переменные с плавающей запятой для равенства, что обычно не-нет. Мой мыслительный процесс затем пошел примерно следующим образом:

  • Я могу только представить настройки свойств Latitude и Longitude один раз, что означает, что не будет ошибок, накопленных, чтобы испортить мои сравнения.

  • С другой стороны, возможно (хотя и бессмысленно) писать

    var geo1 = new GeoCoordinate(1.2, 1.2);
    var geo2 = new GeoCoordinate(1.2, 1.2);
    
    // geo1.Equals(geo2) will definitely be true, BUT:
    
    geo2.Latitude *= 10;
    geo2.Latitude /= 10;
    
    // I would think that now all bets are off
    

    Конечно, это не то, что я могу себе представить, но если открытый интерфейс класса позволяет это, то Equals должен иметь возможность обрабатывать его.

  • Сравнение для равенства с использованием теста difference < epsilon позволило бы решить проблему сравнения двух экземпляров, но создать больше проблем:

    • Как сделать переходным? Это звучит невозможно.
    • Как создать тот же хэш-код для всех значений, которые сравнивались бы равными?

      Скажем, что epsilon = 0.11 (случайный пример). Из этого следует, что GeoCoordinate { 1, 1 } нужен тот же хэш-код, что и GeoCoordinate { 1.1, 1.1 }. Но последнему нужен тот же хэш-код, что и GeoCoordinate { 1.2, 1.2 }. Вы можете видеть, где это происходит: все экземпляры должны иметь один и тот же хэш-код.

  • Решение всего этого заключалось бы в том, чтобы сделать GeoCoordinate неизменяемым классом. Это также решило бы проблему GetHashCode: она основана на широте и долготе (что еще), и если они являются изменяемыми, то использование GeoCoordinate в качестве ключа в словаре требует проблем. Однако для того, чтобы класс неизменяемый имел свои недостатки:

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

Какой подход вы бы предложили? Легко сделать класс соответствующим требованиям, которые я имею прямо сейчас (просто сделайте его неизменным), но есть ли лучший способ?

Изменить. Я добавил элемент 3 в списке выше, переместив предыдущий элемент 3 в позицию 4.

Решение

Я собираюсь дать еще немного времени для обратной связи, но в настоящее время я иду с неизменным подходом. struct (потому что это то, что сейчас) с соответствующими членами, можно увидеть здесь; комментарии и предложения более чем приветствуются.

Ответ 1

"Место" с долготой/широтой меня довольно хорошо вписывается в слот "неизменяемого значения". Сама позиция не меняется - если вы измените широту, которая является другой позицией. Оттуда это может быть struct; для float в любом случае struct будет иметь тот же размер, что и x64-ссылка, поэтому нет реальной нижней стороны.

Re равенство; если позиция не совсем то же самое, она не "равна", по крайней мере, в "ключевой" перспективе, поэтому я был бы счастлив с == здесь. Вы можете добавить метод "находится внутри (x)", если это поможет. Разумеется, геометрия большой дуги также не является абсолютно свободной: p

Мысли:

  • он должен переопределить bool Equals(object), а также добавить bool Equals(GeoCoordinate)
  • он должен переопределить GetHashCode() и реализовать IEquatable<GeoCoordinate>
  • статические операторы являются приятными для использования дополнительными

Ответ 2

  • Equals в основном используется в словарях, поэтому руководство, которое вы должны сравнивать поплавками только с epsilon, здесь не применяется. Не пытайтесь вставить epsilon logic в Equals. Это задача пользователей делать это по мере необходимости. Относительно легко доказать, что единственная хеш-функция, согласующаяся с epsilon, сравнивает ее с постоянной хэш-функцией.

  • У вашей реализации есть одна проблема: она не обрабатывает NaN s. Вы должны использовать Equals вместо == для отдельных координат для вашего метода Equals.

    public bool Equals(GeoCoordinate other)
    {
        if (other == null) {
            return false;
        }
    
        return this.latitude.Equals( other.latitude) && this.longitude.Equals(other.longitude);
    }
    
  • "Не сравнивать с помощью ==, но с использованием epsilon" для кода потребления, а не для кода реализации. Поэтому я бы выполнил функцию, которая возвращает расстояние между двумя гео-координатами и сообщит пользователю использовать это для его сравнений с epsilon.

  • Я определенно сделаю его неизменным (не уверен, что структура или класс). Он имеет семантику значений и, следовательно, должен быть неизменным.
    Обычно я использую что-то вроде Linq-to-Xml/Linq-to-Json для сериализации, поскольку это позволяет мне преобразовать представление между моей моделью в памяти и моделью на диске.
    Но вы правы, что многие сериализаторы не поддерживают конструкторы по умолчанию. Я считаю это большой ошибкой в ​​этих сериализаторах, а не как недостаток в моей модели. Некоторые сериализаторы просто получают доступ к частным сеттерам/полям, но лично я думаю, что воняет.

Ответ 3

Я бы использовал длинные целые числа вместо чисел с плавающей запятой в базовой модели широты и долготы. Миллисекунда градуса в любом случае меньше двух дюймов, что должно быть достаточно деталей: хранить ваши координаты в миллисекундах, и это проще, чище и безошибочно.

Ответ 4

В зависимости от использования структуры lat/lon я буду обеспокоен использованием float вместо double для lat/lon. Например, если вы выполняете интеграцию lat/lon в реальном времени, вам потребуется двойная точность. Это связано с тем, что степень составляет 1 морскую милю, и в зависимости от временного шага интеграции вы перемещаете очень маленькую сумму за итерацию времени.

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