> 백엔드 개발 > 파이썬 튜토리얼 > Python, Ruby 및 기타 언어에서 증가 연산자를 더 이상 사용하지 않는 이유는 무엇입니까?

Python, Ruby 및 기타 언어에서 증가 연산자를 더 이상 사용하지 않는 이유는 무엇입니까?

王林
풀어 주다: 2023-05-11 16:37:06
앞으로
1221명이 탐색했습니다.

为什么 Python、Ruby 等语言弃用了自增运算符?

많은 사람들이 현상을 발견했을 수 있습니다. 즉, 일부 현대 프로그래밍 언어(물론 "최근" 프로그래밍 언어를 말하는 것이 아님)에서 증가 및 감소 연산자가 취소되는 현상을 발견했을 것입니다. 즉, 이러한 언어에는 존재하지 않습니다.​i++​​​또는​​j--​​​이러한 표현식은​​i += 1​​​또는 ​​j -= 1​​이러한 표현입니다. 이번 답변에서는 이러한 현상의 배경과 이유를 디자인 철학의 관점에서 살펴보겠습니다. ​i++​​​或​​j--​​​这样的表达,而是只存在​​i += 1​​​或​​j -= 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也从未公开表示过自己当初为何创建了这两个运算符。然而,有一个误解需要澄清,即这两个运算符的引入不可能是对应于汇编语言的​​INC​​和​​DEC​ 엄밀히 말하면 "i++가 사라지고 있다"고 말하는 것은 편견일 수도 있는데, 주류 프로그래밍 언어 중 Python, Rust, Swift만이 증가 및 감소 연산자를 지원하지 않는 것 같기 때문입니다.

🎜🎜이것은 제가 처음 Python을 접했을 때 저를 혼란스럽게 했습니다. 많은 관련 답변과 기사를 검색해보고 싶었으나 만족스러운 답변을 얻지 못했습니다. 몇 년이 지난 지금, 나는 이 질문에 대해 다시 생각해 보고 답을 생각해 내려고 노력했습니다. 🎜🎜이 문서에서는 이 문제를 "디자인 철학에서"만 논의할 뿐이며 언어 자체의 특성을 구체적으로 다루지는 않습니다. 예를 들어 Python에서 증가 및 감소 연산자가 제공되지 않는 이유 중 큰 부분은 정수 유형이 Immutable이기 때문이지만 이는 "설계 철학에서" 논의된 것이 아니므로 이 기사에서는 관련 내용을 포함하지 않습니다. . 🎜

증가 연산자와 감소 연산자가 있는 이유는 무엇인가요?

원산

🎜Wikipedia에서는 증가 및 감소 연산자가 B 언어에서 처음 등장했다고 지적합니다( C)의 전신이다. B 언어의 창시자는 C 언어의 창시자와 동일하며, 그 중 Ken Thompson은 B 언어의 증가 및 감소 연산자를 최초로 도입했습니다. 그래서 어떤 사람들은 "증가 연산자와 감소 연산자가 처음에는 C에서 유래했다"고 막연하게 말하기도 합니다. 실제 상황은 다소 다르지만 크게 다르지는 않습니다. 🎜🎜B 언어의 구문은 C와 매우 유사합니다. 가장 큰 차이점은 B가 유형이 없다는 점일 수 있습니다. 하지만 여기서는 B 언어에 대해 너무 많이 소개하지 않을 것입니다. 그렇지 않으면 주제에서 벗어날 것입니다. 여기서 강조되는 것은 증가 및 감소 연산자의 최초 기원일 뿐입니다. 🎜🎜B 언어에 증가 연산자와 감소 연산자가 도입된 이유에 대해서는 의견이 다릅니다. Ken Thompson은 이 두 연산자를 만든 이유를 공개적으로 밝힌 적이 없습니다. 그러나 명확히 해야 할 오해가 있습니다. 즉, 이 두 연산자의 도입은 어셈블리 언어​INC​​ ​​DEC​​ 명령. 실제로 B 언어의 또 다른 창시자(물론 C 언어의 창시자)인 Dennis M. Ritchie는 자신의 기억에서 "🎜C 언어의 발전🎜"을 다음과 같이 지적한 적이 있습니다.

……Thompson은 ​​++​​ 및 ​​--​​ 증가 또는 감소하는 연산자; 접두사 또는 접미사 위치는 피연산자의 값을 기록하기 전이나 후에 변경이 발생하는지 여부를 결정합니다. B의 초기 버전에는 없었지만 그 과정에서 나타났습니다. ​++​​ 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语言发明时这台机器甚至都不存在),然而并未指出其是否对应于汇编语言中的​​INC​​和​​DEC​​。为了验证这一说法,我找到了文中提到的PDP-7的指令集,的确不包含​​INC​​或​​DEC​​指令。为了严谨起见,我还查了一下PDP-7的汇编手册,也没有找到相关指令。这证明了自增自减运算符的发明不可能是由于其直接对应于汇编语言中的INC和DEC指令

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

为什么 Python、Ruby 等语言弃用了自增运算符?

PDP-11 Handbook, 1969, Page 34

PDP-11的正式发布时间是1970,而B语言的诞生时间是1969。除非Ken Thompson参与了PDP-11的早期开发工作,否则自增自减运算符的灵感不可能源于​​INC​​和​​DEC​사람들은 C와 Unix가 처음 대중화되었던 DEC PDP-11에서 제공하는 자동 증가 및 자동 감소 주소 모드를 사용하기 위해 만들어졌다고 추측하는 경우가 많습니다. B가 개발될 당시에는 PDP-11이 없었기 때문에 이것은 역사적으로 불가능합니다. 그러나 PDP-7에는 몇 개의 '자동 증가' 메모리 셀이 있었으며 이를 통한 간접 메모리 참조가 셀을 증가시키는 특성이 있었습니다. . 이 기능은 아마도 Thompson에게 그러한 연산자를 제안했을 것입니다. 접두사와 접미사를 모두 만드는 일반화는 그의 것이었습니다. 실제로 자동 증가 셀은 연산자 구현에 직접 사용되지 않았으며 혁신에 대한 더 강력한 동기는 아마도 ​​INC​​화​​DEC​​ .为了验证这一说법, 나는 我找到了文中提到的PDP-7적 指令集, 确不包含​​INC​​或​​DEC​​指令.为了严谨起见,我还查了一下PDP-7的汇编手册,也没有找到关指令.这证明了🎜自增自减运算符INC와 DEC指令🎜.🎜🎜顺带一提,为了考证INC와 DEC汇编指令的最初出现时间,我找到了1969年版的 PDP-11 Handbook, 其中指take了INC and DEC是는 PDP-11中被new引入的汇编指令(截图中没包含DEC,但手册后面有包含这条指令):🎜

为什么 Python、Ruby 等语言弃用了自增运算符?🎜

PDP-11 핸드북, 1969년, 34페이지🎜🎜PDP-11적정式发布时间是1970,而B语言的诞生时间是1969。除不Ken Thompson이 PDP-11을 사용하여 만든 제품입니다. 글꼴 크기: 12px; 배경색: rgba(0, 0, 0, 0.06); 패딩: 0px 2px; 테두리 반경: 6px; 줄 높이: 상속; 오버플로 랩: 중단 단어; text-indent: 0px;">​INC​​와​​DEC​​汇编指令。当然,正如Dennis Ritchie指流,🎜早在PDP-7中就已经流现了자동 증가 메모리 셀,很可能是它启发了Ken Thompson이 자영업자입니다.

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

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

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

好吧,虽然上面已经严肃地论证了自增自减运算符的出现与PDP-11的ISA没关系,但K&R不过是C的创始人,他们懂什么C语言(雾)?K&R之后C语言的各种语法都被玩出花来了,恐怕他们也想不到C语言后续的发展。

自增自减运算符到底会不会被编译成​​INC​​和​​DEC​​,还得看现代的各种编译器。下面我在Ubuntu 22.04下将相关的C代码编译,然后反汇编,看看​​i++​​是否会被编译成​​INC​​,以验证“自增自减运算符能够提高程序运行效率”的逻辑是否成立。

下面是测试程序:

// incr_test.c
#include <stdio.h>

int main(void)
{
for (int i = 0; i <p>然后运行gcc,默认不开启优化:</p>
<pre class="brush:plain;toolbar:false;">gcc -o incr_test incr_test.c
로그인 후 복사

然后运行objdump反汇编:

objdump -d incr_test.c
로그인 후 복사

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

0000000000001149 <main>:
1149: f3 0f 1e fa endbr64 
114d: 55push %rbp
114e: 48 89 e5mov%rsp,%rbp
1151: 48 83 ec 10 sub$0x10,%rsp
1155: c7 45 fc 00 00 00 00movl $0x0,-0x4(%rbp)
115c: eb 1d jmp117b <main>
115e: 8b 45 fcmov-0x4(%rbp),%eax
1161: 89 c6 mov%eax,%esi
1163: 48 8d 05 9a 0e 00 00lea0xe9a(%rip),%rax# 2004 <_io_stdin_used>
116a: 48 89 c7mov%rax,%rdi
116d: b8 00 00 00 00mov$0x0,%eax
1172: e8 d9 fe ff ffcall 1050 <printf>
1177: 83 45 fc 01 addl $0x1,-0x4(%rbp)
117b: 83 7d fc 04 cmpl $0x4,-0x4(%rbp)
117f: 7e dd jle115e <main>
1181: b8 00 00 00 00mov$0x0,%eax
1186: c9leave
1187: c3ret</main></printf></_io_stdin_used></main></main>
로그인 후 복사

可以看到,默认情况下并没有调用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: 55push %rbp
114e: 53push %rbx
114f: 48 83 ec 08 sub$0x8,%rsp
1153: bb 00 00 00 00mov$0x0,%ebx
1158: 48 8d 2d a5 0e 00 00lea0xea5(%rip),%rbp# 2004 <_io_stdin_used>
115f: 89 da mov%ebx,%edx
1161: 48 89 eemov%rbp,%rsi
1164: bf 01 00 00 00mov$0x1,%edi
1169: b8 00 00 00 00mov$0x0,%eax
116e: e8 dd fe ff ffcall 1050 <__printf_chk>
1173: 83 c3 01add$0x1,%ebx
1176: 83 fb 05cmp$0x5,%ebx
1179: 75 e4 jne115f <main>
117b: b8 00 00 00 00mov$0x0,%eax
1180: 48 83 c4 08 add$0x8,%rsp
1184: 5bpop%rbx
1185: 5dpop%rbp
1186: c3ret</main></__printf_chk></_io_stdin_used></main>
로그인 후 복사

至于更高的优化级别,其汇编代码的可读性太差,就不贴出来了。但经过验证,即使是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: 55push %rbp
1141: 48 89 e5mov%rsp,%rbp
1144: 48 83 ec 10 sub$0x10,%rsp
1148: c7 45 fc 00 00 00 00movl $0x0,-0x4(%rbp)
114f: c7 45 f8 00 00 00 00movl $0x0,-0x8(%rbp)
1156: 83 7d f8 05 cmpl $0x5,-0x8(%rbp)
115a: 0f 8d 1f 00 00 00 jge117f <main>
1160: 8b 75 f8mov-0x8(%rbp),%esi
1163: 48 8d 3d 9a 0e 00 00lea0xe9a(%rip),%rdi# 2004 <_io_stdin_used>
116a: b0 00 mov$0x0,%al
116c: e8 bf fe ff ffcall 1030 <printf>
1171: 8b 45 f8mov-0x8(%rbp),%eax
1174: 83 c0 01add$0x1,%eax
1177: 89 45 f8mov%eax,-0x8(%rbp)
117a: e9 d7 ff ff ffjmp1156 <main>
117f: 31 c0 xor%eax,%eax
1181: 48 83 c4 10 add$0x10,%rsp
1185: 5dpop%rbp
1186: c3ret</main></printf></_io_stdin_used></main></main>
로그인 후 복사

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

简洁性

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

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

对于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) { ... }</int>
로그인 후 복사

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

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

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

副作用

可以注意到,在许多编程语言中,具有副作用的操作符除了赋值操作符(包括但不限于=、+=、&=等),就只有自增和自减运算符了。显然,赋值操作符具有副作用是无奈之举,否则无法给变量赋值。

但在一众其他操作符,如+、-、&、||、

副作用的负面影响想必大家或多或少都在关于函数式编程的讨论中能听到一些。显然,纯函数是易于测试和组合的,对于相同的参数,纯函数每次运算都得到相同的结果。而自增和自减运算符从语法设计上就大大违背了函数式编程的不变性原则。

其实可以看到,排除不存在变量的纯函数式语言中不存在自增自减运算符,其实许多包含变量的混合范式(且偏向函数式)的编程语言也不存在自增自减运算符。除了文章一开头提到的Python、Rust和Swift,在其他偏函数式的混合范式语言如Scala中,也不原生存在自增自减运算符。

在一众运算符中,自增与自减运算符总因其具有副作用而显得独树一帜。对于重视函数式编程的语言来说,自增自减运算符是弊大于利的,也是很难被接受的。

可以想象,若有人尝试在混合范式语言中写函数式的代码,然后因为某些原因其中混进了一个​​i++​​,那恐怕是想找到BUG原因都很困难的——相比起​​i += 1​​,​​i++​​看起来确实太隐晦了,很难在杂乱的代码中一眼看出这是个赋值语句,认识到其有副作用的事实,这可能导致潜在的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 <p>JavaScript</p><pre class="brush:javascript;toolbar:false;">const arr = [1, 2, 3, 4, 5]
// 传统计数循环
for (let i = 0; i <p>Go</p><pre class="brush:go;toolbar:false;">arr := [5]int{1, 2, 3, 4, 5}
// 传统计数循环
for i := 0; i <p>可以很明显地看到,使用迭代器减少了代码量,而且反而使得代码变得更加清晰。</p><p>当然,迭代器的作用不仅停留在表面的“减少代码”上。更重要的是迭代器减小了开发人员的心智负担。有过C/C++编程经验的人都知道,在传统for循环中更改i的值是非常危险的,一不留神就会造成严重的BUG甚至产生死循环。而迭代器的逻辑是不同的:每次循环从迭代器中取出值,而不是在某个值上递增。因此,即使不小心在使用迭代器的循环中错误更改了计数变量的值,也不会产生问题:</p><pre class="brush:c;toolbar:false;">for i in range(5):
i -= 1
로그인 후 복사

上面这段Python代码会是一个死循环吗?其实不会。因为​​for i in range(5)​​​的逻辑并非创建一个计数变量i,然后每次递增。其实现方式是先创建迭代器,然后依次从里面取值。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 ​看起来还是要比​<code style="font-family: monospace; font-size: 12px; background-color: rgba(0, 0, 0, 0.06); padding: 0px 2px; border-radius: 6px; line-height: inherit; overflow-wrap: break-word; text-indent: 0px;">​for i := 0; i ​稍微简洁一些,因此就保留了它们。如果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 += 1​​或​​j -= 1​​使用,好像也没太多使用场景了。

运算符重载带来歧义

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

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

첫 번째는 ++를 += 1의 구문 설탕으로 완전히 처리합니다. 오버로드된 경우​+=​​연산자는 자동으로 오버로드됩니다​x = 'a'; x += 'b'​​ 이후 x 값은 'ab'입니다. ​​++​​연산자, 이 규칙에 따라​​x++​​는​​x += 1​​, 이제 문제가 되지 않습니다. 불일치 오류가 보고됩니다. 그러나 Java와 마찬가지로 Python이 문자열을 연결할 때 자동으로 유형 변환을 수행하는 경우 ​​x += 1​​는 다음과 같습니다. 코드 스타일="글꼴 계열: 고정 폭; 글꼴 크기: 12px; 배경색: rgba(0, 0, 0, 0.06); 패딩: 0px 2px; 테두리 반경: 6px; 줄 높이: 상속 ; 오버플로- Wrap: break-word; text-indent: 0px;">​x += '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哪天加入了运算符重载,然后有人闲着没事去重载了内置类型上的​​+=​​运算符,那后果简直有点难以想象了。

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

이것이 약한 유형의 언어에서 어떤 비참한 결과를 가져올지 생각해 보세요. 이제 연산자 오버로드 없이도 JS를 작성할 수 있습니다.​let a = [ ]; code>​그렇다면 a의 값은 0, 즉 흑마법 코드이다. 어느 날 JS에 연산자 오버로딩이 추가되고 누군가가 내장된 type<code style="font-family: monospace;font-size: 12px; background-color: rgba(0, 0, 0, 0.06)를 자유롭게 오버로드할 수 있다면 ); padding: 0px 2px; border-radius: 6px; line-height: receive; Overflow-word: break-word: 0px;">​+=​​연산자, 결과는 다음과 같습니다. 상상할 수 없는. 🎜🎜둘째, 🎜++를 +=와 아무 관련이 없는 연산자로 취급🎜합니다. 이렇게 하면 위에서 설명한 엄청난 문제가 발생하지 않지만, 이렇게 하도록 선택하면 프로그래밍 언어 사용자가 ​+=​ 코드> ​연산자라면​<code style="font-family: monospace;font-size: 12px; background-color: rgba(0, 0, 0, 0.06); padding: 0px 2px; border-라고 생각하는 것이 당연할 수도 있습니다. radius: 6px; line-height: 상속;overflow-wrap: break-word; text-indent: 0px;">​++​​연산자도 오버로드되어 더 많은 모호성을 가져올 수 있습니다. 🎜

사실 여기서 언급한 연산자 오버로딩으로 인한 모호함은 이미 많은 언어에서 발생했습니다. 증가 및 감소 연산자와 연산자 오버로딩을 모두 지원하는 언어에서는 비슷한 이유로 인한 버그가 드물지 않습니다. 한 가지 해결책은 오버로드를 허용하지 않는 것입니다​++​​및​​--​​操作符,只允许它们在整数类型上使用。但既然这样了,为什么不考虑干脆去掉自增自减运算符呢?

一些其他的讨论

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

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

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

다른 토론

위 토론에서 나는 다음과 같은 언어 자체의 많은 기능을 의도적으로 무시했다는 점을 알 수 있습니다. Python에 증가 및 감소 연산자가 없는 또 다른 큰 이유는 정수가 불변 유형이고 증가 및 감소 연산자가 모호하기 쉽기 때문입니다. 🎜🎜글 시작 부분에서 말했듯이 이는 Python의 특성이며 여기서 "디자인 철학" 논의 범위에 포함되지 않습니다. 그러나 엄격함을 위해 여기서는 간단히 언급하겠습니다. 🎜🎜또한 많은 언어에서는 ​​a = a + 1​​、​​a += 1​​및​​a++​​는 같은 의미이지만, ​​​​둘을 구별하는 것입니다. Python, Java 등 가상 머신을 사용하는 여러 언어에서는 ​a += 1​​ ​a = a + 1​​ 구별합니다. 예를 들어 Java에서는 ​​a = a + 1​​바이트코드 iadd를 사용하여 구현 고정폭; 글꼴 크기: 12px; 배경 색상: rgba(0, 0, 0, 0.06); 테두리 반경: 6px; 오버플로 랩: 상속; 들여쓰기: 0px;">​a += 1​​및​​a++​​구현하려면 마찬가지로 Python에서는 바이트코드도 BINARY_ADD 및 INPLACE_ADD로 구분됩니다. 이러한 언어의 경우 ​​a++​​는 결국 요약

켄 톰슨(Ken Thompson)이 원래 이 아이디어를 즉석에서 생각해냈다고 말씀드리고 싶습니다.​++​​및​​--​​연산자의 영향은 예상보다 훨씬 클 수 있습니다. 자동 증가 및 자동 감소 연산자의 기원과 응용 시나리오에 대한 많은 사람들의 이해는 당연한 것으로 간주되며 "작업 효율성 향상", 심지어 "원자적 연산"과 같은 오해도 도처에 날고 있습니다. 동시에 C 언어 초보자(특히 중국)는 종종 혼란스러워합니다.​a = i++ + ++i + i++​​이 반전은 정의되지 않았습니다. 며칠 동안의 작업으로 인해 극심한 두통이 생겼습니다. 이 두 작은 연산자가 더 많은 편의를 가져올지, 더 많은 문제를 가져올지는 독자들이 스스로 생각하도록 남겨둡니다. ​++​​和​​--​​运算符产生的影响恐怕远远超出了本人的预料。许多人对自增和自减运算符起源和应用场景的理解也仅仅是停留在想当然的层面,诸如“提高运行效率”甚至“原子性操作”这样的误解也是满天飞。同时,C语言初学者(尤其是在国内)也常常被​​a = i++ + ++i + i++​​这种逆天未定义操作折腾到头疼欲裂。这两个小小的运算符究竟是带来了更多方便还是带来了更多麻烦,就留给读者自己去思考吧。

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

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

总而言之,不论你是一个很擅长使用​​++​​和​​--​

많은 현대 프로그래밍 언어에서 증가 및 감소 연산자의 지위가 크게 약화되었습니다. 일부 언어에서는 이 두 연산자의 사용을 엄격하게 제한하고 Go와 같은 표현식으로 사용하는 것을 허용하지 않습니다. 일부 언어에서는 ​-=​​는 Python 및 Rust와 같이 완전히 충분합니다. 🎜🎜오늘날 반복자가 점점 더 널리 사용되는 시대에는 ​​++​​및​​--​​한때 역사상 중요한 위치를 차지했던 이 두 사업자는 점차 사람들의 시야에서 사라져가는 것 같습니다. 이것이 좋은지 나쁜지 말하기는 어렵습니다. 결국 우리는 C/C++ 및 Java와 같은 언어에서 증가 및 감소 연산자를 제한적으로 사용하면 때때로 코드를 만들 수 있다는 것을 보았습니다. 매우 간결하고 명확합니다. Python이나 Rust처럼 이 두 연산자를 완전히 없애는 것은 너무 극단적인가요? 이것도 말하기 어렵습니다. 🎜🎜간단히 말하면, 당신이 좋은 사용자인지 여부 -radius: 6px; line-height: 상속; Overflow-word: break-word: 0px;">​++​​and​​--​​C/C++ 프로그래머 또는 부작용이 있는 이 두 연산자에 자연스럽게 혐오감을 느끼는 FP 옹호자는 개발 과정에서 이를 인정해야 합니다. 프로그래밍 언어의 경우 연산자 수와 감소 연산자의 중요성이 점점 줄어들고 있지만 특정 시나리오에서는 여전히 가치가 있습니다. 🎜

위 내용은 Python, Ruby 및 기타 언어에서 증가 연산자를 더 이상 사용하지 않는 이유는 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:51cto.com
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿