第十章 賦值(Assignment)
10.1 導(dǎo)語
我們在第五章學(xué)習(xí)的宏函數(shù)setf改變了變量的值躲履;這個(gè)行為被稱作賦值咆畏。在本書中我們已經(jīng)盡量避免賦值的使用,僅僅是用來在頂層循環(huán)設(shè)置全局變量。我們還沒有學(xué)到如何在函數(shù)內(nèi)部使用setf践宴。
初學(xué)編程的時(shí)候?yàn)槭裁匆苊馐褂觅x值呢?因?yàn)橘x值是很容易被誤用的爷怀,從而導(dǎo)致函數(shù)很難被理解和調(diào)試阻肩。如果你一開始學(xué)編程的時(shí)候用的語言是basic,pascal,modula或者C烤惊,他們都十分仰仗賦值乔煞,你可能對于lisp不怎么用賦值感到很驚訝。相比其他語言柒室,lisp提供了豐富的控制結(jié)構(gòu)集合(比如let渡贾,還有函數(shù)式操作)這使得賦值不那么緊要了。
然而在某些場合下雄右,lisp中使用賦值就是很合適的空骚。本章將會介紹一些使用賦值編程的標(biāo)準(zhǔn)技術(shù),還有一些除了setf之外的內(nèi)建賦值格式擂仍。賦值被經(jīng)常用在迭代控制結(jié)構(gòu)的組合上囤屹,我們將在后續(xù)章節(jié)去討論。
10.2 更新一個(gè)全局變量
假設(shè)我們現(xiàn)在正在經(jīng)營一家檸檬水小攤逢渔,我們想要追蹤到現(xiàn)在為止已經(jīng)賣掉多少瓶肋坚。將整個(gè)銷量存儲在一個(gè)全局變量中,“total-glasses”复局,初始化是0.
Common lisp中有一個(gè)慣例就是全局變量的開頭和結(jié)尾都會加上星號冲簿。自然在頂層循環(huán)進(jìn)行快速的計(jì)算的時(shí)候可以忽略這個(gè)慣例,就是用全局變量進(jìn)行計(jì)算亿昏。但是在你想要寫一個(gè)程序來處理全局變量峦剔,那就要加上星號了。
現(xiàn)在角钩,每一次我們賣出一些檸檬水吝沫,就不得不更新這個(gè)變量。我們也想知道現(xiàn)在為止有多少被賣出递礼。
請注意sell函數(shù)包括了兩個(gè)語句惨险,第一個(gè)是用來更新變量TOTAL-GLASSES脊髓。第二個(gè)語句是用來打印現(xiàn)在為止已經(jīng)賣出多少瓶的信息。因?yàn)槭褂胒ormat來打印結(jié)果恭朗,所以返回值是nil。
10.3 常規(guī)更新函數(shù)
setf可以給任何變量賦任何值依疼。很普遍的賦值用法就是去更新一個(gè)變量律罢,換言之膀值,變量的舊值被用來計(jì)算變量的新值。我們的檸檬水小攤就是地A型的更新變量的例子沧踏。很多悦冀,或者說大部分賦值的使用就是這種方式。Common Lisp提供的內(nèi)建宏函數(shù)踏烙,所表達(dá)的大部分更新函數(shù)都要比使用setf更加的簡潔讨惩。我們來考慮這兩種情況寒屯,通過加數(shù)或者減數(shù)來更新一個(gè)計(jì)數(shù)器寡夹,還有通過在前面增加或者刪除元素來更新一個(gè)列表菩掏。
10.3.1 宏函數(shù)incf和decf
給一個(gè)數(shù)字變量加上數(shù),可以這么寫(setf A (+ A 5))野揪,你也可以這么寫(incf A 5)瞧栗,incf和decf都是為了加數(shù)或者減數(shù)而定義的特殊賦值宏函數(shù)斯稳。如果加減的數(shù)字被省略,那么默認(rèn)是1.
10.3.2 pushhe和pop宏函數(shù)
通過在最前面組合上元素的方式挣惰,可以再列表上加上一個(gè)元素通熄,比如(SETF X (CONS ’FOO X)),你也可以更加優(yōu)雅地表現(xiàn)你的意圖(PUSH ’FOO X)能耻。push,是源自于經(jīng)典計(jì)算機(jī)術(shù)語饿幅,pushdown stacks(壓棧)栗恩,或者說進(jìn)棧磕秤。棧就像自助餐廳里市咆,放盤子的那個(gè)帶彈簧的器具蒙兰,當(dāng)以放入一個(gè)盤子到棧里面搜变,他就會成為最頂上的那個(gè)元素挠他,當(dāng)你把這個(gè)元素從站里面拿出來绩社,下面的盤子就會成為最上面的元素愉耙,我們來嘗試一下使用push來建立一個(gè)盤子的棧拌滋。
dish3現(xiàn)在就是棧最頂上的元素赌渣,(從左向右閱讀一個(gè)列表就是從頂部到底部閱讀一個(gè)棧)坚芜,每一次調(diào)用push都會實(shí)行一個(gè)賦值鸿竖,變量mystack總是會被更新加上一個(gè)內(nèi)存單元。當(dāng)我們把盤子從棧中拿出來的時(shí)候悟泵,最頂上的盤子就是dish3糕非,lisp提供了一個(gè)pop宏函數(shù)來更新一個(gè)變量朽肥,方法是設(shè)置指針指向這個(gè)原始列表的rest鞠呈。
請注意pop返回的結(jié)果是之前棧中最上層的元素蚁吝,這個(gè)元素被彈出來其實(shí)是一個(gè)副作用窘茁,下面兩個(gè)語句是相等的山林。
let表達(dá)式首先記住的棧頂?shù)脑兀镜刈兞縯op-element框冀。之后再函數(shù)體內(nèi)通過設(shè)置mystack成為(rest mystack)而實(shí)現(xiàn)彈棧敏簿。最后返回值topelement。
為了和其他的賦值語句一致温数,push和pop實(shí)際上應(yīng)該被稱作pushf和popf撑刺。他們的名字不是以f結(jié)尾時(shí)因?yàn)闅v史原因猜煮。他們在setf出現(xiàn)之前就被使用了王带,也就是在這個(gè)f慣例出現(xiàn)之前愕撰。順便說一句搞挣,setf就是set field的縮寫囱桨,設(shè)置域舍肠。
10.3.3 更新本地變量
賦值不應(yīng)該被胡亂的使用,例如财边,改變本地變量一般是被認(rèn)為不優(yōu)雅的做法谍夭,只是應(yīng)該使用let綁定本地變量就好了紧索。(當(dāng)然也有例外)齐板。一個(gè)更加不優(yōu)雅的做法就是改變出現(xiàn)在函數(shù)參數(shù)列表里的變量的值葛菇,這樣會使得函數(shù)難以理解眯停,看看接下來的寫的很爛的代碼
這段代碼可以通過引入一些變量和使用let* 函數(shù)來改善莺债。 當(dāng)所有的賦值都被移除,我們可以確保變量的值一旦被創(chuàng)造出來就不會被改變第租。使用無賦值風(fēng)格的程序是很容易理解的慎宾,也很優(yōu)雅趟据。
有一些時(shí)候是使用賦值來代替let綁定是更加方便的做法汹碱。接下來就是例子咳促,請注意每一個(gè)變量初始值都是nil等缀,然后會一次性賦一個(gè)新的值尺迂。這種有紀(jì)律性的賦值并不是一個(gè)壞的風(fēng)格噪裕;與出現(xiàn)在前面例子中的賦值是不一樣的膳音。
10.4 when和unless
when和unless都是需要求值超過一個(gè)表達(dá)式的時(shí)候使用的,它的語法是這樣的:
when函數(shù)首先對測試部分語句求值兵志,如果返回值是nil想罕,when就僅僅返回nil按价。如果結(jié)果是非nil楼镐,when會對他的函數(shù)體內(nèi)的語句求值然后返回最后一個(gè)值框产。unless是相似的茅信,除了對測試部分求值為false的時(shí)候才繼續(xù)計(jì)算之外蘸鲸。對于這兩個(gè)條件式來說,都是先對測試部分求值窑多,然后范湖i最后一個(gè)語句的值埂息。最后一個(gè)語句之前的語句都只是起到副作用千康,比如i/o和賦值拾弃。
when和unless只有在文體上比cond要好一些豪椿,他們的語法更加簡單一些搭盾,也更加平易近人增蹭,因?yàn)樗麄兊闹皇潜磺蟹殖蓛蓚€(gè)部分滋迈。舉個(gè)例子饼灿,假設(shè)我們想要寫一個(gè)函數(shù)來接受兩個(gè)數(shù)字作為輸入碍彭,并使他們相乘庇忌。假設(shè)這個(gè)函數(shù)需要第一個(gè)輸入的數(shù)字是奇數(shù)皆疹,第二個(gè)輸入的數(shù)字是偶數(shù)略就。如果輸入除了一點(diǎn)紕漏窄绒,那么程序可以通過加1或者減1的方式來修正輸入彰导,并且打印出一個(gè)合適的警告信息螺戳。
10.5 虛擬變量
一個(gè)虛擬變量就是指指針可能被存儲的任何地方倔幼。一個(gè)像X或者N的普通變量包含了一個(gè)指向它的值的指針。但是指針也可以被存儲在其他地方鸟款,比如一個(gè)內(nèi)存單元的car和cdr组哩。賦值的意思其實(shí)是將一個(gè)指針替換成另一個(gè)指針伶贰,所以當(dāng)我們說變量N的值是3的時(shí)候黍衙,說的其實(shí)是一個(gè)叫做n的變量包含了一個(gè)指向數(shù)字3的指針琅翻。一個(gè)表達(dá)式(incf n)就是將原來的指針替換為一個(gè)指向4的指針方椎。
本章介紹的賦值宏函數(shù)可以給虛擬變量賦值棠众,也就是說他們可以在很多不同的地方存儲指針。SETF, INCF, DECF, PUSH, 或者POP的第一個(gè)參數(shù)就是一個(gè)位置描述胸墙,請看例子迟隅;
如你所見,setf和相關(guān)的語句可以接受位置描述吼野,如(fourth x)瞳步,然后在那些地方存儲新的指針单起,舉個(gè)例子嘀倒,表達(dá)式(fourth x)定義的指針就是列表x的第四的內(nèi)存單元的car测蘑。這個(gè)為止也被稱為x的cdddr的car乍狐,如下所示浅蚪。
10.6 樣例學(xué)習(xí):井字游戲
在本節(jié)我們會寫我們的第一個(gè)大程序:不止是玩井字游戲,還要解釋每一步背后的意思盗誊。在面對這樣復(fù)雜度的程序設(shè)計(jì)的時(shí)候怒见,我們需要首先互點(diǎn)時(shí)間想一想整個(gè)的設(shè)計(jì)荚藻,特別是將會使用的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)。我們先在畫板上進(jìn)行設(shè)計(jì):
我們怎么來描述一個(gè)井號畫板呢树埠?有字符board帶頭的一個(gè)列表,后面是十個(gè)數(shù)字盛霎,每一個(gè)數(shù)字描述的是每一個(gè)位置的內(nèi)容愤炸。如果對應(yīng)位置的數(shù)字是1凤薛,那么就表示該位置的內(nèi)容是O缤苫,如果對應(yīng)位置的數(shù)字是10活玲,就表示對應(yīng)位置的內(nèi)容是X。函數(shù)make-board創(chuàng)造一個(gè)新的井字游戲畫板穗熬。
請注意如果B是一個(gè)存儲井字游戲畫板的變量镀迂,滑板的位置1可以寫入,(nth 1 B)唤蔗,位置2可以寫入(nth 2 B)探遵,依次類推。(nth 0 B)就是返回一個(gè)字符畫板箱季。
現(xiàn)在,讓我們來寫一個(gè)函數(shù)打印畫板棍掐,convert-to-letter函數(shù)是將0,1,10等分別轉(zhuǎn)換成空格规哪,O或者X的函數(shù)。print-row是打印畫板一行的函數(shù)塌衰,print-row在print-board函數(shù)中逐次調(diào)用。
我們能通過改變列表里面任意位置的數(shù)字來實(shí)現(xiàn)玩家走一步的效果蝠嘉,只要把對應(yīng)位置的數(shù)字從0改成1或者10就好最疆。在make-move函數(shù)中的變量player不是1就是10,這取決于是誰走出這一步蚤告。
在繼續(xù)之前努酸,我們先做一個(gè)模板來測試一下幾個(gè)函數(shù),我們會定義變量 * computer * 和 * opponent * 來存儲10和1的值(分別就是X和O)杜恰,因?yàn)檫@樣子顯得很清楚获诈。
為了讓程序容易表現(xiàn),設(shè)置畫板的表現(xiàn)方式是很重要的心褐,對于井字游戲來說還是比較簡單的舔涎,因?yàn)槿齻€(gè)數(shù)字成一行的話只有8種配置存在。橫的三個(gè)逗爹,豎的三個(gè)亡嫌,還有對角線兩個(gè)。我們可以說每一種組合都是一個(gè)triplet(三聯(lián)體)。我們會把所有的三聯(lián)體存儲在一個(gè)全局變量中 * triplets * 挟冠。
現(xiàn)在于购,我們可以寫一個(gè)函數(shù)sum-triplet來計(jì)算畫板中由三聯(lián)體定義的位置的值的和。例如知染,右對角線的三聯(lián)體是(3 5 7)肋僧。三個(gè)元素的的位置的值的和是11,表示有一個(gè)10和一個(gè)1還有一個(gè)空格(以某一個(gè)順序排布)在那個(gè)對角線上控淡。如果和是21嫌吠,那么就表示有兩個(gè)X和一個(gè)O存在。如果和是12逸寓,就表示有一個(gè)X和兩個(gè)O存在居兆。
想要完全分析出一個(gè)畫板,我們需要瀏覽所有的和竹伸,函數(shù)compute-sums的作用就是返回一個(gè)所有8個(gè)和的列表泥栖。
請注意玩家O如果在一行達(dá)到了三個(gè)O,其中一個(gè)和就會是3勋篓,類似的如果玩家X達(dá)到三個(gè)一線的話吧享,其中一個(gè)和就是30,我們可以寫一個(gè)斷言來檢查這個(gè)條件譬嚣。
我們等會兒回過頭在看畫板分析這個(gè)問題「炙蹋現(xiàn)在我們先來看看這個(gè)游戲的基本框架。函數(shù)play-one-game給用戶提供了先出手的機(jī)會拜银,傳遞一個(gè)新的殊鞭,空的畫板作為輸入。
函數(shù)opponent-move的作用是讓對手走一步尼桶,并且判斷這一步是不是合法操灿。然后更新畫板,調(diào)用computer-move泵督。首先趾盐,如果對手的一步讓三個(gè)符號連成一線,對手會贏游戲結(jié)束小腊。第二救鲤,如果在畫板上已經(jīng)沒有空白的地方存在了,游戲就會陷入和局而結(jié)束秩冈。我們假設(shè)對手是O本缠,計(jì)算機(jī)是X。
合法的一部的意思是輸入1到9之間的一個(gè)整數(shù)入问,這個(gè)證書對應(yīng)了畫板上的空位搓茬。函數(shù)read-a-legal-mobe讀取一個(gè)lisp對象并且檢查他是不是合法的一步犹赖。如果不是,函數(shù)就會調(diào)用他自身然后讀取另一步卷仑。請注意峻村,第一個(gè)兩個(gè)cond語句每個(gè)都包含測試部分和結(jié)果部分,最后一個(gè)值(遞歸調(diào)用)會被返回锡凝。
board-full-p斷言會被opponent-move調(diào)用來判斷在畫板上還有沒有更多的空白位置粘昨。
函數(shù)computer-move類似于oppnent-move,除了晚間是X而不是O之外窜锯,而且駛?cè)氩皇菑逆I盤的來张肾,而是會調(diào)用choose-best-move函數(shù)。這個(gè)函數(shù)會返回一個(gè)雙元素的列表锚扎,第一個(gè)元素師X擺放的位置吞瞪,第二個(gè)元素是一個(gè)字符串來解釋每一步之后的策略。
現(xiàn)在我們機(jī)會已經(jīng)準(zhǔn)備好玩我們的第一個(gè)游戲了驾孔。我們的第一個(gè)版本中芍秆,choose-best-move只有一個(gè)策略,隨機(jī)選一個(gè)合法的位置翠勉。函數(shù)random-move-strategy返回一個(gè)列表妖啥,列表的第一個(gè)元素就是移動(dòng)的位置,第二個(gè)元素是一個(gè)字符串对碌,來解釋走這一步的策略荆虱。函數(shù)pick-random-empty-position從1到9之間選擇一個(gè)隨機(jī)數(shù),如果那個(gè)位置為空朽们,那么就是用怀读,否則他就會遞歸調(diào)用自身來嘗試另一個(gè)隨機(jī)數(shù)。
你可以先嘗試和電腦玩一下游戲來看看感覺如何骑脱,很快你就會感覺到菜枷,隨機(jī)選擇策略對于計(jì)算機(jī)端來說不是一個(gè)很好地選擇;有些時(shí)候會讓計(jì)算機(jī)下出很蠢的棋來:
計(jì)算機(jī)很明顯在已經(jīng)有兩個(gè)X連成一線的情況下惜姐,在本可以贏的時(shí)候在位置3下了一步。隨機(jī)選擇一步椿息,在位置4放一個(gè)X對于全局沒有任何好處歹袁,因?yàn)樵诖怪狈较蛏夏菞l路已經(jīng)被O給封死了。
為了使我們的程序更加聰明寝优,我們可以編程找到兩個(gè)連成一線的情況条舔,如果有兩個(gè)X連成一線,計(jì)算機(jī)應(yīng)該填上第三個(gè)來贏得游戲乏矾,與之相反孟抗,如果有兩個(gè)O連成一線迁杨,就應(yīng)該在第三個(gè)位置放一個(gè)X來阻止對手勝利。
如果不能滿足他們各自的的策略凄硼,make-three-in-a-row和block-opponent都會返回nil∏π現(xiàn)在我們需要去修改choose-best-move函數(shù)來使用更加好的策略。我們引入一個(gè)or到函數(shù)體當(dāng)中這樣就可以一個(gè)個(gè)評判具體策略摊沉,直到有一個(gè)不是nil狐史。
新的策略使得游戲更加有趣了,計(jì)算機(jī)會在對手明顯要贏得時(shí)候進(jìn)行防守说墨,也會在合適的時(shí)候利用機(jī)會取得勝利骏全、
小結(jié)
setf宏可以給變量賦任何值。更新一個(gè)變量意味著基于它的舊值來計(jì)算一個(gè)新的值尼斧。兩個(gè)常規(guī)格式的更新語句是給一個(gè)數(shù)字變量加上或者減去(如incf和decf的操作)姜贡,或者是給一個(gè)列表前面加上或者減去一個(gè)元素(如push和pop的操作)。大部分更新操作是用在全局變量上棺棵。改變本地變量的值一般被認(rèn)為是不好的編程風(fēng)格楼咳,相比之下,使用let函數(shù)來綁定新變量會更好些律秃。
一個(gè)虛擬變量就是一個(gè)指針會被存儲的任何地方爬橡。本章討論的所有膚質(zhì)宏函數(shù)都可以操作虛擬變量,不僅僅是針對普通變量棒动。
賦值操作的使用在lisp編程里是很保守的糙申。let,函數(shù)式操作船惨,還有尾遞歸函數(shù)柜裸,這些其他語言所欠缺的,是的賦值在很對哦情況下變得不是很緊要了粱锐。沒有賦值的程序一般被認(rèn)為是很優(yōu)雅的疙挺。
本章涉及函數(shù)
賦值宏函數(shù): SETF, INCF, DECF, PUSH, POP.
條件式: WHEN, UNLESS.
Lisp Toolkit: BREAK and ERROR
break和error函數(shù)對于調(diào)試是很有用的,也會使得函數(shù)對于bug更有抗性怜浅。break在第八章的工具小結(jié)被介紹铐然,但是沒有展開他的全貌,break和error都接受一個(gè)格式控制字符串作為一個(gè)參數(shù)恶座,附加的參數(shù)搀暑,格式控制指令也會出現(xiàn)在控制字符串里。
break打印的是由格式控制字符串生成的信息跨琳,并且會調(diào)用lisp進(jìn)入調(diào)試器自点。在調(diào)試器使用結(jié)束之后,通過使用一些調(diào)試器命令脉让,比如go桂敛,proceed和restart功炮,你可以從斷點(diǎn)開始繼續(xù)執(zhí)行你的程序。(調(diào)試器的實(shí)現(xiàn)是獨(dú)立的术唬,所以你的調(diào)試器的命令形式取決于你的lisp實(shí)現(xiàn))薪伏。
下面的例子是使用break來調(diào)試一個(gè)函數(shù),這個(gè)函數(shù)假設(shè)接受一個(gè)售價(jià)和一個(gè)傭金率作為輸入碴开,計(jì)算出傭金毅该,打印信息,然后根據(jù)傭金是不是大于100美元潦牛,返回rich或者poor眶掌。有些時(shí)候,他會返回nil巴碗,這就是一個(gè)bug朴爬。
為了調(diào)試這個(gè)程序,我們開始在函數(shù)體中插入break調(diào)用橡淆,然后我們可以使用調(diào)試器來檢查控制棧和本地變量的值召噩。
現(xiàn)在錯(cuò)誤的原因就非常明顯了,當(dāng)傭金剛好等于100美元的時(shí)候逸爵,cond語句都不是一個(gè)真值具滴,所以cond會fanhuinil。解決方法就是將第二個(gè)測試表達(dá)式替換為T师倔。
error函數(shù)接受的參數(shù)和break相同构韵,第一個(gè)參數(shù)是格式化字符串,之后是一些附件參數(shù)趋艘。error和break之間的一個(gè)區(qū)別是error從不返回疲恢。你不能從error中繼續(xù)。第二瓷胧,error僅僅是報(bào)告錯(cuò)誤然后終止程序显拳,沒有進(jìn)入調(diào)試器的打算,雖然打不粉實(shí)現(xiàn)是會進(jìn)的搓萧。
通過插入狀態(tài)檢查(sanity checks)杂数,程序會變得更加健壯。狀態(tài)檢查就是一些確保都是正常瘸洛,有錯(cuò)誤就報(bào)錯(cuò)的表達(dá)式揍移。例如,這個(gè)版本的average函數(shù)就會檢查他的輸入是不是都是數(shù)字货矮。
Common Lisp還提供了一些其他的函數(shù)來報(bào)告錯(cuò)誤羊精。warn函數(shù)打印一個(gè)警告信息但是不會終止運(yùn)行中的程序斯够。cerror表示“continual error”囚玫,用戶會被告知出錯(cuò)然后會有繼續(xù)執(zhí)行的選項(xiàng)喧锦。這些函數(shù),還有新的Common Lisp條件系統(tǒng)都允許你標(biāo)記和設(shè)置任意的錯(cuò)誤條件抓督,這個(gè)不會在本書介紹燃少。請看你的用戶參考手冊來獲取細(xì)節(jié)。
第十章進(jìn)階話題
10.7 Do-It-Yoursef List Surgery
你可以通過使用虛擬變量調(diào)用setf來直接操作指針铃在。例如阵具,假設(shè)我們想要把一個(gè)三個(gè)內(nèi)存單元的列表轉(zhuǎn)換成一個(gè)兩個(gè)內(nèi)存單元的列表,把中間的那個(gè)內(nèi)存單元拿掉定铜。換句話說阳液。我們想要把第一個(gè)內(nèi)存單元的cdr直接指向第三個(gè)內(nèi)存單元。
請注意B的值是不會被snip給改變的揣炕。只有第一個(gè)單元的cdr被改變了帘皿。
我們可以使用setf來創(chuàng)造下面的循環(huán)結(jié)構(gòu)。
循環(huán)列表circ看起來就像這樣:
直接更改內(nèi)存單元的指針來修改列表的方法被稱作list surgery(是在不知道怎么翻譯畸陡,再深入學(xué)習(xí)一下之后回來補(bǔ)上鹰溜,暫且可以理解為列表操作,有朋友知道的話請告知)丁恭。列表操作在面對大型的復(fù)雜的列表的時(shí)候是非常有用的曹动,因?yàn)楦淖円恍┲羔樢冉⑷碌牧斜砜斓亩唷_@也會減少程序的內(nèi)存要求(或者說更少的使用垃圾回收機(jī)制)牲览。進(jìn)階的common lisp編程包括了很多列表操作(list surgery)墓陈,但是對于初學(xué)者就不是很必要了。最常用的列表操作已經(jīng)內(nèi)建在common lisp中竭恬,我們會在下一節(jié)見到跛蛋。
10.8 破壞性操作列表
破壞性列表操作是指那些改變了內(nèi)存單元內(nèi)容的操作。這些操作是很危險(xiǎn)的痊硕,因?yàn)樗麄兡軌騽?chuàng)造循環(huán)結(jié)構(gòu)赊级,變得很難打印出來,而且還有共享結(jié)構(gòu)可能會很淡判斷岔绸。但是破壞性函數(shù)也是很強(qiáng)大而且有效地工具理逊。根據(jù)慣例,大部分破壞性函數(shù)的名字都有一個(gè)前導(dǎo)N(基本上就是意外的歷史遺留吧因?yàn)椋?/p>
10.8.1 NCONC
nconc(由concatenate而來)是一個(gè)破壞性版本的append盒揉。append函數(shù)創(chuàng)造一個(gè)新的列表作為結(jié)果晋被,nconc是物理上的改變第一個(gè)輸入的最后一個(gè)內(nèi)存單元指向第二個(gè)輸入。
如果第一個(gè)輸入是nil刚盈,就會值返回第二個(gè)輸入羡洛,因此,也不應(yīng)該事先就假設(shè)(NCONC X Y)就愛一定會改變x的值藕漱。如果x是nil欲侮,它的值不會被改變崭闲。所以要在setf的函數(shù)體內(nèi),使用nconc來給x賦值才會可以威蕉。
nconc函數(shù)實(shí)際上接受任意數(shù)量的輸入刁俭,并且暴力連接所有的輸入成為一個(gè)內(nèi)存單元鏈條。我們也可以寫一個(gè)自己版本的nconc來接受兩個(gè)列表韧涨。技能點(diǎn):如果第一個(gè)輸入是nil牍戚,那么就簡單append第二個(gè)輸入然后返回。
10.8.2 NSUBST
nsubst是subst的一個(gè)破壞性版本虑粥。他通過改變一些內(nèi)存單元的car指針來修改列表如孝。
在最后一個(gè)例子中,既然我們在列表中搜索(a i)娩贷,我們告訴nsubst使用equal作為等于斷言暑竟,原先默認(rèn)的也不會起作用了。
10.8.3 其他破壞性函數(shù)
很多其他的Common Lisp內(nèi)建函數(shù)也有對應(yīng)的破壞性版本育勺。例如有nreverse但荤,nunion,nintersection和nset-difference涧至。對于前綴n的命名慣例也只有兩個(gè)例外腹躁。
append確實(shí)是第一個(gè)擁有破壞性副本的lisp函數(shù),它的破壞性版本叫做nconc南蓬,(也有一個(gè)函數(shù)叫做conc纺非,但是因?yàn)槭褂玫暮觳磺逶诤罄m(xù)的方言中就消失了)很多年之后nconc才導(dǎo)致了n前綴來表示破壞性函數(shù)的慣例,這也是為什么沒有nappend的原因赘方。另一個(gè)n前綴慣例的例外情況就是remove烧颖。它的破壞性副本叫做delete,再一次是優(yōu)于歷史原因窄陡,(delete在nconc之后才發(fā)明炕淮,但是卻是在n慣例形成之前,所以沒有一個(gè)nremove的版本)跳夭。ni原版本認(rèn)為是noncopying或者nonconsing的縮寫涂圆。
10.9 使用破壞性操作編程
一個(gè)破壞性函數(shù)特別有用的地方在于給復(fù)雜的列表結(jié)構(gòu)做出細(xì)微的改變,比如在井字游戲中的make-move函數(shù)币叹。還有另一個(gè)例子润歉,假設(shè)我們使得接下來的表格存儲在全局變量 * things * 中。
我們?nèi)绾螌⒆址鹢bject1改成frob呢颈抚?表達(dá)式(ASSOC ’OBJECT1 THINGS)將會返回列表(OBJECT1 LARGE GREEN SHINY CUBE)我們可以使用setf來改變第一個(gè)內(nèi)存單元的的car部分存儲的指針踩衩,既然這是一個(gè)列表的破壞性操作,那么列表的值也將會被改變。我們就來寫一個(gè)一般的重命名函數(shù):
我們可以使用nconc驱富,另一個(gè)破壞性操作反砌,來給列表中的對象加上一個(gè)新的屬性。
10.10 SETQ和SET
在早起的Lisp方言中萌朱,setf和虛擬變量是不可獲得的,賦值函數(shù)叫做setq策菜,setq特殊函數(shù)今天仍然存在晶疼。它的語法和宏函數(shù)setf相同,也可以被用在給一般變量賦值(但虛擬變量不行)又憨。
如果你閱讀比較古老的lisp書籍翠霍,你會注意到他們的賦值使用setq而不是setf完成的。現(xiàn)代Common Lisp程序員使用setf作為賦值語句蠢莺,不論是普通變量還是虛擬變量寒匙。setq在今日被認(rèn)為是陳舊的。在內(nèi)部躏将,仍然锄弱,大部分lisp實(shí)現(xiàn)使用setq來完成普通變量的賦值,所以你還可以在調(diào)試器輸出中看到setq祸憋。
set函數(shù)会宪,類似于setf,來自于最早的lisp方言蚯窥,lisp1.5掸鹅,set會對兩個(gè)參數(shù)都進(jìn)行求值,第一個(gè)參數(shù)必須求值為一個(gè)字符拦赠,因?yàn)镃ommon Lisp使用語法作用域巍沙,而lisp1.5則不是,set函數(shù)的意義也就改變了荷鼠。在common lisp中句携,set在字符的值單元里存儲一個(gè)值,及時(shí)是本地變量有同名的變量存在允乐。symbol-value函數(shù)返回的是一個(gè)字符值單元里的呢榮务甥,這里是一個(gè)使用set和symbol-value的例子。