【问题标题】:Implementing a geographic coordinate class: equality comparison实现地理坐标类:相等比较
【发布时间】:2011-07-21 10:48:35
【问题描述】:

我正在将 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. 我只能想象设置LatitudeLongitude 属性一次,这意味着不会累积错误来搞乱我的比较。

  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(因为现在是这样)可以看到here;非常欢迎 cmets 和建议。

【问题讨论】:

  • @ckeller:谢谢你的链接,我没找到。 Skeet 的解决方案几乎就是我在上面的 PasteBin sn-p 中所拥有的,也是答案所暗示的。应该绰绰有余。

标签: c# geocoding equality


【解决方案1】:

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

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

想法:

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

【讨论】:

  • 感谢马克的输入。我越想它就越明显(著名的遗言)它应该是一个不可变类型,特别是如果你认为这个类已经从根本上被破坏了,所以你可以把它的可变方面作为另一个设计错误抹去。另外,实现了IEquatable<GeoCoordinate>(它不在原来的类中),至于Equals(object)GetHashCode 的覆盖就不用说了。
【解决方案2】:
  • Equals 主要用于字典中,因此您应该仅将浮点数与 epsilon 进行比较的准则不适用于此处。不要尝试将 epsilon 逻辑放入Equals。用户的工作就是按照他的需要去做。证明唯一与 epsilon-comparisons 一致的散列函数是常数散列函数相对容易。

  • 您的实现有一个问题:它不能处理NaNs。您需要在 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】:

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

    【讨论】:

    • 我同意定点表示在概念上感觉更清晰。一个问题是三角函数不是在整数上定义的,而且你经常需要它们。
    • CodeInChaos 的评论很到位。此外,正如问题所述,我已经“继承”了实现(看起来不错),这样我就不必从头开始编写它。因此,虽然您的建议本身可能很好,但对我来说使用它并不是一个好主意。
    • 我明白了,乔恩,我没明白这一点。 @CodeInChaos我肯定有一种方法可以为三角函数的坐标获得浮点版本(除以3600000),但将整数作为基础模型进行精确比较,南北或东西距离等。
    【解决方案4】:

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

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

    【讨论】:

    • 以这种方式进行相等的问题在于它不能传递。完全相同的问题在正确实施GetHashCode 时表现出来(不是那么灾难性,但更容易看到)——所以我已经排除了这种方法。
    • 我不得不承认我有点迷失在编程术语中。但是,要强调浮点数与双精度问题:我假设您的浮点数是 IEEE754 32 位,我相信它只有 7 个有效数字。要获得准确的纬度/经度(至少在航空领域),您至少需要 10 个有效数字(考虑 lon -179.1234567)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-08-04
    • 2010-12-06
    • 2020-04-15
    • 1970-01-01
    • 1970-01-01
    • 2010-09-07
    • 1970-01-01
    相关资源
    最近更新 更多