循环增量:哪个“更好”?

时间:2010-05-10 09:26:52

标签: java coding-style

当你有一个循环缓冲区表示为一个数组,你需要索引环绕时(即,当你达到最高可能的索引并增加它时),它是否“更好”:

return (++i == buffer.length) ? 0: i;

return ++i % buffer.length;

使用模运算符有任何缺点吗?它的可读性是否低于第一个解决方案?

修改

当然它应该是++ i而不是i ++,改变了它。

编辑2:

一个有趣的注意事项:我在Doug Lea的ArrayBlockingQueue实现中找到了第一行代码。

12 个答案:

答案 0 :(得分:10)

  

更新:OP已在评论中承认已预先增加。大多数其他答案错过了这个。有证据表明,这种情况的增加导致可怕的可读性:有一个错误,大多数人都看不到它。

最易阅读的版本如下:

return (i == buffer.length-1) ? 0 : i+1;

使用++会给支票带来不必要的副作用(更不用说我强烈认为你应该使用预增量)

原始代码有什么问题?我们来看看吧?

return (i++ == N) ? 0 : i; // OP's original, slightly rewritten

所以我们知道:

  • i 发布 -incremented,因此当i == N-1语句之前的return时,这将返回N而不是立即换行到0
    • 这是打算吗?大多数情况下,目的是使用N作为独占上限
  • 变量名i通过命名约定建议局部变量,但它真的是吗?
    • 由于副作用,需要仔细检查是否为字段

相比之下:

return (i == N-1) ? 0 : i+1; // proposed alternative

我们知道:

  • i未被修改,无论是本地变量还是字段
  • 都无关紧要
  • i == N-1时,返回的值为0,这是更典型的情况

%方法

或者,您也可以使用%版本,如下所示:

return (i+1) % N;

%有什么问题?好吧,问题是即使大多数人认为它是模数运算符,它也不是!它是余数运算符(JLS 15.17.3)。很多人经常对此感到困惑。这是一个典型的例子:

boolean isOdd(int n) {
   return (n % 2) == 1; // does this work???
}

那段代码坏了!!!对于所有负值,它返回false!问题在于-1 % 2 == -1,尽管是数学-1 = 1 (mod 2)

%可能很棘手,这就是我推荐使用三元运算符版本的原因。但最重要的是消除增量的副作用。

另见

答案 1 :(得分:5)

不要让我在两个包含后增量(*)和表达评估的选项之间进行选择。我会说“没有”。

(*)更新:后来被修复为preincrement。

答案 2 :(得分:3)

i++ % buffer.length版本不会有继续递增i的缺点,这可能会导致它达到某种max_int / max_long / max_whatever限制吗?

另外,我会把它分成

i = (i++ == buffer.length) ? 0 : i;
return i;

因为否则你很可能有一个错误。

答案 3 :(得分:3)

即使我们使用无符号类型,我更喜欢条件方法,模运算有缺点。当测试数量回滚到零时,使用模数会产生不良副作用

示例:

255 % 7 == 3

因此,如果您使用字节(无符号字符),例如,当数字在255之后滚动(即零)时,它将不会导致4.应该导致4(当256%7时),因此它正确旋转。因此,只需使用测试(ifternary operator)构造来确保正确性


如果要获得性能,如果数字是2的倍数(即2,4,8,16,32,64 ......),请使用&运算符。

因此,如果缓冲区长度为16,请使用:

n & 15

如果缓冲区长度为64,请使用63:

n & 63

即使数字回零,它们也能正确旋转。顺便说一下,如果数字是2的倍数,即使模/余值方法也适合账单,即它将正确旋转。但我可能会猜测&操作比%操作更快。

答案 4 :(得分:3)

第一个会给你一个ArrayIndexOutOfBoundsException,因为我实际上从未重置为0。

当i == Integer.MAX_VALUE时,第二个(可能)会给你一个溢出错误(或相关的不良影响)(在你的情况下可能不会实际发生,但这不是好习惯,恕我直言)。

所以我会说第二个是“更正确”,但我会使用类似的东西:

i = (i+1) % buffer.length;
return i;

我认为这两个问题都没有。

我继续和测试每个人的代码,并伤心地发现,只有以前的职位(在这篇文章的写作的时间)的作品之一。 (哪一个?试着全部找出来!你可能会感到惊讶!)

public class asdf {

    static int i=0;
    static int[] buffer = {0,1,2};

    public static final void main(String args[]){
        for(int j=0; j<5; j++){
            System.out.println(buffer[getIndex()]);
        }
    }

    public static int getIndex(){
//      return (++i == buffer.length) ? 0: i;
//      return ++i % buffer.length;

//      i = (i++ == buffer.length) ? 0 : i;
//      return i;

//      i++;
//      if (i >= buffer.length) 
//      {
//        i = 0;
//      }
//      return i;

//      return (i+1 == buffer.length) ? 0 : i+1;

        i = (i+1) % buffer.length;
        return i;
    }

}

预期产量为: 1 2 0 1 2

如果我的编码错误而且我不小心侮辱某人,请提前道歉! x.x中

PS:前一条关于不使用后期增量进行平等检查的评论+1(我实际上还不能upmod发布= /)

答案 5 :(得分:2)

我认为第二种解决方案具有明显的优势,即它有效,而第一种解决方案没有。当i变得大于buffer.length时,第一个解决方案将始终返回零,因为i永远不会重置。

模运算符没有缺点。

答案 6 :(得分:2)

使用if:

肯定会更具可读性
i++;
if (i >= buffer.length) 
{
  i = 0;
}
return i;

如果buffer.length发生变化,则取决于该位。

答案 7 :(得分:1)

这是非常主观的,取决于您的同事习惯看到的内容。我个人更喜欢第一个选项,因为它明确表达了代码的作用,即如果达到缓冲区长度,则重置为0.您不必执行任何数学思考,甚至​​不知道模数的作用(当然,应该!:)

答案 8 :(得分:1)

就个人而言,我更喜欢模数方法。当我看到模数时,我立即想到范围限制和循环,但是当我看到三元运算符时,我总是想更仔细地思考它,因为有更多的术语要看。可读性是主观的,正如您在标记中已经指出的那样,我怀疑大多数人会不同意我的看法。

然而,表现并非主观。 Modulo意味着一个divison操作,它通常比对零的比较慢。显然,这在Java中更难确定,因为在抖动开始之前我们不会编译本机代码。

我的建议是写出哪些认为最合适(只要它有效!)并让同事(假设你有一个)来评估它。如果他们不同意,请问另一位同事 - 然后以多数票投票。 #codingbydemocracy

答案 9 :(得分:1)

值得注意的是,如果我们的缓冲区具有2的幂的长度,那么非常有效的位操作将起作用:

idx = (idx + 1) & (length - 1)

答案 10 :(得分:0)

我更喜欢模运算符,原因很简单。任何程序都应该能够以模数进行梦想,因为它几乎和加号运算符一样常见。

答案 11 :(得分:0)

您也可以使用位操作:

idx = idx & ((idx-length)>>31)

但它并不比我机器上的if-variant快。

以下是一些比较C#中运行时间的代码:

Stopwatch sw = new Stopwatch();
long cnt = 0;
int k = 0;
int modulo = 10;

sw.Start();
k = 0;
cnt = 0;
for ( int j=0 ; j<100000000 ; j++ ) {
    k = (k+1) % modulo;
    cnt += k;
}
sw.Stop();
Console.WriteLine( "modulo cnt=" + cnt.ToString() + "     " + sw.Elapsed.ToString() );
sw.Reset();


sw.Start();
k = 0;
cnt = 0;
for (int j = 0; j < 100000000; j++) {
    if ( ++k == modulo )
        k = 0;
    cnt += k;
}
sw.Stop();
Console.WriteLine( "if     cnt=" + cnt.ToString() + "     " + sw.Elapsed.ToString() );
sw.Reset();

sw.Start();
k = 0;
cnt = 0;
for (int j = 0; j < 100000000; j++) {
    ++k;
    k = k&((k-modulo)>>31);
    cnt += k;
}
sw.Stop();
Console.WriteLine( "bit    cnt=" + cnt.ToString() + "     " + sw.Elapsed.ToString() );

输出:

modulo cnt=450000000     00:00:00.6406035
if     cnt=450000000     00:00:00.2058015
bit    cnt=450000000     00:00:00.2182448