Реализация класса географических координат: сравнение на равенство
Я интегрирую класс географических координат из 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
test решит проблему сравнения двух экземпляров, но создаст больше проблем:- Как сделать равенство транзитивным? Это звучит невозможно.
Как создать одинаковый хеш-код для всех значений, которые будут сравниваться равными?
Скажем так
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
(потому что это то, что сейчас) с соответствующими участниками можно увидеть здесь; комментарии и предложения более чем приветствуются.
4 ответа
"Место" с долготой / широтой для меня довольно хорошо попадает в слот "неизменное значение". Сама позиция не меняется - если вы меняете широту, которая является другой позицией. Оттуда это может быть struct
; за float
struct
в любом случае будет такого же размера, что и ссылка на x64, так что никакой реальной обратной стороны.
Ре равенство; если позиция не совсем такая же, она не "равняется", по крайней мере, в "ключевой" перспективе, поэтому я был бы счастлив ==
Вот. Вы можете добавить метод "находится в пределах (x) расстояния", если это помогло. Конечно, геометрия большой дуги тоже не совсем свободна;
Мысли хотя:
- это должно переопределить
bool Equals(object)
а также добавлениеbool Equals(GeoCoordinate)
- это должно переопределить
GetHashCode()
и реализоватьIEquatable<GeoCoordinate>
- статические операторы - это приятное дополнение
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 "указание предназначено для потребляющего кода, а не для реализующего кода. Поэтому я бы реализовал функцию, которая возвращает расстояние между двумя гео-координатами и велел бы пользователю использовать это для своих сравнений epsilon.Я бы определенно сделал его неизменным (не уверен, что это структура или класс). Он имеет семантику значений и поэтому должен быть неизменным.
Я обычно использую что-то вродеLinq-to-Xml
/Linq-to-Json
для сериализации, поскольку это позволяет мне преобразовать представление между моей моделью в памяти и моделью на диске.
Но вы правы, что многие сериализаторы не поддерживают конструкторы не по умолчанию. Я считаю это большим недостатком в этих сериализаторах, а не недостатком в моей модели. Некоторые сериализаторы просто получают доступ к закрытым сеттерам / полям, но лично я думаю, что воняет.
Я бы просто использовал длинные целые числа вместо чисел с плавающей точкой в базовой модели широты и долготы. Миллисекунда градуса в любом случае составляет менее двух дюймов, что должно быть достаточно подробно: хранить ваши координаты в миллисекундах, и это проще, чище и защищено от ошибок.
В зависимости от того, как вы используете структуру lat/lon, я бы хотел использовать float вместо double для lat/lon. Например, если вы выполняете интеграцию широты / долготы в реальном времени, вам потребуется двойная точность. Это связано с тем, что степень составляет 1 морскую милю, и в зависимости от временного шага интеграции вы перемещаете очень маленькое количество за итерацию времени.
Для выполнения теста на равенство я использовал бы простую формулу расстояния, основанную на равностороннем приближении, и решил, что если бы другая точка находилась в пределах моего установленного допуска (дюйм или два), то объявил бы их равными. Равноугольное приближение, которое я использовал бы, дано здесь.