Реализация класса географических координат: сравнение на равенство - PullRequest
11 голосов
/ 21 июля 2011

Я интегрирую класс географических координат из 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;
}

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

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

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

    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 сможет справиться с этим.

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

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

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

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

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

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

Редактировать : я добавил элемент 3 в список выше, переместив предыдущий элемент 3 в положение 4.

Решение

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

Ответы [ 4 ]

6 голосов
/ 21 июля 2011

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

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

Мысли хотя:

  • она должна переопределить bool Equals(object), а также добавить bool Equals(GeoCoordinate)
  • он должен переопределить GetHashCode() и реализовать IEquatable<GeoCoordinate>
  • статические операторы - это приятный дополнительный опциональный
6 голосов
/ 21 июля 2011
  • Equals в основном используется в словарях, поэтому правило, согласно которому вы должны сравнивать поплавки только с эпсилоном, здесь не применимо. Не пытайтесь поместить эпсилон логику в Equals. Это работа пользователей делать это так, как ему нужно. Относительно легко доказать, что единственная хеш-функция, которая соответствует эпсилон-сравнениям, это постоянная хеш-функция.

  • В вашей реализации есть одна проблема: она не обрабатывает 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» предназначена для потребляющего кода, а не для реализующего кода. Поэтому я бы реализовал функцию, которая возвращает расстояние между двумя гео-координатами, и велел бы пользователю использовать это для своих сравнений эпсилон.

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

2 голосов
/ 21 июля 2011

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

1 голос
/ 21 июля 2011

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

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

...