我知道UIKit使用CGFloat,因为它是分辨率无关的坐标系。

但每次我想检查frame.origin.x是否为0时,我都觉得很恶心:

if (theView.frame.origin.x == 0) {
    // do important operation
}

当与==,<=,>=,<,>比较时,CGFloat是否容易出现假阳性? 它是一个浮点数,它们有不精确的问题:例如0.0000000000041。

Objective-C在比较时是否会在内部处理这个或者是否会发生原点。读作0的X不与0比较为真?


当前回答

First of all, floating point values are not "random" in their behavior. Exact comparison can and does make sense in plenty of real-world usages. But if you're going to use floating point you need to be aware of how it works. Erring on the side of assuming floating point works like real numbers will get you code that quickly breaks. Erring on the side of assuming floating point results have large random fuzz associated with them (like most of the answers here suggest) will get you code that appears to work at first but ends up having large-magnitude errors and broken corner cases.

首先,如果你想用浮点数编程,你应该读一下:

每个计算机科学家都应该知道浮点运算

是的,通读一遍。如果这对你来说负担太大,你应该使用整数/固定点来计算,直到你有时间阅读它。: -)

现在,说了这么多,精确浮点比较的最大问题归结为:

The fact that lots of values you may write in the source, or read in with scanf or strtod, do not exist as floating point values and get silently converted to the nearest approximation. This is what demon9733's answer was talking about. The fact that many results get rounded due to not having enough precision to represent the actual result. An easy example where you can see this is adding x = 0x1fffffe and y = 1 as floats. Here, x has 24 bits of precision in the mantissa (ok) and y has just 1 bit, but when you add them, their bits are not in overlapping places, and the result would need 25 bits of precision. Instead, it gets rounded (to 0x2000000 in the default rounding mode). The fact that many results get rounded due to needing infinitely many places for the correct value. This includes both rational results like 1/3 (which you're familiar with from decimal where it takes infinitely many places) but also 1/10 (which also takes infinitely many places in binary, since 5 is not a power of 2), as well as irrational results like the square root of anything that's not a perfect square. Double rounding. On some systems (particularly x86), floating point expressions are evaluated in higher precision than their nominal types. This means that when one of the above types of rounding happens, you'll get two rounding steps, first a rounding of the result to the higher-precision type, then a rounding to the final type. As an example, consider what happens in decimal if you round 1.49 to an integer (1), versus what happens if you first round it to one decimal place (1.5) then round that result to an integer (2). This is actually one of the nastiest areas to deal with in floating point, since the behaviour of the compiler (especially for buggy, non-conforming compilers like GCC) is unpredictable. Transcendental functions (trig, exp, log, etc.) are not specified to have correctly rounded results; the result is just specified to be correct within one unit in the last place of precision (usually referred to as 1ulp).

When you're writing floating point code, you need to keep in mind what you're doing with the numbers that could cause the results to be inexact, and make comparisons accordingly. Often times it will make sense to compare with an "epsilon", but that epsilon should be based on the magnitude of the numbers you are comparing, not an absolute constant. (In cases where an absolute constant epsilon would work, that's strongly indicative that fixed point, not floating point, is the right tool for the job!)

编辑:特别地,相对大小的检查应该看起来像这样:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))

FLT_EPSILON是float.h中的常量(将其替换为双精度的DBL_EPSILON或长双精度的LDBL_EPSILON), K是你选择的常量,这样你的计算的累积误差在最后一个地方肯定是由K个单位限制的(如果你不确定你得到的误差范围计算是正确的,让K比你的计算说的应该大几倍)。

最后,请注意,如果使用此方法,可能需要在接近零时进行一些特殊处理,因为FLT_EPSILON对于非法线没有意义。一个快速的解决方法是:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)

如果使用double,同样替换DBL_MIN。

其他回答

你可以使用这样的代码来比较float和0:

if ((int)(theView.frame.origin.x * 100) == 0) {
    // do important operation
}

这将与0.1的精度进行比较,在这种情况下,这对CGFloat来说足够了。

我认为正确的做法是将每个数字声明为一个对象,然后在该对象中定义三个东西:1)相等运算符。2)一个setAcceptableDifference方法。3)价值本身。如果两个值的绝对差小于设置为可接受的值,则相等运算符返回true。

您可以对对象进行子类化以适应该问题。例如,如果1到2英寸之间的圆金属棒的直径相差小于0.0001英寸,则可以认为它们的直径相等。因此,您可以使用参数0.0001调用setAcceptableDifference,然后放心地使用相等操作符。

[“正确答案”掩盖了选择K。选择K就像选择VISIBLE_SHIFT一样特别,但选择K不那么明显,因为与VISIBLE_SHIFT不同,它不基于任何显示属性。因此选择你的毒药-选择K或选择VISIBLE_SHIFT。这个答案主张选择VISIBLE_SHIFT,然后演示了选择K的困难。

正是由于四舍五入的错误,您不应该在逻辑操作中使用“精确”值的比较。在视觉显示的特定情况下,位置是0.0还是0.0000000003可能无关紧要——肉眼是看不见差异的。所以你的逻辑应该是这样的:

#define VISIBLE_SHIFT    0.0001        // for example
if (fabs(theView.frame.origin.x) < VISIBLE_SHIFT) { /* ... */ }

然而,最终,“看不见的眼睛”将取决于你的显示属性。如果你能上界显示(你应该可以);然后选择VISIBLE_SHIFT作为上限的一个分数。

现在,“正确答案”取决于K,所以让我们来探索选择K。

K是一个常数,你选择使你的累积误差 计算的最后一个位置肯定是K个单位(和 如果你不确定误差范围的计算是正确的,取K a 比你的计算结果大几倍)

所以我们需要K,如果得到K比选择VISIBLE_SHIFT更难,更不直观,那么你就决定什么对你有效。为了找到K,我们要写一个测试程序,看一堆K的值,这样我们就能看到它的行为。如果“正确答案”可用,那么如何选择K应该是显而易见的。没有?

我们将使用,作为“正确答案”的细节:

if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)

我们试一下K的所有值:

#include <math.h>
#include <float.h>
#include <stdio.h>

void main (void)
{
  double x = 1e-13;
  double y = 0.0;

  double K = 1e22;
  int i = 0;

  for (; i < 32; i++, K = K/10.0)
    {
      printf ("K:%40.16lf -> ", K);

      if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)
        printf ("YES\n");
      else
        printf ("NO\n");
    }
}
ebg@ebg$ gcc -o test test.c
ebg@ebg$ ./test
K:10000000000000000000000.0000000000000000 -> YES
K: 1000000000000000000000.0000000000000000 -> YES
K:  100000000000000000000.0000000000000000 -> YES
K:   10000000000000000000.0000000000000000 -> YES
K:    1000000000000000000.0000000000000000 -> YES
K:     100000000000000000.0000000000000000 -> YES
K:      10000000000000000.0000000000000000 -> YES
K:       1000000000000000.0000000000000000 -> NO
K:        100000000000000.0000000000000000 -> NO
K:         10000000000000.0000000000000000 -> NO
K:          1000000000000.0000000000000000 -> NO
K:           100000000000.0000000000000000 -> NO
K:            10000000000.0000000000000000 -> NO
K:             1000000000.0000000000000000 -> NO
K:              100000000.0000000000000000 -> NO
K:               10000000.0000000000000000 -> NO
K:                1000000.0000000000000000 -> NO
K:                 100000.0000000000000000 -> NO
K:                  10000.0000000000000000 -> NO
K:                   1000.0000000000000000 -> NO
K:                    100.0000000000000000 -> NO
K:                     10.0000000000000000 -> NO
K:                      1.0000000000000000 -> NO
K:                      0.1000000000000000 -> NO
K:                      0.0100000000000000 -> NO
K:                      0.0010000000000000 -> NO
K:                      0.0001000000000000 -> NO
K:                      0.0000100000000000 -> NO
K:                      0.0000010000000000 -> NO
K:                      0.0000001000000000 -> NO
K:                      0.0000000100000000 -> NO
K:                      0.0000000010000000 -> NO

啊,所以K应该是1e16或者更大如果我想让1e13等于0。

所以,我认为你有两个选择:

就像我建议的那样,用你的工程判断来做一个简单的计算。如果你做的是图形,“零”意味着“可见的变化”,那就检查你的视觉资产(图像等)并判断epsilon可以是什么。 不要尝试任何浮点计算,直到你阅读了非货物崇拜答案的参考资料(并在此过程中获得博士学位),然后使用你的非直觉判断选择K。

我使用下面的比较函数来比较小数点后的数位:

bool compare(const double value1, const double value2, const int precision)
{
    int64_t magnitude = static_cast<int64_t>(std::pow(10, precision));
    int64_t intValue1 = static_cast<int64_t>(value1 * magnitude);
    int64_t intValue2 = static_cast<int64_t>(value2 * magnitude);
    return intValue1 == intValue2;
}

// Compare 9 decimal places:
if (compare(theView.frame.origin.x, 0, 9)) {
    // do important operation
}

The last time I checked the C standard, there was no requirement for floating point operations on doubles (64 bits total, 53 bit mantissa) to be accurate to more than that precision. However, some hardware might do the operations in registers of greater precision, and the requirement was interpreted to mean no requirement to clear lower order bits (beyond the precision of the numbers being loaded into the registers). So you could get unexpected results of comparisons like this depending on what was left over in the registers from whoever slept there last.

也就是说,尽管我一看到它就努力删除它,但我工作的机构有大量使用gcc编译并运行在linux上的C代码,我们已经很长时间没有注意到这些意想不到的结果了。我不知道这是否是因为gcc为我们清除了低阶位,80位寄存器在现代计算机上不用于这些操作,标准已经改变了,还是什么。我想知道是否有人可以引用章节和诗句。