ホームページ  >  記事  >  バックエンド開発  >  Python、Ruby、その他の言語でインクリメント演算子が廃止されたのはなぜですか?

Python、Ruby、その他の言語でインクリメント演算子が廃止されたのはなぜですか?

王林
王林転載
2023-05-11 16:37:061196ブラウズ

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

多くの人は、いくつかの最新のプログラミング言語 (もちろん、「最近登場した」プログラミング言語のことではありません) で、自動インクリメントと自動-decrement 操作 シンボルはキャンセルされました。言い換えると、これらの言語には i j-- のような表現はなく、 i = 1 のみが存在します。 or j -= 1 そのような式です。この回答では、この現象の背景と理由を設計哲学の観点から探ります。

主流のプログラミング言語の中でインクリメント演算子とデクリメント演算子をサポートしていないのは Python、Rust、Swift だけのようですので、厳密に言うと「i が消える」と言うのは偏見かもしれません。

私が初めて Python に触れたとき、これにも戸惑いました。多くの関連する回答や記事を検索することに興味がありましたが、満足のいく回答が得られませんでした。数年が経った今、私はこの質問を再考し、答えを導き出そうとしました。

この記事はこの問題を「設計哲学から」のみ説明しており、言語自体の性質には特に関与していないことに注意してください。たとえば、Python でインクリメント演算子とデクリメント演算子が提供されない理由の大部分は、その整数型が不変であるためですが、これは「設計思想からの」議論ではないため、この記事には関連する内容は含まれません。 。

インクリメント演算子とデクリメント演算子があるのはなぜですか?

起源

Wikipedia は、インクリメント演算子とデクリメント演算子が最初に登場したのは B 言語 (C の前身) であると指摘しています。 B 言語の発明者は C 言語の発明者と同じ K&R で、その中でも Ken Thompson が最初に B 言語にインクリメント演算子とデクリメント演算子を導入しました。そのため、よく「インクリメント演算子やデクリメント演算子は C で生まれた」などと大雑把に言う人がいますが、事実は多少異なりますが、それほど違いはありません。

言語 B の構文は C の構文とよく似ています。最大の違いは、B が型指定されていないことかもしれません。ただし、B 言語については本題から逸れるので、ここではあまり紹介しません。ここで強調されているのは、インクリメント演算子とデクリメント演算子の最も古い起源だけです。

インクリメント演算子とデクリメント演算子が B 言語に導入された理由についてはさまざまな意見がありますが、Ken Thompson はそもそもこれら 2 つの演算子を作成した理由を公に述べたことはありません。ただし、明確にする必要がある誤解があります。つまり、これら 2 つの演算子の導入は、アセンブリ言語。実際、B 言語のもう一人の作成者であるデニス M. リッチー (もちろん、C 言語の作成者でもあります) は、かつて自分の記憶の中で「C 言語の開発」:## を指摘しました。 #

……トンプソンはさらに一歩進んで、インクリメントまたはデクリメントする および -- 演算子を発明しました。それらの接頭辞または接尾辞の位置によって、変更がオペランドの値に注目する前に行われるか後から行われるかが決まります。これらは B の初期バージョンには存在しませんでしたが、途中で登場しました。 人々は、C と Unix が最初に普及した DEC PDP-11 によって提供される自動インクリメントおよび自動デクリメント アドレス モードを使用するために作成されたと推測することがよくあります。 B が開発されたときには PDP-11 が存在しなかったため、これは歴史的に不可能です。 ただし、PDP-7 にはいくつかの「自動インクリメント」メモリ セルがあり、それらを介した間接的なメモリ参照によってインクリメントされるという特性がありました。セル。この機能はおそらくそのような演算子を Thompson に提案したと思われます。それらを接頭辞と接尾辞の両方にするという一般化は彼自身のものでした。実際、自動インクリメント セルは演算子の実装に直接使用されませんでした。また、革新へのより強い動機は、おそらく x の変換が # の変換よりも小さいという彼の観察でした。 ## x=x 1 .

文中の说法にはいくつかの模倣があり、

PDP-11 から生成されたものである可能性はありません。の自動インクリメントおよび自動デクリメント モード (B トークの発見時にはこのマシンさえ存在しませんでした) ですが、それがクライミング トークの INC およびに該当するかどうかは示されていません。この方法を検証するために、文中に記載されている PDP-7 のコマンド集を発見しました。これには、 INC または が含まれていません。参考までに、私たちは次の PDP-7 の実装も検討しましたが、関連するコマンドもまだ見つかっていません。 INCおよびDEC命令の最初の出現時期を考慮するために、私たちは1969年版のPDP-11ハンドブックを入手した。 λINCとDECはPDP-11に新しく導入された命令です(図にはDECは含まれていませんが、マニュアルの後ろにこの命令が含まれています): PDP-11 ハンドブック、1969 年、34 ページ

PDP-11 の正式な配布時期は 1970 年ですが、その開発時期は Ken Thompson が PDP-11 に参加したことを除けば 1969 年です。当然、デニス・リッチーが指摘するように、 INC および

DEC

という命令が動作するかどうかの影響はあり得ません。 PDP-7 には自動インクリメント メモリ セルがすでに導入されていますが、これはおそらく Ken Thompson が独自に自動計算番号 为什么 Python、Ruby 等语言弃用了自增运算符? を導入したものと考えられます。

另一个能够反驳“自增自减运算符直接对应于汇编指令”的事实是,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 < 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: 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+0x32>
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+0x4>
116a: 48 89 c7mov%rax,%rdi
116d: b8 00 00 00 00mov$0x0,%eax
1172: e8 d9 fe ff ffcall 1050 <printf@plt>
1177: 83 45 fc 01 addl $0x1,-0x4(%rbp)
117b: 83 7d fc 04 cmpl $0x4,-0x4(%rbp)
117f: 7e dd jle115e <main+0x15>
1181: b8 00 00 00 00mov$0x0,%eax
1186: c9leave
1187: c3ret

可以看到,默认情况下并没有调用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+0x4>
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@plt>
1173: 83 c3 01add$0x1,%ebx
1176: 83 fb 05cmp$0x5,%ebx
1179: 75 e4 jne115f <main+0x16>
117b: b8 00 00 00 00mov$0x0,%eax
1180: 48 83 c4 08 add$0x8,%rsp
1184: 5bpop%rbx
1185: 5dpop%rbp
1186: c3ret

至于更高的优化级别,其汇编代码的可读性太差,就不贴出来了。但经过验证,即使是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+0x3f>
1160: 8b 75 f8mov-0x8(%rbp),%esi
1163: 48 8d 3d 9a 0e 00 00lea0xe9a(%rip),%rdi# 2004 <_IO_stdin_used+0x4>
116a: b0 00 mov$0x0,%al
116c: e8 bf fe ff ffcall 1030 <printf@plt>
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+0x16>
117f: 31 c0 xor%eax,%eax
1181: 48 83 c4 10 add$0x10,%rsp
1185: 5dpop%rbp
1186: c3ret

同理,对于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) { ... }

有些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 < 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,然后每次递增。其实现方式是先创建迭代器,然后依次从里面取值。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 の糖衣構文として扱います。 = 演算子がオーバーロードされると、 演算子も自動的にオーバーロードされます。ただし、これは深刻な曖昧さをもたらします。たとえば、Python は、 x = 'a'; x = ' After b' を実行するなど、文字列の = 演算子をオーバーロードします。 、x の値は「ab」です。 演算子が Python に存在する場合、このルールに従って、 x x = 1 と見なされます。 、これは問題ありませんが、型の不一致エラーが報告されます。しかし、Python が Java と同様に、文字列を結合するときに型変換を自動的に実行する場合、 x = 1 は、 x = '1' と同じように正当になります。 x を実行すると、x の値は 'ab1' になります。これは非常に驚くべきことです。

弱く型付けされた言語でこれがもたらす壊滅的な結果を考えてみましょう。JS は演算子のオーバーロードなしでも記述できるようになりました。 let a = []; a その場合、a の値は 0 になります。 、黒魔術の暗号。ある日、演算子のオーバーロードが JS に追加され、その後誰かが何もせずに組み込み型の = 演算子をオーバーロードした場合、その結果は想像を絶するものになるでしょう。

2番目、は=とは関係のない演算子として扱います。これを実行しても、上で説明した信じられないような問題は発生しませんが、これを選択した場合、プログラミング言語のユーザーが = 演算子をオーバーロードしたときに、当然のことながら、 と考えるかもしれません。 演算子も過負荷になるため、さらに曖昧になる可能性があります。

実際、ここで述べた演算子のオーバーロードによって生じるあいまいさは、すでに多くの言語で発生しています。インクリメント演算子とデクリメント演算子、および演算子のオーバーロードの両方をサポートする言語では、同様の理由によるバグは珍しくありません。解決策の 1 つは、 演算子と -- 演算子のオーバーロードを禁止し、整数型でのみ使用を許可することです。しかし、それが事実であるのであれば、単純にインクリメント演算子とデクリメント演算子を取り除くことを検討してみてはいかがでしょうか。

その他の議論

上記の議論では、言語自体の多くの機能を意図的に無視していることに注意してください。自動 インクリメント演算子とデクリメント演算子を使用するもう 1 つの大きな理由は、整数が不変型であり、インクリメント演算子とデクリメント演算子があいまいになりやすいことです。

記事の冒頭で述べたように、これは Python の特性であり、ここでの「設計哲学」の議論の範囲内ではありません。ただし、厳密さを期すために、ここでは簡単に言及します。

また、多くの言語では a = a 1 a = 1 、および a は同じ意味を表しますが、この 2 つを区別する言語は数多くあります。 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 を意味しますか?意味を考えると、おそらく別の曖昧さの層が生じるでしょう。

概要

最初にこのアイデアを思いついたのはケン・トンプソンであると言わざるを得ません。 -- 演算子の影響は私の予想をはるかに超えている可能性があります。自動インクリメント演算子や自動デクリメント演算子の成り立ちや応用シナリオについては多くの人が当然のこととして理解しており、「演算効率の向上」や「アトミックな操作」といった誤解も飛び交っています。同時に、C 言語の初心者 (特に中国) は、a = i i i などの未定義の演算による分割頭痛に悩まされることがよくあります。これら 2 つの小さな演算子がより多くの利便性をもたらすのか、それともより多くの問題をもたらすのかは、読者が自分で考えることに委ねられています。 現代のプログラミング言語の多くでは、インクリメント演算子とデクリメント演算子の地位が大幅に弱まっています。 Go など、これら 2 つの演算子の使用を厳しく制限し、式として使用することを許可していない言語もあります。一部の言語では、

=

# と # と考えて、これら 2 つの演算子を単純に取り消します。 Python や Rust など、 # # -= で十分です。 現在、反復子がますます広く使用されているため、

-- これら 2 つは歴史の中で重要な役割を果たしています ステータス演算子曖昧になりつつあるようです。これが良いことなのか悪いことなのか、私には判断するのは難しいのですが、結局のところ、C/C や Java などの言語では、インクリメント演算子やデクリメント演算子の使用を制限すると、コードが非常に不安定になることがあります。簡潔かつ明確。 Python や Rust のように、これら 2 つの演算子を完全に廃止するのは極端すぎますか?これも言いにくいです。 つまり、あなたが

-- を使用するのが非常に上手な C/C プログラマーであるか、それともこれら 2 つの演算子に興味がある 副作用のある演算子を自然に嫌う FP 支持者は皆、プログラミング言語の発展に伴い、インクリメント演算子とデクリメント演算子がますます重要ではなくなっていることを認めざるを得ませんが、特定のシナリオでは依然として価値があります。

以上がPython、Ruby、その他の言語でインクリメント演算子が廃止されたのはなぜですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事は51cto.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。