Python Ruby 等语言弃用自增运算符原因剖析

目录
  • 正文
  • 为什么会存在自增自减运算符?
    • 起源
    • 提高程序运行效率?原子性?
    • 简洁性
  • 为什么一些现代编程语言取消了自增自减运算符?
    • 副作用
    • 迭代器替代了大多数自增自减运算符的使用场景
    • 赋值语句返回值的消失
    • 想要获取下标怎么办?
    • 运算符重载带来歧义
    • 一些其他的讨论
  • 总结

正文

许多人也许会注意到一个现象,那就是在一些现代编程语言(当然,并不是指“最近出现”的编程语言)中,自增和自减运算符被取消了。也就是说,在这些语言中不存在i++j--这样的表达,而是只存在i += 1j -= 1这样的表达方式了。本回答将从设计哲学这个角度上探讨这一现象产生的背景与原因。

严格来说,说"i++正在消失"也许有失偏颇,因为主流编程语言中似乎只有Python、Rust和Swift不支持自增自减运算符。

当我第一次接触Python时,这也曾令我感到困惑。我曾经有兴趣地搜索了很多相关的回答和文章,但都没有得到满意的答案。如今数年过去了,我尝试重新思考这个问题,并给出我的答案。

请注意,本文仅“从设计哲学上”讨论这一问题,不会特别涉及语言本身的性质。例如在Python中,不提供自增自减运算符很大一部分原因是由于其整数类型为 Immutable 的,但这并不是“从设计哲学上”的讨论,因此本文不会包含相关内容。

为什么会存在自增自减运算符?

起源

维基百科指出,自增和自减运算符最早出现在B语言(即C的前身)中。B语言的发明者与C语言的发明者相同,也是K&R,其中Ken Thompson最早在B语言中引入了自增与自减运算符。因此也常常有人不太严谨地说“自增自减运算符最早起源于C”,事实情况虽然有些出入,但也差不了太多。

B语言的语法与C高度相似,最大的不同可能在于B是无类型的。不过,这里不太多介绍B语言,否则就偏离主题了。这里所要强调的只是自增自减运算符最早的起源。

关于为什么B语言中引入了自增自减运算符这个问题众说纷纭,Ken Thompson也从未公开表示过自己当初为何创建了这两个运算符。然而,有一个误解需要澄清,即这两个运算符的引入不可能是对应于汇编语言的INCDEC指令。事实上,B语言的另一位创造者(当然,也是C语言的创造者)Dennis M. Ritchie曾在其回忆"The Development of the C Language"中指出:

……Thompson went a step further by inventing the ++ and -- operators, which increment or decrement; their prefix or postfix position determines whether the alteration occurs before or after noting the value of the operand. They were not in the earliest versions of B, but appeared along the way. People often guess that they were created to use the auto-increment and auto-decrement address modes provided by the DEC PDP-11 on which C and Unix first became popular. This is historically impossible, since there was no PDP-11 when B was developed.  The PDP-7, however, did have a few 'auto-increment' memory cells, with the property that an indirect memory reference through them incremented the cell. This feature probably suggested such operators to Thompson; the generalization to make them both prefix and postfix was his own. Indeed, the auto-increment cells were not used directly in implementation of the operators, and a stronger motivation for the innovation was probably his observation that the translation of ++x was smaller than that of x=x+1.

文中的说法有些模糊,仅指出自增自减运算符不可能是产生于PDP-11的auto-increment和auto-decrement地址模式(因为B语言发明时这台机器甚至都不存在),然而并未指出其是否对应于汇编语言中的INCDEC。为了验证这一说法,我找到了文中提到的PDP-7的指令集,的确不包含INCDEC指令。为了严谨起见,我还查了一下PDP-7的汇编手册,也没有找到相关指令。这证明了自增自减运算符的发明不可能是由于其直接对应于汇编语言中的INC和DEC指令

顺带一提,为了考证INC和DEC汇编指令的最初出现时间,我找到了1969年版的PDP-11 Handbook, 其中指出了INC和DEC是在PDP-11中被新引入的汇编指令(截图中没包含DEC,但手册后面有包含这条指令):

PDP-11 Handbook, 1969, Page 34

PDP-11的正式发布时间是1970,而B语言的诞生时间是1969。除非Ken Thompson参与了PDP-11的早期开发工作,否则自增自减运算符的灵感不可能源于INCDEC汇编指令。当然,正如Dennis Ritchie指出,早在PDP-7中就已经出现了auto-increment memory cells,很可能是它启发了Ken Thompson引入自增自减运算符

另一个能够反驳“自增自减运算符直接对应于汇编指令”的事实是,B语言最初并不能直接编译成机器码,而是需要编译成一种被称作“线程码(threaded code)”的东西(原谅我找不到合适的翻译) 。既然最初都无法直接编译成机器码,那就更没有这种说法了。

所以说,自增自减运算符最初出现的原因可能非常简单——当年机器字节很珍贵,而++x能比x=x+1或x+=1少写一点代码,在那时候能少写一点代码总是好的——于是自增自减运算符出现了

提高程序运行效率?原子性?

好吧,虽然上面已经严肃地论证了自增自减运算符的出现与PDP-11的ISA没关系,但K&R不过是C的创始人,他们懂什么C语言(雾)?K&R之后C语言的各种语法都被玩出花来了,恐怕他们也想不到C语言后续的发展。自增自减运算符到底会不会被编译成INCDEC,还得看现代的各种编译器。下面我在Ubuntu 22.04下将相关的C代码编译,然后反汇编,看看i++是否会被编译成INC,以验证“自增自减运算符能够提高程序运行效率”的逻辑是否成立。

下面是测试程序:

// incr_test.c
#include <stdio.h>
int main(void)
{
    for (int i = 0; i < 5; i++)
    {
        printf("%d", i);
    }
    return 0;
}

然后运行gcc,默认不开启优化:

gcc -o incr_test incr_test.c

然后运行objdump反汇编:

objdump -d incr_test.c

下面展示相关汇编代码(我所使用的是x86-64平台),已剔除无关代码:

0000000000001149 <main>:
    1149:       f3 0f 1e fa             endbr64
    114d:       55                      push   %rbp
    114e:       48 89 e5                mov    %rsp,%rbp
    1151:       48 83 ec 10             sub    $0x10,%rsp
    1155:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
    115c:       eb 1d                   jmp    117b <main+0x32>
    115e:       8b 45 fc                mov    -0x4(%rbp),%eax
    1161:       89 c6                   mov    %eax,%esi
    1163:       48 8d 05 9a 0e 00 00    lea    0xe9a(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
    116a:       48 89 c7                mov    %rax,%rdi
    116d:       b8 00 00 00 00          mov    $0x0,%eax
    1172:       e8 d9 fe ff ff          call   1050 <printf@plt>
    1177:       83 45 fc 01             addl   $0x1,-0x4(%rbp)
    117b:       83 7d fc 04             cmpl   $0x4,-0x4(%rbp)
    117f:       7e dd                   jle    115e <main+0x15>
    1181:       b8 00 00 00 00          mov    $0x0,%eax
    1186:       c9                      leave
    1187:       c3                      ret

可以看到,默认情况下并没有调用inc,仍然使用了 addl。

有人肯定要问了,是不是没有开优化的原因?好,那就开优化试试:

gcc -o incr_test incr_test.c -O1
objdump -d incr_test.c

这次把addl改成了add,但inc还是没出现:

0000000000001149 <main>:
    1149:       f3 0f 1e fa             endbr64
    114d:       55                      push   %rbp
    114e:       53                      push   %rbx
    114f:       48 83 ec 08             sub    $0x8,%rsp
    1153:       bb 00 00 00 00          mov    $0x0,%ebx
    1158:       48 8d 2d a5 0e 00 00    lea    0xea5(%rip),%rbp        # 2004 <_IO_stdin_used+0x4>
    115f:       89 da                   mov    %ebx,%edx
    1161:       48 89 ee                mov    %rbp,%rsi
    1164:       bf 01 00 00 00          mov    $0x1,%edi
    1169:       b8 00 00 00 00          mov    $0x0,%eax
    116e:       e8 dd fe ff ff          call   1050 <__printf_chk@plt>
    1173:       83 c3 01                add    $0x1,%ebx
    1176:       83 fb 05                cmp    $0x5,%ebx
    1179:       75 e4                   jne    115f <main+0x16>
    117b:       b8 00 00 00 00          mov    $0x0,%eax
    1180:       48 83 c4 08             add    $0x8,%rsp
    1184:       5b                      pop    %rbx
    1185:       5d                      pop    %rbp
    1186:       c3                      ret

至于更高的优化级别,其汇编代码的可读性太差,就不贴出来了。但经过验证,即使是O3甚至Ofast优化级别的汇编代码中都看不到inc的身影。也许在某些特殊的情况下i++会被编译成inc,但是如果要指望编译器将i++编译成inc这样的单指令以提高速度(其实inc甚至不是atomic的,因此也不要指望这能带来什么“原子性” ),那确实是想当然了。事实上对于gcc来说,i++i += 1没什么区别。

这会不会是gcc的问题?用clang会不会产生不一样的结果?答案是同样不会。

clang -o incr_test incr_test.c
objdump -d incr_test

结果:

0000000000001140 <main>:
    1140:       55                      push   %rbp
    1141:       48 89 e5                mov    %rsp,%rbp
    1144:       48 83 ec 10             sub    $0x10,%rsp
    1148:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
    114f:       c7 45 f8 00 00 00 00    movl   $0x0,-0x8(%rbp)
    1156:       83 7d f8 05             cmpl   $0x5,-0x8(%rbp)
    115a:       0f 8d 1f 00 00 00       jge    117f <main+0x3f>
    1160:       8b 75 f8                mov    -0x8(%rbp),%esi
    1163:       48 8d 3d 9a 0e 00 00    lea    0xe9a(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    116a:       b0 00                   mov    $0x0,%al
    116c:       e8 bf fe ff ff          call   1030 <printf@plt>
    1171:       8b 45 f8                mov    -0x8(%rbp),%eax
    1174:       83 c0 01                add    $0x1,%eax
    1177:       89 45 f8                mov    %eax,-0x8(%rbp)
    117a:       e9 d7 ff ff ff          jmp    1156 <main+0x16>
    117f:       31 c0                   xor    %eax,%eax
    1181:       48 83 c4 10             add    $0x10,%rsp
    1185:       5d                      pop    %rbp
    1186:       c3                      ret

同理,对于clang,各种优化级别我也试过了,都见不到inc的影子。

简洁性

上面的考证似乎有些太过分了,以至于稍微有些偏离了“从设计哲学上讨论”的初衷。上面讨论了这么多,只是为了证明自增自减运算符真的不能带来什么性能提升,在设计之初这两个运算符就没考虑过这方面的问题,而且出于各种原因,现代编译器也几乎不会把i++编译成inc(事实上,只有在非常陈旧的编译器中才会出现这样的情况,参见StackOverflow) 。而且,由于incdec并非原子指令,这也不能给程序带来任何“原子性”。

好吧,话题终于回归到“设计哲学”上了。现在已经排除了一切“为了性能/为了原子性/为了直接对应汇编语言……”而使用自增自减运算符的说法,这些更多是想当然的看法,而非事实。显然,那么答案只有从设计哲学上考虑了。

对于C/C++程序员,for循环语句是一个很得心应手的工具。C语言(甚至B语言)并非最早引入由分号分隔的for循环的语言,但却是真正将其推广开来的语言。而自增自减操作符的引入,使得for循环变得极其强大,甚至许多C/C++程序员习惯到尽可能将代码压缩到一个以分号结尾的for循环语句(或while循环语句)中,使代码极为简洁。最初接触这些形式代码的程序员可能还不太习惯,但若看多了类似的写法,其实可以发现这些写法也非常简洁明白:

for(vector<int>::iterator iter = vec.begin(); iter != vec.end(); add(*(iter++)));
for(size_t i = 0; arr[i] == 0; i++);
while(v->data[i++] > 5);
while(--i) { ... }

有些C/C++程序员认为这类传统for循环比起许多现代语言中采用迭代器的for更有优势,也更具表达能力。此外,由于C/C++中无法直接在数组中使用迭代器(不像Java后来可以加入迭代数组的语法糖),指针的递增和递减操作使用非常频繁,也相当重要,因此提供自增自减运算符无疑是很符合C/C++的设计哲学的。

为什么一些现代编程语言取消了自增自减运算符?

事先声明,就像上面已经说过的,在C++中(甚至是任何采用传统for循环的语言中)可以认为自增自减运算符是利大于弊的,它使得代码变得更为简洁。而且在谨慎使用的前提下,也可能使得代码更加清晰。判断一个语法特性是否是个好设计,显然要看环境。这里只是指在许多精心设计的现代编程语言中,自增自减运算符似乎显得没那么重要了。

副作用

可以注意到,在许多编程语言中,具有副作用的操作符除了赋值操作符(包括但不限于=、+=、&=等),就只有自增和自减运算符了。显然,赋值操作符具有副作用是无奈之举,否则无法给变量赋值。但在一众其他操作符,如+、-、&、||、<<中,唯独自增和自减运算符这两个具有副作用,会原地改变变量值,就显得十分奇怪。即使是三元运算符?:,其本身也不会产生副作用。

副作用的负面影响想必大家或多或少都在关于函数式编程的讨论中能听到一些。显然,纯函数是易于测试和组合的,对于相同的参数,纯函数每次运算都得到相同的结果。而自增和自减运算符从语法设计上就大大违背了函数式编程的不变性原则。其实可以看到,排除不存在变量的纯函数式语言中不存在自增自减运算符,其实许多包含变量的混合范式(且偏向函数式)的编程语言也不存在自增自减运算符。除了文章一开头提到的Python、Rust和Swift,在其他偏函数式的混合范式语言如Scala中,也不原生存在自增自减运算符。

在一众运算符中,自增与自减运算符总因其具有副作用而显得独树一帜。对于重视函数式编程的语言来说,自增自减运算符是弊大于利的,也是很难被接受的。可以想象,若有人尝试在混合范式语言中写函数式的代码,然后因为某些原因其中混进了一个i++,那恐怕是想找到BUG原因都很困难的——相比起i += 1i++看起来确实太隐晦了,很难在杂乱的代码中一眼看出这是个赋值语句,认识到其有副作用的事实,这可能导致潜在的BUG。

迭代器替代了大多数自增自减运算符的使用场景

近年来,似乎但凡是个新语言,都会优先采用迭代式循环而非C-style的传统for循环。即使像是Go这种复古语法的语言,也推荐优先使用range而非传统for循环。而Rust更是直接删除了传统for循环,只保留迭代式for循环。即使是那些老语言,也纷纷加入了迭代式循环,如Java、JavaScript、C++等,都陆续加入了相关语法。

简单对比一下各语言中的传统for循环和迭代式循环:

Java

int[] arr = { 1, 2, 3, 4, 5 };
// 传统计数循环
for (int i = 0; i < arr.length; i++) {
    System.out.println(arr[i]);
}
// 迭代
for (int num: arr) {
    System.out.println(num);
}

JavaScript

const arr = [1, 2, 3, 4, 5]
// 传统计数循环
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i])
}
// 迭代
for (const num of arr) {
  console.log(num)
}

Go

arr := [5]int{1, 2, 3, 4, 5}
// 传统计数循环
for i := 0; i < len(arr); i++ {
 fmt.Println(arr[i])
}
// 迭代
for _, num := range arr {
 fmt.Println(num)
}

可以很明显地看到,使用迭代器减少了代码量,而且反而使得代码变得更加清晰。

当然,迭代器的作用不仅停留在表面的“减少代码”上。更重要的是迭代器减小了开发人员的心智负担。有过C/C++编程经验的人都知道,在传统for循环中更改i的值是非常危险的,一不留神就会造成严重的BUG甚至产生死循环。

而迭代器的逻辑是不同的:每次循环从迭代器中取出值,而不是在某个值上递增。因此,即使不小心在使用迭代器的循环中错误更改了计数变量的值,也不会产生问题:

for i in range(5):
    i -= 1

上面这段Python代码会是一个死循环吗?其实不会。因为for i in range(5)的逻辑并非创建一个计数变量i,然后每次递增。其实现方式是先创建迭代器<range {0, 1, 2, 3, 4}>,然后依次从里面取值。i的取值在最初就已经固定了,因此在循环体中更改i的值并不会造成什么影响,到下一次循环时,i只是取迭代器中的下一个值,不管在上一次循环中有没有更改。当然,上面这样的代码是不建议在生产环境中编写的,容易造成误会。

可以看到,在现代编程语言中,迭代器替代了自增自减运算符绝大多数的使用场景,而且能够使得代码更加简洁与清晰。而对于那些只存在迭代式for循环的编程语言,如Python、Rust等,自然也就不那么必要加入自增自减运算符了。

赋值语句返回值的消失

熟悉C/C++的程序员肯定知道,赋值语句是有返回值的,也可以时常看到C/C++程序员写出下面这样的代码(Java中也可以实现这样的操作,但似乎Java程序员不太喜欢写这样的代码):

int a = 1, b = 2, c = 3;
a = (b += 3);

赋值语句的返回值即被赋值变量执行赋值语句之后的值。在上面的例子中,a最终等于5.

为什么赋值语句会有返回值,而不是返回一个null或者其他类似的东西?这很大程度上是为了满足连续赋值的需要:

int a = 1, b = 2, c = 3;
a = b = c = 5;

上面的代码中,a = b = c = 5这句似乎太符合直觉,以至于人们常常忘记类似的连续赋值语句并非语法糖,而是赋值语句返回值的必然结果。赋值操作符是右结合的,因此上面这条语句先执行c = 5,然后返回5,再执行b = 5,以此类推,就实现了连续赋值。

在很多现代语言中,赋值语句都没有了返回值,或者其返回值只用于实现连续赋值,不允许作为表达式使用。例如在Go中,类似的语句就会报错,它甚至不支持连续赋值:

var a = 1
var b = 2
var c = 3
a = b = c = 5 // 报错

在Go中,赋值语句不能作为表达式,也自然没有赋值语句。同理,在Rust、Python等语言中,赋值语句也仅仅是“语句”而已,不能作为表达式使用,像是a = (b += c)这样的语句是不合法的。

不过,Python虽然不支持赋值语句作为表达式,但却是支持连续赋值的,像是a = b = c这样的语句是合法的。然而在这里,连续赋值就不是赋值语句返回值产生的自然结果了,在这里它确实是某种“语法糖”。

不过,有时候赋值表达式也不完全是一件坏事,它在特定情况下能够简化代码,使其更加清晰。例如在Python 3.8中,就加入了赋值表达式语法,使用“海象操作符(:=)”作为赋值表达式。例如:

found = {name: batches for name in order
         if (batches := get_batches(stock.get(name, 0), 8))}

……话题似乎有些扯远了,赋值语句返回值和自增自减运算符有什么关系?其实稍微想一想,就会发现它们之间有很强的关联性:自增自减运算虽然看起来不像赋值语句,但其本质上确实是赋值。既然赋值语句都没了返回值,不能作为表达式使用,那么自增自减运算符理论上也不该例外,也不该当作表达式使用。

可是若自增自减运算只能当作普通的赋值语句使用,那么就几乎只能i++j--等语句单独成行了。而实际上,自增自减运算符更多的使用场景是作为表达式而非语句使用。这样一来,自增自减运算符的使用场景就变得非常有限了,而在本身已经存在迭代式循环的语言中,要使自增自减运算符单独成行使用的场景本就很罕见,那么加入自增自减运算符自然就显得没什么意义了。

当然,也存在例外。例如在Go中自增自减运算符也不是真正的“运算符”,而仅仅是赋值语句的语法糖,还真就只能单独成行使用。但Go就是任性地把它们加入到了语法中。例如下面的Go代码就会在编译时报错:

i := 0
j := i++

不过,Go选择保留自增自减运算符也并非毫无道理。毕竟Go中仍保留了C-Style的传统for循环,而for i := 0; i < len(arr); i++看起来还是要比for i := 0; i < len(arr); i += 1稍微简洁一些,因此就保留了它们。如果Go选择删除传统for循环,那大概率自增自减运算符就不复存在了。(虽然我个人认为其实现在自增自减运算符在Go中也没有太大存在价值)

想要获取下标怎么办?

至此为止,自增自减运算符的大多数使用场景似乎已经被各种更现代的语法替代了。但似乎自增自减运算符还有一个很小的优势,就是可以简化单独成行的i += 1 或j -= 1这样的赋值语句。比如说,需要在迭代数组的同时获得下标,那么i++是否能做到简化代码?

答案是不能,因为各大语言其实很早就考虑过这个问题了。比如在Python中,没经验的新手程序员可能会写出这样的代码,然后抱怨Python中为什么没有自增自减运算符:

lst = ['a', 'b', 'c', 'd', 'e']
i = 0
for c in lst:
    print(i, c)
    i += 1

或是写出这样的代码:

lst = ['a', 'b', 'c', 'd', 'e']
for i in range(len(lst)):
    c = lst[i]
    print(i, c)

然而Python早就提供了enumerate函数用来解决这个问题,该函数会返回一个每次返回下标和元素的可迭代对象:

lst = ['a', 'b', 'c', 'd', 'e']
for i, c in enumerate(lst):
    print(i, c)

类似地,Go也可以在迭代时直接获取数组下标:

arr := [5]int{1, 2, 3, 4, 5}
for i, num := range arr {
 fmt.Println(i, num)
}

在Swift中也一样:

let arr: [String] = ["a", "b", "c", "d"]
for (i, c) in arr.enumerated() {
    print(i, c)
}

在Rust中:

let arr = [1, 2, 3, 4, 5];
for (i, &num) in arr.iter().enumerate() {
    println!("arr[{}] = {}", i, num);
}

在C++中并没有直接包含类似enumerate的语法,这个函数写起来其实也比较困难,但善用模板元编程也是可以实现的,感兴趣可以自己试试。

显然,在大多数包含迭代式循环语法的语言中,要在迭代对象的同时获取下标也是相当轻松的。即使那门语言中没有类似Python中enumerate的语法,手写一个类似的函数也没有那么困难。

于是,自增自减运算符的使用场景被进一步压缩,现在即使是作为纯粹的语法糖当作单独成行的i += 1j -= 1使用,好像也没太多使用场景了。

运算符重载带来歧义

一般来说,自增和自减运算符都应视作与+= 1-= 1同义 。然而,运算符重载使其产生了某些歧义。

若一门语言支持运算符重载,那么对于+=++,有两种处理方法:

第一种,将++完全视作+= 1的语法糖。当重载+=运算符时,也自动重载++运算符。然而这会带来很严重的歧义,例如Python就重载了字符串上的+=运算符,如运行x = 'a'; x += 'b' 后,x的值为'ab'。如果Python中存在++运算符,那么按照这一规则,x++就应被视为x += 1,现在这还没问题,会报类型不匹配错误。但是若Python像Java一样在拼接字符串时会自动进行类型转换,x += 1就变得合法了,同x += '1',然后运行x++,x的值就会变成'ab1',这就极其匪夷所思了。

考虑一下在弱类型语言中这将产生什么样的灾难性后果,JS现在即使没有运算符重载都能写出let a = []; a++然后a的值为0这种黑魔法代码了。如果JS哪天加入了运算符重载,然后有人闲着没事去重载了内置类型上的+=运算符,那后果简直有点难以想象了。

第二种,将++视作与+=无关的操作符。这样做不会产生上面描述中那样匪夷所思的问题,但若选择这么做,当编程语言的使用者重载了+=运算符后,可能会自然而然地认为++运算符也被重载了,这可能带来更多歧义。

事实上,这里提到的运算符重载带来的歧义已经在很多语言中发生了。在同时支持自增自减运算符和操作符重载的语言中,由于类似原因产生的BUG已经并不少见了。一种解决方案是不允许重载++--操作符,只允许它们在整数类型上使用。但既然这样了,为什么不考虑干脆去掉自增自减运算符呢?

一些其他的讨论

可以注意到,在上面的讨论中,我有意忽视了许多语言本身的特性,例如在Python中,不存在自增自减运算符的另一大原因是因其整数是不可变类型,自增自减运算符容易带来歧义。

正如我在文章开头所说的,这属于Python的特性,不在这里的“设计哲学”讨论范畴内。不过,为了严谨起见,这里还是简单提一下。

此外,尽管在许多语言中,a = a + 1a += 1a++代表的意义都是相同的,但也存在不少语言区分这两者。在很多使用虚拟机的语言,如Python和Java中,a += 1作为原地操作与a = a + 1区别开来的。例如在Java中,a = a + 1使用字节码iadd实现,而a += 1a++使用iinc实现。

同理,在Python中,它们的字节码也有BINARY_ADD和INPLACE_ADD的区分。对于这些语言,a++到底表示a += 1还是a = a + 1,由于它们含义不同,或许又会产生一重歧义。

总结

不得不说,Ken Thompson最初一拍脑袋想出来的++--运算符产生的影响恐怕远远超出了本人的预料。许多人对自增和自减运算符起源和应用场景的理解也仅仅是停留在想当然的层面,诸如“提高运行效率”甚至“原子性操作”这样的误解也是满天飞。同时,C语言初学者(尤其是在国内)也常常被a = i++ + ++i + i++这种逆天未定义操作折腾到头疼欲裂。这两个小小的运算符究竟是带来了更多方便还是带来了更多麻烦,就留给读者自己去思考吧。

在许多现代编程语言中,自增和自减运算符的地位都被大大削弱了。有些语言严格限制了这两个运算符的使用,不允许其作为表达式使用,如Go;有些干脆取消了这两个运算符,认为+=-=已经完全足够了,如Python和Rust。

在迭代器被越来越广泛使用的今天,++--这两个在历史上曾占据重要地位的运算符似乎正在逐渐淡出人们的视野。我很难评价这是件好事还是坏事,毕竟我们也见到在诸如C/C++和Java这样的语言中,克制地使用自增和自减运算符有些时候也能使代码非常简洁明白。像Python和Rust一样完全取消这两个运算符是否过于极端了?这也很不好说。

总而言之,不论你是一个很擅长使用++--的C/C++程序员,亦或是对这两个具有副作用的操作符天生厌恶的FP拥护者,都得承认随着程序设计语言的发展,自增和自减运算符正变得越来越不重要,但它们仍在特定场景下很有价值。

以上就是Python Ruby 等语言弃用自增运算符原因剖析的详细内容,更多关于Python Ruby自增运算符的资料请关注我们其它相关文章!

(0)

相关推荐

  • Python+MongoDB自增键值的简单实现

    背景 最近在写一个测试工具箱,里面有一个bug记录系统,因为后台我是用Django和MongoDB来实现的,就遇到了一个问题,要如何实现一个自增的字段. 传统的关系型数据库要实现起来是非常容易,只要直接设置一个自增字段就行了,插入数据时不用管这个键值,只管自己处理的数据就行了,会自动实现自增的功能,但是非关系型数据库好像没有这个功能(或者我不知道).百度之后发现都是MongoDB的设置方法,并不是我想要的. 解决思路 百度没有找到好的思路,那就只能自己解决了,我的想法很简单,字段不会自增,那么就

  • Python3实现自定义比较排序/运算符

    目录 自定义比较排序/运算符 1.cmp函数 2.重写类方法 Python3实现各种排序方法 自定义比较排序/运算符 Python3和Python2相比有挺多变化. 在Python2中可以直接写一个cmp函数作为参数传入sort来自定义排序,但是Python3取消了. 在这里总结一下Python3的自定义排序的两种写法,欢迎补充. 我们以二维空间中的点来作为待排序的数据结构,我们希望能先比较x后再比较y. class Pos:     def __init__(self, x = 0, y =

  • python mysql自增字段AUTO_INCREMENT值的修改方式

    在之前得文章中我们说过,如果使用delete对数据库中得表进行删除,那么只是把记录删除掉,并且id的值还会保持上次的状态. 即删除之前如果有四条数据,删除之后,再添加新的数据,id怎会从5开始. 但是我们显示想让id从2开始,应该怎么做呢? 这个时候我们就要学习去修改数据表的一些属性值了,而这个属性值就是AUTO_INCREMENT. 首先我们要知道怎么查看这个属性的值. 例如我建了一张表: create table t4(id int auto_increment primary key, n

  • python不等于运算符的具体使用

    Python not equal operator returns True if two variables are of same type and have different values, if the values are same then it returns False. 如果两个变量具有相同的类型并且具有不同的值 ,则Python不等于运算符将返回True :如果值相同,则它将返回False . Python is dynamic and strongly typed lan

  • python中and和or逻辑运算符的用法示例

    目录 一.概述 二.用法说明 (一)and 用法 (二)or 用法 三.两个集合and 和or操作的时候的问题 四.优先级问题(and > or) 附:python中if语句and和or用法总结 总结 一.概述 python中的逻辑操作符and 和or,也叫惰性求值,由于是惰性,只要确定了值就不往后解析代码了. 二.用法说明 (一)and 用法 文字说明: 第一种情况(判断语句): 从右往左解析,只要第一个是False,就是False,后面的就不需要运算了,整个都是错误的,无论后面是正确还是错误

  • Ruby的运算符和语句优先级介绍

    Ruby 是一种表达能力很强的语言,这得意于它异常丰富的运算符和语法糖,虽然 Ruby 一直把最小惊讶原则作为它的哲学之一,但还是常常看到让人惊讶不已,难于理解的代码,这可能是因为对它运算符和语句优先级理解不透导致,今天就和大家聊一聊 Ruby 运算符和语句的优先级. 先看一句简单的代码,猜一猜它的输出是什么. 复制代码 代码如下: puts {}.class 很多人一定以为结果是 Hash,但实事上结果是空,不信可以在 irb 里试一试. 再看一段代码. 复制代码 代码如下: puts "5

  • Python Ruby 等语言弃用自增运算符原因剖析

    目录 正文 为什么会存在自增自减运算符? 起源 提高程序运行效率?原子性? 简洁性 为什么一些现代编程语言取消了自增自减运算符? 副作用 迭代器替代了大多数自增自减运算符的使用场景 赋值语句返回值的消失 想要获取下标怎么办? 运算符重载带来歧义 一些其他的讨论 总结 正文 许多人也许会注意到一个现象,那就是在一些现代编程语言(当然,并不是指“最近出现”的编程语言)中,自增和自减运算符被取消了.也就是说,在这些语言中不存在i++或j--这样的表达,而是只存在i += 1或j -= 1这样的表达方式

  • 放弃 Python 转向 Go语言有人给出了 9 大理由

    转用一门新语言通常是一项大决策,尤其是当你的团队成员中只有一个使用过它时.今年 Stream 团队的主要编程语言从 Python 转向了 Go.本文解释了其背后的九大原因以及如何做好这一转换. 一.为什么使用 Go 原因 1:性能 Go 极其地快.其性能与 Java 或 C++相似.在我们的使用中,Go 一般比 Python 要快 30 倍.以下是 Go 与 Java 之间的基准比较: 原因 2:语言性能很重要 对很多应用来说,编程语言只是简单充当了其与数据集之间的胶水.语言本身的性能常常无关轻

  • python属于解释语言吗

    Python是一门解释型语言? Python是一门解释性语言,我就这样一直相信下去,直到发现了*.pyc文件的存在. 如果是解释型语言,那么生成的*.pyc文件是什么呢?c应该是compiled的缩写才对啊! 为了防止其他学习Python的人也被这句话误解,那么我们就在文中来澄清下这个问题,并且把一些基础概念给理清. python并非完全是解释性语言,它是有编译的,先把源码py文件编译成pyc或者pyo,然后由python的虚拟机执行,相对于py文件来说,编译成pyc和pyo本质上和py没有太大

  • 为什么称python为胶水语言

    什么是胶水语言? 胶水语言(glue language)是用来连接软件组件的程序设计语言(通常是脚本语言). 胶水语言的例子: Shell scripts Python Ruby Lua Tcl Perl PHP 参见: Category:脚本语言 Glue language (uses OS commands, interfaces, DLLs, shared libraries, services, objects, etc.) 胶水语言:使用输入输出命令,接口,动态链接库,静态链接库,服务

  • python 调用c语言函数的实例讲解

    虽然python是万能的,但是对于某些特殊功能,需要c语言才能完成.这样,就需要用python来调用c的代码了 具体流程: c编写相关函数 ,编译成库 然后在python中加载这些库,指定调用函数. 这些函数可以char ,int, float, 还能返回指针. 以下示例: 通过python调用c函数,返回"hello,world 字符串" 新建c语言文件 hello.c touch hello.c #include <stdio.h> char *get_str() {

  • 浅谈python和C语言混编的几种方式(推荐)

    Python这些年风头一直很盛,占据了很多领域的位置,Web.大数据.人工智能.运维均有它的身影,甚至图形界面做的也很顺,乃至full-stack这个词语刚出来的时候,似乎就是为了描述它. Python虽有GIL的问题导致多线程无法充分利用多核,但后来的multiprocess可以从多进程的角度来利用多核,甚至affinity可以绑定具体的CPU核,这个问题也算得到解决.虽基本为全栈语言,但有的时候为了效率,可能还是会去考虑和C语言混编.混编是计算机里一个不可回避的话题,涉及的东西很多,技术.架

  • Python与R语言的简要对比

    数据挖掘技术日趋成熟和复杂,随着互联网发展以及大批海量数据的到来,之前传统的依靠spss.SAS等可视化工具实现数据挖掘建模已经越来越不能满足日常需求,依据美国对数据科学家(data scientist)的要求,想成为一名真正的数据科学家,编程实现算法以及编程实现建模已经是必要条件:目前很多从事数据挖掘工作的人,大多都是出身非计算机专业,本身对编程基础比较低,所以找到一门快速上手而又高效的编程语言是至关重要的,好的工具和编程语言可以起到事半功倍的效果. 目前在数据挖掘算法方面用的最多的编程语言有

  • Python调用C语言的方法【基于ctypes模块】

    本文实例讲述了Python调用C语言的方法.分享给大家供大家参考,具体如下: Python中的ctypes模块可能是Python调用C方法中最简单的一种.ctypes模块提供了和C语言兼容的数据类型和函数来加载dll文件,因此在调用时不需对源文件做任何的修改.也正是如此奠定了这种方法的简单性. 示例如下 实现两数求和的C代码,保存为add.c //sample C file to add 2 numbers - int and floats #include <stdio.h> int add

  • python链接oracle数据库以及数据库的增删改查实例

    初次使用python链接oracle,所以想记录下我遇到的问题,便于向我这样初次尝试的朋友能够快速的配置好环境进入开发环节. 1.首先,python链接oracle数据库需要配置好环境. 我的相关环境如下: 1)python:Python 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 17:26:49) [MSC v.1900 32 bit (Intel)] on win32 2)oracle:11.2.0.1.0 64bit.这个是server版本号,在链接oracle

  • python和c语言的主要区别总结

    Python可以说是目前最火的语言之一了,人工智能的兴起让Python一夜之间变得家喻户晓,Python号称目前最最简单易学的语言,现在有不少高校开始将Python作为大一新生的入门语言.本萌新也刚开始接触Python,发现Python与其他语言确实有很大的区别.Python是由C语言实现的,因此想把Python与C语言做一个简单的比较. 1.语言类型 Python是一种基于解释器的语言,解释器会逐行读取代码:首先将Python编译为字节码,然后由大型C程序解释. C是一种编译语言,完整的源代码

随机推荐