I 'm integrating a geographic coordinate class from CodePlex to my personal "toolbox" library. This class uses float
fields to store latitude and longitude.
Since the class GeoCoordinate
implements IEquatable<GeoCoordinate>
, I habitually wrote the Equals
method like so:
public bool Equals(GeoCoordinate other)
{
if (other == null) {
return false;
}
return this.latitude == other.latitude && this.longitude == other.longitude;
}
At this point I stopped and considered that I 'm comparing floating point variables for equality, which is generally a no-no. My thought process then went roughly as follows:
I can only imagine setting the Latitude
and Longitude
properties once, which means that there will be no errors being accumulated to mess up my comparisons.
On the other hand, it's possible (albeit pointless) to write
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
Of course this is not something I can imagine doing, but if the public interface of the class allows it then Equals
should be able to handle it.
Comparing for equality using a difference < epsilon
test would solve the problem of comparing two instances, but create more problems:
How to produce the same hash code for all values that would compare equal?
Let's say that epsilon = 0.11
(random example). It follows that GeoCoordinate { 1, 1 }
would need the same hash code as GeoCoordinate { 1.1, 1.1 }
. But the latter would need the same hash code as GeoCoordinate { 1.2, 1.2 }
. You can see where this is going: all instances would need to have the same hash code.
A solution to all of this would be to make GeoCoordinate
an immutable class. This would also solve the GetHashCode
problem: it is based on latitude and longitude (what else), and if these are are mutable then using GeoCoordinate
as a key into a dictionary is asking for trouble. However, to make the class immutable has its own drawbacks:
Which approach would you suggest? It's easy to make the class fit the requirements I have right now (just make it immutable), but is there some better way?
Edit: I added an item 3 in the list above, shifting the previous item 3 to position 4.
I 'm going to allow some more time for feedback, but presently I 'm going with the immutable approach. The struct
(because that's what it is now) with the relevant members can be seen here; comments and suggestions more than welcome.
A geographic coordinate system (GCS) is used to define locations on a model of the surface of the earth. The GCS uses a network of imaginary lines (longitude and latitude) to define locations.
A GCS includes an angular unit of measure, a prime meridian, and a datum (based on a spheroid). A point is referenced by its longitude and latitude values. Longitude and latitude are angles measured from the earth's center to a point on the earth's surface. The angles often are measured in degrees (or in grads).
Geographic coordinate systems are based on a spheroid and utilize angular units (degrees). Projected coordinate systems are based on a plane (the spheroid projected onto a 2D surface) and utilize linear units (feet, meters, etc.).
For example, Baltimore, Maryland (in the USA) has a latitude of 39.3° North, and a longitude of 76.6° West. So, a vector drawn from the center of the Earth to a point 39.3° north of the equator and 76.6° west of Greenwich will pass through Baltimore.
A "location" with longitude/latitude to me falls quite nicely into the "immutable value" slot. The position itself doesn't change - if you change the latitude that is a different position. From there, it could be a struct
; for float
the struct
would be the same size as an x64 reference anyway, so no real down side.
Re equality; if the position isn't quite the same, it isn't "equals", at least in the "key" perspective, so I'd be happy with ==
here. You could add a "is within (x) distance" method if it helped. Of course, great-arc geometry isn't exactly totally free either ;p
Thoughts though:
bool Equals(object)
as well as adding a bool Equals(GeoCoordinate)
GetHashCode()
and implement IEquatable<GeoCoordinate>
Equals
is mainly used in dictionaries, so the guideline that you should compare floats only with an epsilon does not apply here. Do not attempt to put epsilon logic into Equals
. It's the users job to do that as he needs. It's relatively easy to prove that the only hash function that's consistent with epsilon-comparisons it the constant hashfunction.
Your implementation has one problem though: It doesn't handle NaN
s. You need to use Equals
instead of ==
on the individual coordinates for your Equals
method.
public bool Equals(GeoCoordinate other)
{
if (other == null) {
return false;
}
return this.latitude.Equals( other.latitude) && this.longitude.Equals(other.longitude);
}
The "Don't compare using ==
but using epsilon" guideline is for the consuming code, not for the implementing code. So I'd implement a function that returns the distance between two geo-coordinates and tell the user to use this for his epsilon comparisons.
I'd definitely make it immutable (not sure if a struct or class). It has value semantics and thus should be immutable.
I usually use something like Linq-to-Xml
/Linq-to-Json
for serialization, since this allows me to transform the representation between my in-memory model and the on-disk model.
But you're right that many serializers don't support non default constructors. I regard that as a big flaw in those serializers, not as a flaw in my model. Some serializers simply access the private setters/fields, but personally I think that stinks.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With