问题 实现地理坐标类:相等比较


我正在将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. 使用a比较相等性 difference < epsilon test会解决比较两个实例的问题,但会产生更多问题:

    • 如何使平等传递?听起来不可能。
    • 如何生产 相同 哈希代码 所有 比较相等的价值?

      我们这样说吧 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 (因为这就是现在的情况)可以看到相关成员 这里;评论和建议超过欢迎。


3323
2017-07-21 10:48


起源

也可以看看 stackoverflow.com/questions/6151625/... - ccellar
@ckeller:感谢您的链接,我没有找到它。 Skeet的解决方案与我上面的PasteBin片段完全相同,也是答案所暗示的。应该绰绰有余。 - Jon


答案:


对我来说经度/纬度的“位置”非常适合“不可变值”槽。该 定位本身 不会改变 - 如果你改变纬度是一个 不同的立场。从那里,它 可以 是一个 struct;对于 floatstruct 无论如何,它将与x64参考大小相同,因此没有真正的缺点。

重新平等;如果位置不完全相同,那就不是“等于”,至少在“关键”的角度来看,所以我很满意 == 这里。如果它有帮助,您可以添加“在(x)距离内”方法。当然,大弧几何也不是完全自由的; p

但是思考:

  • 它应该覆盖 bool Equals(object) 以及添加一个 bool Equals(GeoCoordinate)
  • 它应该覆盖 GetHashCode() 并实施 IEquatable<GeoCoordinate>
  • 静态运算符是一个很好的可选附加功能

6
2017-07-21 10:58



感谢输入Marc。我越是想到它就越明显(着名的最后一句话)变成它应该是一个不可变的类型,特别是如果你认为这个类已经从根本上被破坏了所以你可以把它的可变方面作为另一个设计错误。也, IEquatable<GeoCoordinate> 实现(它不是在原始类中),至于 Equals(object) 和 GetHashCode 不言自明的压倒一切。 - Jon


  • Equals 主要用于词典,所以你应该只用epsilon比较的指南不适用于此。不要试图将epsilon逻辑放入 Equals。根据需要,这是用户的工作。证明与epsilon一致的唯一哈希函数与常量哈希函数进行比较相对容易。

  • 您的实现有一个问题:它无法处理 NaN秒。你需要使用 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 对于序列化,因为这允许我转换我的内存模型和磁盘上模型之间的表示。
    但你是对的,许多序列化程序不支持非默认构造函数。我认为这是那些序列化程序中的一个大缺陷,而不是我模型中的一个缺陷。一些序列化程序只是访问私有的setter /字段,但我个人认为这很糟糕。


6
2017-07-21 10:55





我会在纬度和经度的基础模型中使用长整数而不是浮点数。在任何情况下,毫秒的度数都小于2英寸,这应该足够详细:以毫秒为单位存储坐标,它更简单,更清晰,更防错。


2
2017-07-21 10:59



我同意固定点表示在概念上更清晰。一个问题是三角函数没有在整数上定义,而且你经常需要这些函数。 - CodesInChaos
CodeInChaos的评论是现货。此外,正如问题所述,我已经“继承”了实现(看起来还不错),所以我不必从头开始编写它。因此,虽然您的建议本身可能很好,但使用它并不是一个好主意。 - Jon
我明白了,乔恩,我没有明白这一点。 @CodeInChaos我肯定会有一个方法来获得三角形坐标的坐标度(除以3600000)的浮动版本,但保持整数作为精确比较,南北或东西距离等的基础模型。 - Mauro Vanetti


根据你对lat / lon结构的使用,我会担心使用浮点数代替lat / lon的双精度数。例如,如果您正在进行lat / lon的实时集成,那么您将需要双精度。这是因为度数是1海里,并且取决于积分的时间步长,每次迭代只移动非常小的量。

为了进行相等测试,我将使用基于等距矩形近似的简单距离公式,并确定如果另一个点在我的确定公差范围内(一英寸或两英寸),则声明它们相等。给出了我将使用的等距矩形近似 这里


1
2017-07-21 16:04



以这种方式做平等的问题在于它不能被传递。完全相同的问题在正确实现中表现出来(不是灾难性的,但更容易看到) GetHashCode  - 所以我已经决定退出了。 - Jon
我不得不承认我在编程术语中有点迷失。但是,在浮动和双重问题上竖琴:我假设您的浮点数是IEEE754 32位,我相信只有7位有效数字。对于准确的纬度/经度(至少在航空中),您需要至少10个有效数字(考虑lon -179.1234567)。 - TreyA