Domanda

Is it safe to compare C# doubles or floats against assigned, default values?

For example:

public class Foo
{
    private double defaultEfficiency = 100.0;
    public double efficiencyBar = defaultEfficiency;

    public bool IsBarAtDefaultValue()
    {
        if(efficiencyBar == defaultEfficiency)
            return true;
        else
            return false;
    }
}

So my question is if the check within IsBarAtDefaultValue() is going to work as I expect it to? ie. It will return true if efficiencyBar is the same as defaultEfficiency.

This SO question: Is it safe to check floating ... asks about a specific case when the default value is 0.0. My question concerns the broader case of any default value.


To provide a more concrete example...

I am working on an application that deals with motor efficiencies which are generally in the range of 0 - 100%, or 0.0 to 1.0. The user has the ability to define new motors and assign various efficiencies.

In the panel to define a new motor, I want to populate the various efficiencies with default values. Later on, I want to check and see if the user altered the value to something other than the default. For example, I want to check and see if they made changes but accidentally forgot to save their work.

That led to my wondering about what value is actually used in the assignment of a default value for floating point types (double & float). The linked SO question discusses the 0.0 case, but I wondered about the broader (not 0.0) case as I don't want to use 0.0 for the default values for the efficiencies.

È stato utile?

Soluzione

Well, yes and no. If all of the floating-point values you are comparing are stored as constants and the constants are the same data width (i.e., you compare float to float and double to double), this is a safe operation. But if everything you were comparing was a constant, why not use integers or enums?

In general, comparing floating-point numbers for equality is unsafe. The reason for this is that floating-point numbers can't perfectly store all values. This is a lot like the problem with the number 1/3 in decimal which we would have to write as 0.33333... The same problem exists for storing fractional parts in binary, and numbers that have finite representations in decimal notation are not guaranteed to have finite binary representations. Since we are limited to 32 or 64 bits, part of the number gets truncated. This means that performing math operations on floating-point numbers could result in unexpected consequences.

Consider this quote from this post from Bruce M. Bush:

At the heart of many strange results is one fundamental: floating-point on computers is usually base 2, whereas the external representation is base 10. We expect that 1/3 will not be exactly representable, but it seems intuitive that .01 would be. Not so! .01 in IEEE single-precision format is exactly 10737418/1073741824 or approximately 0.009999999776482582.

You should usually check equality in floating-point values using some small epsilon for variance.

public class Foo
{
    //Choose a small value that is appropriate for your needs
    //see the info below for some info from Microsoft
    private static double epsilon = 0.00001;
    private double defaultEfficiency = 100.0;
    public double efficiencyBar = defaultEfficiency;

    public bool IsBarAtDefaultValue()
    {
        //we use the absolute value of the difference. If this is smaller than
        //epsilon, then the value is "good enough" for equal
        if (Math.Abs(efficiencyBar - defaultEfficiency) < epsilon)
            return true;
        else
            return false;
    }
}

You could use something like Double.Epsilon for your epsilon value, but this is probably way too small for your needs and is recommended against in the documentation:

If you create a custom algorithm that determines whether two floating-point numbers can be considered equal, we do not recommend that you base your algorithm on the value of the Epsilon constant to establish the acceptable absolute margin of difference for the two values to be considered equal. (Typically, that margin of difference is many times greater than Epsilon.)

And in their documentation on the Double.Equals() method:

Because Epsilon defines the minimum expression of a positive value whose range is near zero, the margin of difference between two similar values must be greater than Epsilon. Typically, it is many times greater than Epsilon. Because of this, we recommend that you do not use Epsilon when comparing Double values for equality.

Both places are good sources of additional information on comparing floating-point numbers safely.

Altri suggerimenti

Yes it is safe to do this. Consider that your code is just doing this:

double x = ...;
double y = x;
bool t = (x == y); //always true regardless of the value of x

Because efficiencyBar is public and can be modified outside of the class, you have no idea what it could be set to and if floating point prevision issues come into play dependent on the specified new values at run-time. It would be best then to compare against an epsilon value, due to floating point precision issues.

return (Math.Abs(efficiencyBar - defaultEfficiency) < Double.Epsilon);

If the primary goal is to detect if the value changed from a default/initial value, you could just define a custom setter that kept track of whether the value was ever changed.

public class Foo
{
    private double defaultEfficiency = 100.0;
    private double _efficiencyBar = defaultEfficiency;

    public double efficiencyBar
    {
        get
        {
            return _efficiencyBar;
        }
        set
        {
            _efficiencyBar = value;
            _efficiencyBarChanged = true;
        }
    }

    private bool _efficiencyBarChanged = false;
    //Now you know if it was ever changed, period
    //even if it got changed back to the default value
    public bool IsBarAtDefaultValue
    {
        get
        {
            return !_efficiencyBarChanged;
            //if you preferred, this could still be an equality-like test
            //but keeping track of a state change in a bool value makes more sense to me
        }
    }
}

If you wanted to be able to "reset" the value to default and have your check come back false, I would recommend a method like the following:

public void resetEfficiencyBar()
{
    _efficiencyBar = defaultEfficiency;
    _efficiencyBarChanged = false;
}

This avoids all of the complications associated with floating-point comparisons, and I think makes the intent of your code more clear.

Note that all integers of magnitude less than or equal to 253+1 are exactly representable in IEEE754 double precision and consequently your IsBarAtDefaultValue function will do exactly what you expect it to in your case of defaultEfficiency equal to 100.0.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top