你已經(jīng)學(xué)到了大多數(shù)C語(yǔ)言的基礎(chǔ),并且準(zhǔn)備好開(kāi)始成為一個(gè)更嚴(yán)謹(jǐn)?shù)某绦騿T了。這里就是從初學(xué)者走向?qū)<业牡胤?,不僅僅對(duì)于C,更對(duì)于核心的計(jì)算機(jī)科學(xué)概念。我將會(huì)教給你一些核心的數(shù)據(jù)結(jié)構(gòu)和算法,它們是每個(gè)程序員都要懂的,還有一些我在真實(shí)程序中所使用的一些非常有趣的東西。
在我開(kāi)始之前,我需要教給你一些基本的技巧和觀念,它們能幫助你編寫更好的軟件。練習(xí)27到31會(huì)教給你高級(jí)的概念和特性,而不是談?wù)摼幊?,但是這些之后你將會(huì)應(yīng)用它們來(lái)編寫核心庫(kù)或有用的數(shù)據(jù)結(jié)構(gòu)。
編寫更好的C代碼(實(shí)際上是所有語(yǔ)言)的第一步是,學(xué)習(xí)一種新的觀念叫做“防御性編程”。防御性編程假設(shè)你可能會(huì)制造出很多錯(cuò)誤,之后嘗試在每一步盡可能預(yù)防它們。這個(gè)練習(xí)中我打算教給你如何以防御性的思維來(lái)思考編程。
在這個(gè)簡(jiǎn)單的練習(xí)中要告訴你如何做到創(chuàng)造性是不可能的,但是我會(huì)告訴你一些涉及到任務(wù)風(fēng)險(xiǎn)和開(kāi)放思維的創(chuàng)造力。恐懼會(huì)快速地扼殺創(chuàng)造力,所以我采用,并且許多程序員也采用的這種思維方式使我不會(huì)懼怕風(fēng)險(xiǎn),并且看上去像個(gè)傻瓜。
我只是暫時(shí)接受了這種思維,并且在應(yīng)用中用了一些小技巧。為了這樣做我會(huì)提出一些想法,尋找創(chuàng)造性的解決方案,開(kāi)一些奇奇怪怪的腦洞,并且不會(huì)害怕發(fā)明一些古怪的東西。在這種思維方式下,我通常會(huì)編寫出第一個(gè)版本的糟糕代碼,用于將想法描述出來(lái)。
然而,當(dāng)我完成我的創(chuàng)造性原型時(shí),我會(huì)將它扔掉,并且將它變得嚴(yán)謹(jǐn)和可考。其它人在這里常犯的一個(gè)錯(cuò)誤就是將創(chuàng)造性思維引入它們的實(shí)現(xiàn)階段。這樣會(huì)產(chǎn)生一種非常不同的破壞性思維,它是創(chuàng)造性思維的陰暗面:
這些都是錯(cuò)誤的。你經(jīng)常會(huì)碰到一些程序員,它們對(duì)自己創(chuàng)造的軟件具有強(qiáng)烈的榮譽(yù)感。這很正常,但是這種榮譽(yù)感會(huì)成為客觀上改進(jìn)作品的阻力。由于這種榮譽(yù)感和它們對(duì)作品的依戀,它們會(huì)一直相信它們編寫的東西是完美的。只要它們忽視其它人的對(duì)這些代碼的觀點(diǎn),它們就可以保護(hù)它們的玻璃心,并且永遠(yuǎn)不會(huì)改進(jìn)。
同時(shí)具有創(chuàng)造性思維和編寫可靠軟件的技巧是,采用防御性編程的思維。
在你做出創(chuàng)造性原型,并且對(duì)你的想法感覺(jué)良好之后,就應(yīng)該切換到防御性思維了。防御性思維的程序員大致上會(huì)否定你的代碼,并且相信下面這些事情:
這種思維方式讓你誠(chéng)實(shí)地對(duì)待你的代碼,并且為改進(jìn)批判地分析它。注意上面并沒(méi)有說(shuō)你充滿了錯(cuò)誤,只是說(shuō)你的代碼充滿錯(cuò)誤。這是一個(gè)需要理解的關(guān)鍵,因?yàn)樗o了你編寫下一個(gè)實(shí)現(xiàn)的客觀力量。
就像創(chuàng)造性思維,防御性編程思維也有陰暗面。防御性程序員是一個(gè)懼怕任何事情的偏執(zhí)狂,這種恐懼使他們遠(yuǎn)離可能的錯(cuò)誤或避免犯錯(cuò)誤。當(dāng)你嘗試做到嚴(yán)格一致或正確時(shí)這很好,但是它是創(chuàng)造力和專注的殺手。
一旦你接受了這一思維,你可以重新編寫你的原型,并且遵循下面的八個(gè)策略,它們被我用于盡可能把代碼變得可靠。當(dāng)我編寫代碼的“實(shí)際”版本,我會(huì)嚴(yán)格按照下面的策略,并且嘗試消除盡可能多的錯(cuò)誤,以一些會(huì)破壞我軟件的人的方式思考。
永遠(yuǎn)不要信任輸入
永遠(yuǎn)不要提供的輸入,并總是校驗(yàn)它。
避免錯(cuò)誤
如果錯(cuò)誤可能發(fā)生,不管可能性多低都要避免它。
過(guò)早暴露錯(cuò)誤
過(guò)早暴露錯(cuò)誤,并且評(píng)估發(fā)生了什么、在哪里發(fā)生以及如何修復(fù)。
記錄假設(shè)
清楚地記錄所有先決條件,后置條件以及不變量。
防止過(guò)多的文檔
不要在實(shí)現(xiàn)階段就編寫文檔,它們可以在代碼完成時(shí)編寫。
使一切自動(dòng)化
使一切自動(dòng)化,尤其是測(cè)試。
簡(jiǎn)單化和清晰化
永遠(yuǎn)簡(jiǎn)化你的代碼,在沒(méi)有犧牲安全性的同時(shí)變得最小和最整潔。
質(zhì)疑權(quán)威
不要盲目遵循或拒絕規(guī)則。
這些并不是全部,僅僅是一些核心的東西,我認(rèn)為程序員應(yīng)該在編程可靠的代碼時(shí)專注于它們。要注意我并沒(méi)有真正說(shuō)明如何具體做到這些,我接下來(lái)會(huì)更細(xì)致地講解每一條,并且會(huì)布置一些覆蓋它們的練習(xí)。
這些觀點(diǎn)都是一些流行心理學(xué)的陳詞濫調(diào),但是你如何把它們應(yīng)用到實(shí)際編程中呢?我現(xiàn)在打算向你展示這本書中的一些代碼所做的事情,這些代碼用具體的例子展示每一條策略。這八條策略并不止于這些例子,你應(yīng)該使用它們作為指導(dǎo),使你的代碼更可靠。
讓我們來(lái)看一個(gè)壞設(shè)計(jì)和“更好”的設(shè)計(jì)的例子。我并不想稱之為好設(shè)計(jì),因?yàn)樗梢宰龅酶谩?匆豢催@兩個(gè)函數(shù),它們都復(fù)制字符串,main函數(shù)用于測(cè)試哪個(gè)更好。
undef NDEBUG
#include "dbg.h"
#include <stdio.h>
#include <assert.h>
/*
* Naive copy that assumes all inputs are always valid
* taken from K&R C and cleaned up a bit.
*/
void copy(char to[], char from[])
{
int i = 0;
// while loop will not end if from isn't '\0' terminated
while((to[i] = from[i]) != '\0') {
++i;
}
}
/*
* A safer version that checks for many common errors using the
* length of each string to control the loops and termination.
*/
int safercopy(int from_len, char *from, int to_len, char *to)
{
assert(from != NULL && to != NULL && "from and to can't be NULL");
int i = 0;
int max = from_len > to_len - 1 ? to_len - 1 : from_len;
// to_len must have at least 1 byte
if(from_len < 0 || to_len <= 0) return -1;
for(i = 0; i < max; i++) {
to[i] = from[i];
}
to[to_len - 1] = '\0';
return i;
}
int main(int argc, char *argv[])
{
// careful to understand why we can get these sizes
char from[] = "0123456789";
int from_len = sizeof(from);
// notice that it's 7 chars + \0
char to[] = "0123456";
int to_len = sizeof(to);
debug("Copying '%s':%d to '%s':%d", from, from_len, to, to_len);
int rc = safercopy(from_len, from, to_len, to);
check(rc > 0, "Failed to safercopy.");
check(to[to_len - 1] == '\0', "String not terminated.");
debug("Result is: '%s':%d", to, to_len);
// now try to break it
rc = safercopy(from_len * -1, from, to_len, to);
check(rc == -1, "safercopy should fail #1");
check(to[to_len - 1] == '\0', "String not terminated.");
rc = safercopy(from_len, from, 0, to);
check(rc == -1, "safercopy should fail #2");
check(to[to_len - 1] == '\0', "String not terminated.");
return 0;
error:
return 1;
}
copy函數(shù)是典型的C代碼,而且它是大量緩沖區(qū)溢出的來(lái)源。它有缺陷,因?yàn)樗偸羌僭O(shè)接受到的是合法的C字符串(帶有'\0'),并且只是用一個(gè)while循環(huán)來(lái)處理。問(wèn)題是,確保這些是十分困難的,并且如果沒(méi)有處理好,它會(huì)使while循環(huán)無(wú)限執(zhí)行。編寫可靠代碼的一個(gè)要點(diǎn)就是,不要編寫可能不會(huì)終止的循環(huán)。
safecopy函數(shù)嘗試通過(guò)要求調(diào)用者提供兩個(gè)字符串的長(zhǎng)度來(lái)解決問(wèn)題。它可以執(zhí)行有關(guān)這些字符串的、copy函數(shù)不具備的特定檢查。他可以保證長(zhǎng)度正確,to字符串具有足夠的容量,以及它總是可終止。這個(gè)函數(shù)不像copy函數(shù)那樣可能會(huì)永遠(yuǎn)執(zhí)行下去。
這個(gè)就是永遠(yuǎn)不信任輸入的實(shí)例。如果你假設(shè)你的函數(shù)要接受一個(gè)沒(méi)有終止標(biāo)識(shí)的字符串(通常是這樣),你需要設(shè)計(jì)你的函數(shù),不要依賴字符串本身。如果你想讓參數(shù)不為NULL,你應(yīng)該對(duì)此做檢查。如果大小應(yīng)該在正常范圍內(nèi),也要對(duì)它做檢查。你只需要簡(jiǎn)單假設(shè)調(diào)用你代碼的人會(huì)把它弄錯(cuò),并且使他們更難破壞你的函數(shù)。
這個(gè)可以擴(kuò)展到從外部環(huán)境獲取輸入的的軟件。程序員著名的臨終遺言是,“沒(méi)人會(huì)這樣做?!蔽铱吹剿麄冋f(shuō)了這句話后,第二天有人就這樣做,黑掉或崩潰它們的應(yīng)用。如果你說(shuō)沒(méi)有人會(huì)這樣做,那就加固代碼來(lái)保證他們不會(huì)簡(jiǎn)單地黑掉你的應(yīng)用。你會(huì)因所做的事情而感到高興。
這種行為會(huì)出現(xiàn)收益遞減。下面是一個(gè)清單,我會(huì)嘗試對(duì)我用C寫的每個(gè)函數(shù)做如下工作:
assert(test && "message");在最開(kāi)始添加assert檢查。這句代碼會(huì)執(zhí)行檢查,失敗時(shí)OS通常會(huì)打印斷言行,通常它包括信息。當(dāng)你嘗試弄清assert為什么在這里時(shí),這會(huì)非常有用。check宏來(lái)執(zhí)行它并且提供錯(cuò)誤信息。我在這個(gè)例子中沒(méi)有使用check,因?yàn)樗鼤?huì)混淆比較。fopen或fread的返回代碼,這會(huì)導(dǎo)致他們?cè)阱e(cuò)誤下仍然使用這個(gè)資源。這會(huì)導(dǎo)致你的程序崩潰或者易受攻擊。check宏這樣工作。只是這些微小的事情就會(huì)改進(jìn)你的資源處理方式,并且避免一大堆錯(cuò)誤。
上一個(gè)例子中你可能會(huì)聽(tīng)到別人說(shuō),“程序員不會(huì)經(jīng)常錯(cuò)誤地使用copy。”盡管大量攻擊都針對(duì)這類函數(shù),他們?nèi)耘f相信這種錯(cuò)誤的概率非常低。概率是個(gè)很有趣的事情,因?yàn)槿藗儾簧瞄L(zhǎng)猜測(cè)所有事情的概率,這非常難以置信。然而人們對(duì)于判斷一個(gè)事情是否可能,是很擅長(zhǎng)的。他們可能會(huì)說(shuō)copy中的錯(cuò)誤不常見(jiàn),但是無(wú)法否認(rèn)它可能發(fā)生。
關(guān)鍵的原因是對(duì)于一些常見(jiàn)的事情,它首先是可能的。判斷可能性非常簡(jiǎn)單,因?yàn)槲覀兌贾朗虑槿绾伟l(fā)生。但是隨后判斷出概率就不是那么容易了。人們錯(cuò)誤使用copy的情況會(huì)占到20%、10%,或1%?沒(méi)有人知道。為了弄清楚你需要收集證據(jù),統(tǒng)計(jì)許多軟件包中的錯(cuò)誤率,并且可能需要調(diào)查真實(shí)的程序員如何使用這個(gè)函數(shù)。
這意味著,如果你打算避免錯(cuò)誤,你不需要嘗試避免可能發(fā)生的事情,而是要首先集中解決概率最大的事情。解決軟件所有可能崩潰的方式并不可行,但是你可以嘗試一下。同時(shí),如果你不以最少的努力解決最可能發(fā)生的事件,你就是在不相關(guān)的風(fēng)險(xiǎn)上浪費(fèi)時(shí)間。
下面是一個(gè)決定避免什么的處理過(guò)程:
這一微小的過(guò)程會(huì)產(chǎn)生一份不錯(cuò)的待辦列表。更重要的是,當(dāng)有其它重要的事情需要解決時(shí),它讓你遠(yuǎn)離勞而無(wú)功。你也可以更正式或更不正式地處理這一過(guò)程。如果你要完成整個(gè)安全審計(jì),你最好和團(tuán)隊(duì)一起做,并且有個(gè)更詳細(xì)的電子表格。如果你只是編寫一個(gè)函數(shù),簡(jiǎn)單地復(fù)查代碼之后劃掉它們就夠了。最重要的是你要停止假設(shè)錯(cuò)誤不會(huì)發(fā)生,并且著力于消除它們,這樣就不會(huì)浪費(fèi)時(shí)間。
如果你遇到C中的錯(cuò)誤,你有兩個(gè)選擇:
這就是處理方法,你需要執(zhí)行它來(lái)確保錯(cuò)誤盡快發(fā)生,記錄清楚,提供錯(cuò)誤信息,并且易于程序員來(lái)避免它。這就是我提供的check宏這樣工作的原因。對(duì)于每一個(gè)錯(cuò)誤,你都要讓它你打印信息、文件名和行號(hào),并且強(qiáng)制返回錯(cuò)誤代碼。如果你使用了我的宏,你會(huì)以正確的方式做任何事情。
我傾向于返回錯(cuò)誤代碼而不是終止程序。如果出現(xiàn)了大錯(cuò)誤我會(huì)中止程序,但是實(shí)際上我很少碰到大錯(cuò)誤。一個(gè)需要中止程序的很好例子是,我獲取到了一個(gè)無(wú)效的指針,就像safecopy中那樣。我沒(méi)有讓程序在某個(gè)地方產(chǎn)生“段錯(cuò)誤”,而是立即捕獲并中止。但是,如果傳入NULL十分普遍,我可能會(huì)改變方式而使用check來(lái)檢查,以保證調(diào)用者可以繼續(xù)運(yùn)行。
然而在庫(kù)中,我盡我最大努力永不中止。使用我的庫(kù)的軟件可以決定是否應(yīng)該中止。如果這個(gè)庫(kù)使用非常不當(dāng),我才會(huì)中止程序。
最后,關(guān)于“暴露”的一大部分內(nèi)容是,不要對(duì)多于一個(gè)錯(cuò)誤使用相同的信息或錯(cuò)誤代碼。你通常會(huì)在外部資源的錯(cuò)誤中見(jiàn)到這種情況。比如一個(gè)庫(kù)捕獲了套接字上的錯(cuò)誤,之后簡(jiǎn)單報(bào)告“套接字錯(cuò)誤”。它應(yīng)該做的是返回具體的信息,比如套接字上發(fā)生了什么錯(cuò)誤,使它可以被合理地調(diào)試和修復(fù)。當(dāng)你設(shè)計(jì)錯(cuò)誤報(bào)告時(shí),確保對(duì)于不同的錯(cuò)誤你提供了不同的錯(cuò)誤消息。
如果你遵循并執(zhí)行了這個(gè)建議,你就構(gòu)建了一份“契約”,關(guān)于函數(shù)期望這個(gè)世界是什么樣子。你已經(jīng)為每個(gè)參數(shù)預(yù)設(shè)了條件,處理潛在的錯(cuò)誤,并且優(yōu)雅地產(chǎn)生失敗。下一步是完善這一契約,并且添加“不變量”和“后置條件”。
不變量就是在函數(shù)運(yùn)行時(shí),一些場(chǎng)合下必須恒為真的條件。這對(duì)于簡(jiǎn)單的函數(shù)并不常見(jiàn),但是當(dāng)你處理復(fù)雜的結(jié)構(gòu)時(shí),它會(huì)變得很必要。一個(gè)關(guān)于不變量的很好的例子是,結(jié)構(gòu)體在使用時(shí)都會(huì)合理地初始化。另一個(gè)是有序的數(shù)據(jù)結(jié)構(gòu)在處理時(shí)總是排好序的。
后置條件就是退出值或者函數(shù)運(yùn)行結(jié)果的保證。這可以和不變了混在一起,但是也可以是一些很簡(jiǎn)單的事情,比如“函數(shù)應(yīng)總是返回0,或者錯(cuò)誤時(shí)返回-1”。通常這些都有文檔記錄,但是如果你的函數(shù)返回一個(gè)分配的資源,你應(yīng)該添加一個(gè)后置條件,做檢查來(lái)確保它返回了一個(gè)不為NULL的東西?;蛘?,你可以使用NULL來(lái)表示錯(cuò)誤,這種情況下,你的后置條件就是資源在任何錯(cuò)誤時(shí)都會(huì)被釋放。
在C編程中,不變量和后置條件都通常比實(shí)際的代碼和斷言更加文檔化。處理它們的最好當(dāng)時(shí)就是盡可能添加assert調(diào)用,之后記錄剩下的部分。如果你這么做了,當(dāng)其它人碰到錯(cuò)誤時(shí),他們可以看到你在編寫函數(shù)時(shí)做了什么假設(shè)。
程序員編寫代碼時(shí)的一個(gè)普遍問(wèn)題,就是他們會(huì)記錄一個(gè)普遍的bug,而不是簡(jiǎn)單地修復(fù)它。我最喜歡的方式是,Ruby on Rails系統(tǒng)只是簡(jiǎn)單地假設(shè)所有月份都有30天。日歷太麻煩了,所以與其修復(fù)它,不如在一些地方放置一個(gè)小的注釋,說(shuō)這是故意的,并且?guī)啄陜?nèi)都不會(huì)改正。每次一些人試圖抱怨它時(shí),他們都會(huì)說(shuō),“文檔里面都有!”
如果你能夠?qū)嶋H修復(fù)問(wèn)題,文檔并不重要,并且,如果函數(shù)具有嚴(yán)重的缺陷,你在修復(fù)它之前可以不記錄它。在Ruby on Rails的例子中,不包含日期函數(shù)會(huì)更好一些,而不是包含一個(gè)沒(méi)人會(huì)用的錯(cuò)誤的函數(shù)。
當(dāng)你為防御性編程執(zhí)行清理時(shí),盡可能嘗試修復(fù)任何事情。如果你發(fā)現(xiàn)你記錄了越來(lái)越多的,你不能修復(fù)的事情,需要考慮重新設(shè)計(jì)特性,或簡(jiǎn)單地移除它。如果你真的需要保留這一可怕的錯(cuò)誤的特性,那么我建議你編寫它、記錄它,并且在你受責(zé)備之前找一份新的工作。
你是個(gè)程序員,這意味著你的工作是通過(guò)自動(dòng)化消滅其它人的工作。它的終極目標(biāo)是使用自動(dòng)化來(lái)使你自己也失業(yè)。很顯然你不應(yīng)該完全消除你做的東西,但如果你花了一整天在終端上重復(fù)運(yùn)行手動(dòng)測(cè)試,你的工作就不是編程。你只是在做QA,并且你應(yīng)該使自己自動(dòng)化,消除這個(gè)你可能并不是真的想干的QA工作。
實(shí)現(xiàn)它的最簡(jiǎn)單方式就是編寫自動(dòng)化測(cè)試,或者單元測(cè)試。這本書里我打算講解如何使它更簡(jiǎn)單,并且我會(huì)避免多數(shù)編寫測(cè)試的信條。我只會(huì)專注于如何編寫它們,測(cè)試什么,以及如何使測(cè)試更高效。
下面是程序員沒(méi)有但是應(yīng)該自動(dòng)化的一些事情:
嘗試花一些時(shí)間在自動(dòng)化上面,你會(huì)有更多的時(shí)間用來(lái)處理一些有趣的事情?;蛘?,如果這對(duì)你來(lái)說(shuō)很有趣,也許你應(yīng)該編寫自動(dòng)化完成這些事情的軟件。
“簡(jiǎn)單性”的概念對(duì)許多人來(lái)說(shuō)比較微妙,尤其是一些聰明人。它們通常將“內(nèi)涵”與“簡(jiǎn)單性”混淆起來(lái)。如果他們很好地理解了它,很顯然非常簡(jiǎn)單。簡(jiǎn)單性的測(cè)試是通過(guò)將一個(gè)東西與比它更簡(jiǎn)單的東西比較。但是,你會(huì)看到編寫代碼的人會(huì)使用最復(fù)雜的、匪夷所思的數(shù)據(jù)結(jié)構(gòu),因?yàn)樗鼈冋J(rèn)為做同樣事情的簡(jiǎn)單版本非常“惡心”。對(duì)復(fù)雜性的愛(ài)好是程序員的弱點(diǎn)。
你可以首先通過(guò)告訴自己,“簡(jiǎn)單和清晰并不惡心,無(wú)論誰(shuí)在干什么事情”來(lái)戰(zhàn)勝這一弱點(diǎn)。如果其它人編寫了愚蠢的觀察者模式涉及到19個(gè)類,12個(gè)接口,而你只用了兩個(gè)字符串操作就可以實(shí)現(xiàn)它,那么你贏了。他們就是錯(cuò)了,無(wú)論他們認(rèn)為自己的復(fù)雜設(shè)計(jì)有多么高大上。
對(duì)于要使用哪個(gè)函數(shù)的最簡(jiǎn)單測(cè)試是:
你會(huì)注意到,最后我一般會(huì)放棄并告訴你根據(jù)你的判斷。簡(jiǎn)單性非常諷刺地是一件復(fù)雜的事情,所以使用你的品位作為指引是最好的方式。只需要確保在你獲取更多經(jīng)驗(yàn)之后,你會(huì)調(diào)整你對(duì)于什么是“好”的看法。
最后一個(gè)策略是最重要的,因?yàn)樗屇阃黄品烙跃幊趟季S,并且讓你轉(zhuǎn)換為創(chuàng)造性思維。防御性編程是權(quán)威性的,并且比較無(wú)情。這一思維方式的任務(wù)是讓你遵循規(guī)則,因?yàn)榉駝t你會(huì)錯(cuò)失一些東西或心煩意亂。
這一權(quán)威性的觀點(diǎn)的壞處是扼殺了獨(dú)立的創(chuàng)造性思維。規(guī)則對(duì)于完成事情是必要的,但是做它們的奴隸會(huì)扼殺你的創(chuàng)造力。
這條最后的策略的意思是你應(yīng)該周期性地質(zhì)疑你遵循的規(guī)則,并且假設(shè)它們都是錯(cuò)誤的,就像你之前復(fù)查的軟件那樣。在一段防御性編程的時(shí)間之后,我通常會(huì)這樣做,我會(huì)擁有一個(gè)不編程的休息并讓這些規(guī)則消失。之后我會(huì)準(zhǔn)備好去做一些創(chuàng)造性的工作,或按需做更多的防御型編程。
在這一哲學(xué)上我想說(shuō)的最后一件事,就是我并不是告訴你要按照一個(gè)嚴(yán)格的規(guī)則,比如“創(chuàng)造!防御!創(chuàng)造!防御!”去做這件事。最開(kāi)始你可能想這樣做,但是我實(shí)際上會(huì)做不等量的這些事情,取決于我想做什么,并且我可能會(huì)將二者融合到一起,沒(méi)有明確的邊界。
我也不認(rèn)為其中一種思維會(huì)優(yōu)于另一種,或者它們之間有嚴(yán)格的界限。你需要在編程上既有創(chuàng)造力也要嚴(yán)格,所以如果想要提升的話,需要同時(shí)做到它們。