Search code examples
c#dictionarydouble-precision

Dictionary Key Not Found When It Does Exist


Why is my Dictionary saying a key doesn't exist when I just inserted it? Is it to do with my Equals method or double comparision?

Here is the code:

// Test: I know the dictionary contains nCoord but its saying the key doesn't exist
Dictionary<UTMCoordinate, int> planes = new Dictionary<UTMCoordinate, int>();
UTMCoordinate nCoord    = new UTMCoordinate(337394.136407966, 6263820.40182064, 0, 56, UTMCoordinate.Hemisphere.H_SOUTHERN);
planes[nCoord]          = 1;

bool exists = planes.ContainsKey(nCoord);  // always returns false

My implementation of UTMCoordinate is below:

public class UTMCoordinate
{
    public enum                 Hemisphere {H_NOTHERN, H_SOUTHERN};
    public const double         DIF_TOLERANCE    = 0.0005;
    public double               x               { get; set; }
    public double               y               { get; set; }
    public double               elev            { get; set; }
    public uint                 UTMZone         { get; set; }
    public Hemisphere           hemisphere      { get; set; }

    public UTMCoordinate(double x, double y, double elev=double.MinValue, uint utmZone=uint.MinValue, Hemisphere hemisphere=Hemisphere.H_SOUTHERN) {
        this.x          = x;
        this.y          = y;
        this.elev       = elev;
        this.UTMZone    = utmZone;
        this.hemisphere = hemisphere;
    }

    public override int GetHashCode() {
        unchecked // Overflow is fine, just wrap
        {
            int hash = 17;
            // Suitable nullity checks etc, of course :)
            hash = hash * 23 + x.GetHashCode();
            hash = hash * 23 + y.GetHashCode();
            hash = hash * 23 + elev.GetHashCode();
            hash = hash * 23 + UTMZone.GetHashCode();
            hash = hash * 23 + hemisphere.GetHashCode();
            return hash;
        }
    }

    public override bool Equals(object obj)
    {
        UTMCoordinate other = obj as UTMCoordinate;
        if (other == null)
            return false;

        return double.Equals(x, other.x) && double.Equals(y, other.y) && double.Equals(elev, other.elev) && uint.Equals(UTMZone, other.UTMZone) && double.Equals(hemisphere, other.hemisphere);
    }
}

Edit Using Daniel A. Whites advice I've used a different double comparision method. Unfortunately its still not identifying the key:

public override bool Equals(object obj)
{
    //return base.Equals (obj);
    UTMCoordinate other = obj as UTMCoordinate;
    if (other == null)
        return false;

    //return double.Equals(x, other.x) && double.Equals(y, other.y) && double.Equals(elev, other.elev) && uint.Equals(UTMZone, other.UTMZone) && double.Equals(hemisphere, other.hemisphere);
    return Math.Abs (x-other.x) <= DIF_TOLERANCE && Math.Abs (y-other.y) <= DIF_TOLERANCE && Math.Abs (elev-other.elev) <= DIF_TOLERANCE && uint.Equals(UTMZone, other.UTMZone) && hemisphere == other.hemisphere;
}

Solution

  • If I take your code from your question:

    Dictionary<UTMCoordinate, int> planes = new Dictionary<UTMCoordinate, int>();
    UTMCoordinate nCoord    = new UTMCoordinate(337394.136407966, 6263820.40182064, 0, 56, UTMCoordinate.Hemisphere.H_SOUTHERN);
    planes[nCoord]          = 1;
    
    bool exists = planes.ContainsKey(nCoord);
    

    The value I get for exists is true.

    However, if I do this:

    nCoord.x = 1.0;
    exists = planes.ContainsKey(nCoord);
    

    The value for exists suddenly becomes false even though the object is still in the dictionary. This is because the value for GetHashCode has changed. It was -1473667404, but after the assignment of the property x it becomes 201352392.

    The dictionary uses the value of GetHashCode to determine which bucket to put the key in so when the hash code changes the dictionary may try finding the key in the wrong bucket and then reports that it doesn't contain the key.

    I suspect in your code that is what is happening.

    So you need to change your object so that it is immutable.

    public double               x               { get; private set; }
    public double               y               { get; private set; }
    public double               elev            { get; private set; }
    public uint                 UTMZone         { get; private set; }
    public Hemisphere           hemisphere      { get; private set; }
    

    And then do NOT change any of the values outside of the constructor.