负数的Objective-C模运算行为很奇怪

43

我认为负数在进行取模运算时应该转换成正数……但是我无法在Objective-C中实现这一点。

我期望得到以下结果:

-1 % 3 = 2
 0 % 3 = 0
 1 % 3 = 1
 2 % 3 = 2

但是注意这一点

-1 % 3 = -1
 0 % 3 = 0
 1 % 3 = 1
 2 % 3 = 2

为什么会这样,有没有解决方法?


Clojure是我遇到的第一种正确实现mod的语言。耶! - Todd Owen
2
@ToddOwen:Python 也可以正确地完成它 - Claudiu
@Todd,Lisp两者都有。 - Pacerier
使用frem(a, b) - 你所期望的模除(在标准数学中使用的那种)在编程中称为“余数”。C语言有fmod()和frem()函数,而你正在使用mod(也称为“%”),你需要使用rem。在数学中的模除等同于代码中的余数(rem)。有点傻,我知道。 - Albert Renshaw
我注意到frem(a,b)仅在GNU C中存在,而未被引入Obj-C。相当于这个是:a-b*floor((float)a/(float)b) - Albert Renshaw
12个回答

54
result = n % 3;
if( result < 0 ) result += 3;

不要像其他答案中建议的那样执行额外的模运算。它们非常昂贵且不必要。


1
条件语句是否比取模更好,这不取决于处理器吗? - Nosredna
2
我从未见过一个处理器,其中除法或取模不是最慢的操作。检查负数可以简化为测试位值,因此通常是最快的指令之一。 - user66363
是的,那可能是对的。我认为在Core Duo Intel处理器中,分支指令大约需要15-17个时钟周期。现在很多人都试图避免使用分支指令。但是通过一些修改,分支指令可能是值得的。 - Nosredna
据我所知,避免分支的想法是为了防止流水线停顿。但执行两个模块操作可能会导致流水线停顿;第二个模块操作需要等待第一个操作的结果。 - user66363
5
if() 这个条件语句几乎肯定比第二个取模运算快;任何一个不错的编译器都可以将 if() 转化为 cmov(条件移动)指令,这样就可以在不使用分支的情况下完成相同的操作。 - Adam Rosenfield

15
在C和Objective-C中,除法和取模运算符会向零方向截断。如果a/b>0,则a / b是floor(a / b),否则a / b是ceiling(a / b)如果a / b<0。除非b为0,否则始终有a == (a / b) * b + (a % b)。因此,positive%positive == positive,positive%negative == positive,negative%positive == negative和negative%negative == negative(您可以计算出所有4种情况的逻辑,尽管有些棘手)。

9
如果n的范围有限,那么你可以通过添加已知常数的3倍来获得所需结果,该常数应大于最小值的绝对值。
例如,如果n的范围限制在-1000..2000之间,则可以使用以下表达式:
result = (n+1002) % 3;

确保在求和时,最大值加上您的常数不会溢出。


7
我们有一个语言问题:
math-er-says:我把这个数字加上那个数字再对其他数字取模
code-er-hears:我把两个数字相加,然后将结果除以其他数字
code-er-says:负数怎么办?
math-er-says:什么?在字段模其他数字中没有负数的概念?
code-er-says:领域是什么?...
在这种情况下,您想要数学家的模运算符,并且可以使用余数函数。 您可以通过每次进行减法时检查是否跌落到底部来将余数运算符转换为数学家的模运算符。
  • 这段对话中的数学家正在谈论在一个循环数字线上进行数学计算。如果您从底部减去,则会绕回到顶部。
  • 代码人员正在谈论计算余数的运算符。

自然数定义了除法,使得 (n+d)/d==(n/d)+1。实数也是如此。实数还支持 -(n/d)==(-n)/d,但自然数不支持。整数可能支持其中一个公理,但不能同时支持两个。虽然编程语言可以为整数维护第一个公理,并且有些确实这样做了,但 FORTRAN 的创建者决定支持第二个公理,许多其他语言也效仿了这种做法。 - supercat

5
如果这将是行为方式,而您知道它将是,那么对于m%n = r,只需使用r = n + r。如果您不确定会发生什么,请使用r = r%n
编辑:总之,使用r =(n +(m%n))%n

1
如果你刚才说他应该做(-1 + 3) % 3 = 2,我同意。 :-) - Nosredna
1
我会说做 ( 3 + ( -1 % 3 ) ) % 3 == (3 + (-1) ) % 3 == ( 2 % 3 ) == 2 - maxwellb
明白了,很好。也可以用条件语句(if或三元运算符)来实现。 - Nosredna

4
我本来也期望得到一个正数,但是我在 ISO/IEC 14882:2003:编程语言--C++,5.6.4 中找到了这个内容(在维基百科上关于模运算的文章中发现):
二元 % 运算符给出第一个表达式除以第二个表达式的余数。……如果两个操作数都是非负数,则余数为非负数;否则,余数的符号是实现定义的。

1
糟糕的规格说明,万岁。 :| - devios1

1

UncleO的答案可能更加健壮,但是如果你想在一行代码中完成,并且你确定负值不会比模数的单次迭代更小(例如,如果你只在任何时候最多减去模数值),那么你可以简化为一个表达式:

int result = (n + 3) % 3;

既然您已经在进行修改,将初始值加3不会产生任何影响除非n为负数(但不小于-3),这种情况下会导致结果成为预期的正模数。


1

JavaScript也是这样做的。我被它抓住了几次。把它看作是关于零的反射,而不是延续。


1
为什么:因为这是C标准中指定的模运算符的方式(请记住,Objective-C是C的扩展)。它让我认识的大多数人(包括我自己)感到困惑,因为它很出乎意料,你必须记住它。
至于解决方法:我会使用uncleo的。

是的,就像你在y轴上减去来向上移动一样令人困惑。这只是语言早期设计者偏好之一。起初很烦人,但后来可能会很有用。事实上,我敢打赌,当程序员必须设计紧凑函数以在程序中减少字节时,这种模式工作方式非常有用,同样的原因数组从索引0开始而不是1,这使得数学更加紧凑。 - Albert Renshaw

0

不仅是 JavaScript,几乎所有的语言都会显示错误的答案。康耐比尔所说的是正确的,当我们进行模运算时,必须得到余数。余数就是除法后剩下的部分,它应该是一个正整数...

如果你查看数轴,你就能理解这一点。

我在 VB 中也遇到了同样的问题,这迫使我强制添加额外的检查,例如:如果结果为负数,我们必须将除数加到结果中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接