问题
I'm looking to implement an IEqualityComparer class that stores and compares floating point keys that are rounded to the nearest 0.01. In particular, I want to make sure I implement the GetHashCode method correctly. I would like to make this as efficient as possible. Can I use just use the float value itself as it's own hash?
I could multiply by 100, cast to int and use an int as a key, but I'm curious if this can be done with float keys.
Note: I would wrap the dictionary in a class to ensure that only values rounded to .01 are ever added or compared.
Follow up question: If I used Decimal (guaranteed to always be rounded to .01) could I just use the default comparer for Decimal with Decimal keys in a Dictionary?
My first thought is to try this implementation. Any pitfalls?
class FloatEqualityComparer : IEqualityComparer<float>
{
public bool Equals(float b1, float b2)
{
int i1 = (int)(b1 * 100);
int i2 = (int)(b2 * 100);
if(i1 == i2)
return true;
else
return false;
}
public float GetHashCode(float x)
{
return x;
}
}
回答1:
The problem is the GetHashCode
implementation. If two floats might be considered equal, they must yield the same hash codes.
Why not
sealed class FloatEqualityComparer : IEqualityComparer<float>
{
public bool Equals(float x, float y) => Math.Round(x, 3) == Math.Round(y, 3);
public int GetHashCode(float f) => Math.Round(f, 3).GetHashCode();
}
The reason for this is that the equality test is not performed if two hash codes are different. This improves performance dramatically as otherwise each value would need to be compared to every other value ->
O(N2). Therefore, if two values should be compared to one another for equality, their hash codes must collide.
Note that any type may be used as a key in an IDictionary<TKey, TValue>
.
回答2:
There is nothing in the .NET documentation to say that floating values returned from Math.Round() will pass the equality comparison when they should, e.g. 2.32 should always equal 2.32, but if either value is plus or minus float.Epsilon, the equality could befalse. This risks creating 2 keys for the same value shifted by only float.Epsilon. I'm solving this unlikely (although buggy) issue by handling the rounding by multiplying and casting to int instead of calling Math.Round().
sealed class FloatEqualityComparer : IEqualityComparer<float>
{
int GetPreciseInt(float f)
{
int i1 = (int)(b1 * 100);
int i2 = (int)(b2 * 100);
return (i1 == i2);
}
public bool Equals(float f1, float f2) => GetPreciseInt(f1) == GetPreciseInt(f2);
public int GetHashCode(float f) => GetPreciseInt(f).GetHashCode();
}
*I am not concerned about the edge cases in rounding floating point numbers of finite precision, rather I am concerned about using those rounded imprecise floats as keys in a dictionary.
来源:https://stackoverflow.com/questions/48655003/how-to-use-a-float-as-key-in-c-sharp-dictionary-with-custom-comparer-that-rounds