国产gaysexchina男同gay,japanrcep老熟妇乱子伦视频,吃奶呻吟打开双腿做受动态图,成人色网站,国产av一区二区三区最新精品

附錄 A:調(diào)試

2018-02-24 15:51 更新

這個(gè)附錄演示了如何調(diào)試 Lisp 程序,并給出你可能會(huì)遇到的常見錯(cuò)誤。

中斷循環(huán) (Breakloop)

如果你要求 Lisp 做些它不能做的事,求值過程會(huì)被一個(gè)錯(cuò)誤訊息中斷,而你會(huì)發(fā)現(xiàn)你位于一個(gè)稱為中斷循環(huán)的地方。中斷循環(huán)工作的方式取決于不同的實(shí)現(xiàn),但通常它至少會(huì)顯示三件事:一個(gè)錯(cuò)誤信息,一組選項(xiàng),以及一個(gè)特別的提示符。

在中斷循環(huán)里,你也可以像在頂層那樣給表達(dá)式求值。在中斷循環(huán)里,你或許能夠找出錯(cuò)誤的起因,甚至是修正它,并繼續(xù)你程序的求值過程。然而,在一個(gè)中斷循環(huán)里,你想做的最常見的事是跳出去。多數(shù)的錯(cuò)誤起因于打錯(cuò)字或是小疏忽,所以通常你只會(huì)想終止程序并返回頂層。在下面這個(gè)假定的實(shí)現(xiàn)里,我們輸入?:abort?來回到頂層。

> (/ 1 0)
Error: Division by zero.
       Options: :abort, :backtrace
>> :abort
>

在這些情況里,實(shí)際上的輸入取決于實(shí)現(xiàn)。

當(dāng)你在中斷循環(huán)里,如果一個(gè)錯(cuò)誤發(fā)生的話,你會(huì)到另一個(gè)中斷循環(huán)。多數(shù)的 Lisp 會(huì)指出你是在第幾層的中斷循環(huán),要嘛通過印出多個(gè)提示符,不然就是在提示符前印出數(shù)字:

>> (/ 2 0)
Error: Division by zero.
       Options: :abort, :backtrace, :previous
>>>

現(xiàn)在我們位于兩層深的中斷循環(huán)。此時(shí)我們可以選擇回到前一個(gè)中斷循環(huán),或是直接返回頂層。

追蹤與回溯 (Traces and Backtraces)

當(dāng)你的程序不如你預(yù)期的那樣工作時(shí),有時(shí)候第一件該解決的事情是,它在做什么?如果你輸入?(trace?foo)?,則 Lisp 會(huì)在每次調(diào)用或返回?foo?時(shí)顯示一個(gè)信息,顯示傳給?foo?的參數(shù),或是?foo?返回的值。你可以追蹤任何自己定義的 (user-defined)函數(shù)。

一個(gè)追蹤通常會(huì)根據(jù)調(diào)用樹來縮進(jìn)。在一個(gè)做遍歷的函數(shù),像下面這個(gè)函數(shù),它給一個(gè)樹的每一個(gè)非空元素加上 1,

(defun tree1+ (tr)
  (cond ((null tr) nil)
        ((atom tr) (1+ tr))
        (t (cons (treel+ (car tr))
                 (treel+ (cdr tr))))))

一個(gè)樹的形狀會(huì)因此反映出它被遍歷時(shí)的數(shù)據(jù)結(jié)構(gòu):

> (trace tree1+)
(tree1+)
> (tree1+ '((1 . 3) 5 . 7))
1 Enter TREE1+ ((1 . 3) 5 . 7)
  2 Enter TREE1+ (1.3)
    3 Enter TREE1+ 1
    3 Exit TREE1+ 2
    3 Enter TREE1+ 3
    3 Exit TREE1+ 4
  2 Exit TREE1+ (2 . 4)
  2 Enter TREE1+ (5 . 7)
    3 Enter TREE1+ 5
    3 Exit TREE1+ 6
    3 Enter TREE1+ 7
    3 Exit TREE1+ 8
  2 Exit TREE1+ (6 . 8)
1 Exit TREE1+ ((2 . 4) 6 . 8)
((2 . 4) 6 . 8)

要關(guān)掉?foo?的追蹤,輸入?(untrace?foo)?;要關(guān)掉所有正在追蹤的函數(shù),只要輸入?(untrace)?就好。

一個(gè)更靈活的追蹤辦法是在你的代碼里插入診斷性的打印語(yǔ)句。如果已經(jīng)知道結(jié)果了,這個(gè)經(jīng)典的方法大概會(huì)與復(fù)雜的調(diào)適工具一樣被使用數(shù)十次。這也是為什么可以互動(dòng)地重定義函數(shù)式多么有用的原因。

一個(gè)回溯 (backtrace)是一個(gè)當(dāng)前存在棧的調(diào)用的列表,當(dāng)一個(gè)錯(cuò)誤中止求值時(shí),會(huì)由一個(gè)中斷循環(huán)生成此列表。如果追蹤像是”讓我看看你在做什么”,一個(gè)回溯像是詢問”我們是怎么到達(dá)這里的?” 在某方面上,追蹤與回溯是互補(bǔ)的。一個(gè)追蹤會(huì)顯示在一個(gè)程序的調(diào)用樹里,選定函數(shù)的調(diào)用。一個(gè)回溯會(huì)顯示在一個(gè)程序部分的調(diào)用樹里,所有函數(shù)的調(diào)用(路徑為從頂層調(diào)用到發(fā)生錯(cuò)誤的地方)。

在一個(gè)典型的實(shí)現(xiàn)里,我們可通過在中斷循環(huán)里輸入?:backtrace?來獲得一個(gè)回溯,看起來可能像下面這樣:

> (tree1+ ' ( ( 1 . 3) 5 . A))
Error: A is not a valid argument to 1+.
       Options: :abort, :backtrace
? :backtrace
(1+ A)
(TREE1+ A)
(TREE1+ (5 . A))
(TREE1+ ((1 . 3) 5 . A))

出現(xiàn)在回溯里的臭蟲較容易被發(fā)現(xiàn)。你可以僅往回檢查調(diào)用鏈,直到你找到第一個(gè)不該發(fā)生的事情。另一個(gè)函數(shù)式編程 (2.12 節(jié))的好處是所有的臭蟲都會(huì)在回溯里出現(xiàn)。在純函數(shù)式代碼里,每一個(gè)可能出錯(cuò)的調(diào)用,在錯(cuò)誤發(fā)生時(shí),一定會(huì)在棧出現(xiàn)。

一個(gè)回溯每個(gè)實(shí)現(xiàn)所提供的信息量都不同。某些實(shí)現(xiàn)會(huì)完整顯示一個(gè)所有待調(diào)用的歷史,并顯示參數(shù)。其他實(shí)現(xiàn)可能僅顯示調(diào)用歷史。一般來說,追蹤與回溯解釋型的代碼會(huì)得到較多的信息,這也是為什么你要在確定你的程序可以工作之后,再來編譯。

傳統(tǒng)上我們?cè)诮忉屍骼镎{(diào)試代碼,且只在工作的情況下才編譯。但這個(gè)觀點(diǎn)也是可以改變的:至少有兩個(gè) Common Lisp 實(shí)現(xiàn)沒有包含解釋器。

當(dāng)什么事都沒發(fā)生時(shí) (When Noting Happens)

不是所有的 bug 都會(huì)打斷求值過程。另一個(gè)常見并可能更危險(xiǎn)的情況是,當(dāng) Lisp 好像不鳥你一樣。通常這是程序進(jìn)入無(wú)窮循環(huán)的徵兆。

如果你懷疑你進(jìn)入了無(wú)窮循環(huán),解決方法是中止執(zhí)行,并跳出中斷循環(huán)。

如果循環(huán)是用迭代寫成的代碼,Lisp 會(huì)開心地執(zhí)行到天荒地老。但若是用遞歸寫成的代碼(沒有做尾遞歸優(yōu)化),你最終會(huì)獲得一個(gè)信息,信息說 Lisp 把棧的空間給用光了:

> (defun blow-stack () (1+ (blow-stack)))
BLOW-STACK
> (blow-stack)
Error: Stack Overflow

在這兩個(gè)情況里,如果你懷疑進(jìn)入了無(wú)窮循環(huán),解決辦法是中斷執(zhí)行,并跳出由于中斷所產(chǎn)生的中斷循環(huán)。

有時(shí)候程序在處理一個(gè)非常龐大的問題時(shí),就算沒有進(jìn)入無(wú)窮循環(huán),也會(huì)把棧的空間用光。雖然這很少見。通常把??臻g用光是編程錯(cuò)誤的徵兆。

遞歸函數(shù)最常見的錯(cuò)誤是忘記了基本用例 (base case)。用英語(yǔ)來描述遞歸,通常會(huì)忽略基本用例。不嚴(yán)謹(jǐn)?shù)卣f,我們可能說“obj 是列表的成員,如果它是列表的第一個(gè)元素,或是剩余列表的成員” 嚴(yán)格上來講,應(yīng)該添加一句“若列表為空,則 obj 不是列表的成員”。不然我們描述的就是個(gè)無(wú)窮遞歸了。

在 Common Lisp 里,如果給入?nil?作為參數(shù),?car?與?cdr?皆返回?nil?:

> (car nil)
NIL
> (cdr nil)
NIL

所以若我們?cè)?member?函數(shù)里忽略了基本用例:

(defun our-member (obj lst)
  (if (eql (car lst) obj)
      lst
      (our-member obj (cdr lst))))

要是我們找的對(duì)象不在列表里的話,則會(huì)陷入無(wú)窮循環(huán)。當(dāng)我們到達(dá)列表底端而無(wú)所獲時(shí),遞歸調(diào)用會(huì)等價(jià)于:

(our-member obj nil)

在正確的定義中(第十六頁(yè)「譯注: 2.7 節(jié)」),基本用例在此時(shí)會(huì)停止遞歸,并返回?nil?。但在上面錯(cuò)誤的定義里,函數(shù)愚昧地尋找?nil?的?car?,是?nil?,并將?nil?拿去跟我們尋找的對(duì)象比較。除非我們要找的對(duì)象剛好是?nil?,不然函數(shù)會(huì)繼續(xù)在?nil?的?cdr里尋找,剛好也是?nil?── 整個(gè)過程又重來了。

如果一個(gè)無(wú)窮循環(huán)的起因不是那么直觀,可能可以通過看看追蹤或回溯來診斷出來。無(wú)窮循環(huán)有兩種。簡(jiǎn)單發(fā)現(xiàn)的那種是依賴程序結(jié)構(gòu)的那種。一個(gè)追蹤或回溯會(huì)即刻演示出,我們的?our-member?究竟哪里出錯(cuò)了。

比較難發(fā)現(xiàn)的那種,是因?yàn)閿?shù)據(jù)結(jié)構(gòu)有缺陷才發(fā)生的無(wú)窮循環(huán)。如果你無(wú)意中創(chuàng)建了環(huán)狀結(jié)構(gòu)(見 199頁(yè)「12.3 節(jié)」,遍歷結(jié)構(gòu)的代碼可能會(huì)掉入無(wú)窮循環(huán)里。這些 bug 很難發(fā)現(xiàn),因?yàn)椴辉诤竺娌粫?huì)發(fā)生,看起來像沒有錯(cuò)誤的代碼一樣。最佳的解決辦法是預(yù)防,如同 199 頁(yè)所描述的:避免使用破壞性操作,直到程序已經(jīng)正常工作,且你已準(zhǔn)備好要調(diào)優(yōu)代碼來獲得效率。

如果 Lisp 有不鳥你的傾向,也有可能是等待你完成輸入什么。在多數(shù)系統(tǒng)里,按下回車是沒有效果的,直到你輸入了一個(gè)完整的表達(dá)式。這個(gè)方法的好事是它允許你輸入多行的表達(dá)式。壞事是如果你無(wú)意中少了一個(gè)閉括號(hào),或是一個(gè)閉引號(hào),Lisp 會(huì)一直等你,直到你真正完成輸入完整的表達(dá)式:

> (format t "for example ~A~% 'this)

這里我們?cè)诳刂谱址淖詈蠛雎粤碎]引號(hào)。在此時(shí)按下回車是沒用的,因?yàn)?Lisp 認(rèn)為我們還在輸入一個(gè)字符串。

在某些實(shí)現(xiàn)里,你可以回到上一行,并插入閉引號(hào)。在不允許你回到前行的系統(tǒng),最佳辦法通常是中斷執(zhí)行,并從中斷循環(huán)回到頂層。

沒有值或未綁定 (No Value/Unbound)

一個(gè)你最常聽到 Lisp 的抱怨是一個(gè)符號(hào)沒有值或未綁定。數(shù)種不同的問題都用這種方式呈現(xiàn)。

局部變量,如?let?與?defun?設(shè)置的那些,只在創(chuàng)建它們的表達(dá)式主體里合法。所以要是我們?cè)囍?創(chuàng)建變量的?let?外部引用它,

> (progn
    (let ((x 10))
      (format t "Here x = ~A. ~%" x))
    (format t "But now it's gone...~%")
    x)
Here x = 10.
But now it's gone...
Error: X has no value.

我們獲得一個(gè)錯(cuò)誤。當(dāng) Lisp 抱怨某些東西沒有值或未綁定時(shí),它的意思通常是你無(wú)意間引用了一個(gè)不存在的變量。因?yàn)闆]有叫做?x的局部變量,Lisp 假定我們要引用一個(gè)有著這個(gè)名字的全局變量或常量。錯(cuò)誤會(huì)發(fā)生是因?yàn)楫?dāng) Lisp 試著要查找它的值的時(shí)候,卻發(fā)現(xiàn)根本沒有給值。打錯(cuò)變量的名字通常會(huì)給出同樣的結(jié)果。

一個(gè)類似的問題發(fā)生在我們無(wú)意間將函數(shù)引用成變量。舉例來說:

> defun foo (x) (+ x 1))
Error: DEFUN has no value

這在第一次發(fā)生時(shí)可能會(huì)感到疑惑:?defun?怎么可能會(huì)沒有值?問題的癥結(jié)點(diǎn)在于我們忽略了最初的左括號(hào),導(dǎo)致 Lisp 把符號(hào)defun?解讀錯(cuò)誤,將它視為一個(gè)全局變量的引用。

有可能你真的忘記初始化某個(gè)全局變量。如果你沒有給?defvar?第二個(gè)參數(shù),你的全局變量會(huì)被宣告出來,但沒有初始化;這可能是問題的根源。

意料之外的 Nil (Unexpected Nils)

當(dāng)函數(shù)抱怨傳入?nil?作為參數(shù)時(shí),通常是程序先前出錯(cuò)的徵兆。數(shù)個(gè)內(nèi)置操作符返回?nil?來指出失敗。但由于?nil?是一個(gè)合法的 Lisp 對(duì)象,問題可能之后才發(fā)生,在程序某部分試著要使用這個(gè)信以為真的返回值時(shí)。

舉例來說,返回一個(gè)月有多少天的函數(shù)有一個(gè) bug;假設(shè)我們忘記十月份了:

(defun month-length (mon)
  (case mon
    ((jan mar may jul aug dec) 31)
    ((apr jun sept nov) 30)
    (feb (if (leap-year) 29 28))))

如果有另一個(gè)函數(shù),企圖想計(jì)算出一個(gè)月當(dāng)中有幾個(gè)禮拜,

(defun month-weeks (mon) (/ (month-length mon) 7.0))

則會(huì)發(fā)生下面的情形:

> (month-weeks 'oct)
Error: NIL is not a valud argument to /.

問題發(fā)生的原因是因?yàn)?month-length?在?case?找不到匹配 。當(dāng)這個(gè)情形發(fā)生時(shí),?case?返回?nil?。然后?month-weeks?,認(rèn)為獲得了一個(gè)數(shù)字,將值傳給?/?,/?就抱怨了。

在這里最起碼 bug 與 bug 的臨床表現(xiàn)是挨著發(fā)生的。這樣的 bug 在它們相距很遠(yuǎn)時(shí)很難找到。要避免這個(gè)可能性,某些 Lisp 方言讓跑完?case?或?cond?又沒匹配的情形,產(chǎn)生一個(gè)錯(cuò)誤。在 Common Lisp 里,在這種情況里可以做的是使用?ecase?,如 14.6 節(jié)所描述的。

重新命名 (Renaming)

在某些場(chǎng)合里(但不是全部場(chǎng)合),有一種特別狡猾的 bug ,起因于重新命名函數(shù)或變量,。舉例來說,假設(shè)我們定義下列(低效的) 函數(shù)來找出雙重嵌套列表的深度:

(defun depth (x)
  (if (atom x)
      1
      (1+ (apply #'max (mapcar #'depth x)))))

測(cè)試函數(shù)時(shí),我們發(fā)現(xiàn)它給我們錯(cuò)誤的答案(應(yīng)該是 1):

> (depth '((a)))
3

起初的?1?應(yīng)該是?0?才對(duì)。如果我們修好這個(gè)錯(cuò)誤,并給這個(gè)函數(shù)一個(gè)較不模糊的名稱:

(defun nesting-depth (x)
  (if (atom x)
      0
      (1+ (apply #'max (mapcar #'depth x)))))

當(dāng)我們?cè)贉y(cè)試上面的例子,它返回同樣的結(jié)果:

> (nesting-depth '((a)))
3

我們不是修好這個(gè)函數(shù)了嗎?沒錯(cuò),但答案不是來自我們修好的代碼。我們忘記也改掉遞歸調(diào)用中的名稱。在遞歸用例里,我們的新函數(shù)仍調(diào)用先前的?depth?,這當(dāng)然是不對(duì)的。

作為選擇性參數(shù)的關(guān)鍵字 (Keywords as Optional Parameters)

若函數(shù)同時(shí)接受關(guān)鍵字與選擇性參數(shù),這通常是個(gè)錯(cuò)誤,無(wú)心地提供了關(guān)鍵字作為選擇性參數(shù)。舉例來說,函數(shù)?read-from-string有著下列的參數(shù)列表:

(read-from-string string &optional eof-error eof-value
                         &key start end preserve-whitespace)

這樣一個(gè)函數(shù)你需要依序提供值,給所有的選擇性參數(shù),再來才是關(guān)鍵字參數(shù)。如果你忘記了選擇性參數(shù),看看下面這個(gè)例子,

> (read-from-string "abcd" :start 2)
ABCD
4

則?:start?與?2?會(huì)成為前兩個(gè)選擇性參數(shù)的值。若我們想要?read?從第二個(gè)字符開始讀取,我們應(yīng)該這么說:

> (read-from-string "abcd" nil nil :start 2)
CD
4

錯(cuò)誤聲明 (Misdeclarations)

第十三章解釋了如何給變量及數(shù)據(jù)結(jié)構(gòu)做類型聲明。通過給變量做類型聲明,你保證變量只會(huì)包含某種類型的值。當(dāng)產(chǎn)生代碼時(shí),Lisp 編譯器會(huì)依賴這個(gè)假定。舉例來說,這個(gè)函數(shù)的兩個(gè)參數(shù)都聲明為?double-floats?,

(defun df* (a b)
  (declare (double-float a b))
  (* a b))

因此編譯器在產(chǎn)生代碼時(shí),被授權(quán)直接將浮點(diǎn)乘法直接硬連接 (hard-wire)到代碼里。

如果調(diào)用?df*?的參數(shù)不是聲明的類型時(shí),可能會(huì)捕捉一個(gè)錯(cuò)誤,或單純地返回垃圾。在某個(gè)實(shí)現(xiàn)里,如果我們傳入兩個(gè)定長(zhǎng)數(shù),我們獲得一個(gè)硬體中斷:

> (df* 2 3)
Error: Interrupt.

如果獲得這樣嚴(yán)重的錯(cuò)誤,通常是由于數(shù)值不是先前聲明的類型。

警告 (Warnings)

有些時(shí)候 Lisp 會(huì)抱怨一下,但不會(huì)中斷求值過程。許多這樣的警告是錯(cuò)誤的警鐘。一種最常見的可能是由編譯器所產(chǎn)生的,關(guān)于未宣告或未使用的變量。舉例來說,在 66 頁(yè)「譯注: 6.4 節(jié)」,?map-int?的第二個(gè)調(diào)用,有一個(gè)?x?變量沒有使用到。如果想要編譯器在每次編譯程序時(shí),停止通知你這些事,使用一個(gè)忽略聲明:

(map-int #'(lambda (x)
             (declare (ignore x))
             (random 100))
         10)
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)