首頁 > 軟體

Python Ruby 等語言棄用自增運運算元原因剖析

2022-08-07 14:00:15

正文

許多人也許會注意到一個現象,那就是在一些現代程式語言(當然,並不是指“最近出現”的程式語言)中,自增和自減運運算元被取消了。也就是說,在這些語言中不存在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自增運運算元的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com