#

迴圈命令可以讓某些程式碼有條件的反覆執行,如果你想讓程式擁有「如果某件事是真的,就反覆做某些事」的功能,這時就要仰賴迴圈命令了。例如,寫一個讀取檔案的程式,你可能會一次從檔案內讀取一部份的內容,然後一直反覆直到讀完檔案。像這樣的情況就會用到迴圈,因為你需要「反覆的讀取,直到讀完整個檔案」。這篇文章會簡單的介紹與迴圈相關的命令。

7.1 while迴圈命令

while是最單純的迴圈命令,常被用來執行不確定次數的迴圈,當然這並不是硬性的規定,真正該如何使用還是得看個人的使用習慣,while的語法如下:

while test body

while命令判斷條件式test的值來決定是否反覆執行body。test是條件式用來決定是否繼續執行迴圈。body是條件式為真的時候要執行的程式碼。while的運作流程如下:

step1 : 直譯器執行到while命令。
step2 : 如果需要的話先處理test及body的代換動作。
step3 : 運算條件式test, 如果執行結果是「真」就執行step4,否則就跳到step5。
step4 : 執行body的內容,執行完跳回step3。
step5 : 結束while迴圈。

§ 關於迴圈條件式

Tcl使用與expr相同的方式來運算迴圈的條件式。換句話說,你可以想像迴圈的條件式,會交由expr命令運算,然後再由運算的結果來判斷迴圈是否反覆執行。

下方是一個非常簡單的例子,透過while命令產出10個按鈕,每按下其中一個按鈕就會印出對應的數字,執行後的結果如下:

圖7-1 用while產生10個按鈕

程式碼如下:

程式的第2行設定while的條件式為「當變數i的值小於10」就執行後面大括號裡的程式,所以只要是i的值小於10程式3~5行就會反覆執行。 接下來請看到第5行,incr命令每執行一次就會把變數i的值加1,也就是說迴圈每執行一次i的值就會加1,所以執行完10次的迴圈i的值將會變為10,這樣會剛好會讓第2行的$i<10判斷為不成立,然後跳出迴圈。像這樣在迴圈裡改變條件式中的變數是很重要的技巧,只要學會控制好裡頭的變數,你就可以隨心所欲的控制迴圈了。 然後請看到第3及第4行,根據代換的規則第3行中的三個$i及第4行中的一個$i都會被代換掉。由於第5行每執行一次i的值都會加1,所以不用擔心button會出現相同的元件ID。注意!! i的值等於10的時候迴圈即會結束,換句話說第3行只會產生.btn0~.btn9,然後跳開迴圈。 接下來讓我們再來看一個讀檔案的例子,下方的程式可以讓使用者選擇一個檔案,並且把所選中檔案的內容全部輸出,程式碼如下:

請把焦點放在第2~9行。第2行的tk_getOpenFile會呼叫出選擇檔案的對話方塊,然後把選中的檔案路徑儲存在變數f裡。第3行判斷使用者是否有選擇檔案,若有的話才執行4~8行。

程式的第4行以讀取模式開啟剛才選中的檔案,然後把控制檔案的代碼儲存在fd變數裡。第5行的while條件式會判斷檔案是否讀取完畢,如果還沒就反覆的執行讀取檔案的動作,一直到讀取完畢才跳開迴圈。最後程式的第8行把讀完的檔案關起來,以免佔用資源。

關於檔案I/O的操作,後面的章節會有較詳細的說明,目前我們只要知道第5行的eof命令可以用來判斷檔案是否已經讀取完畢,若是的話就回傳「真」,否則回傳「假」。而「!」可以用來把真假值反相。所以整個迴圈條件式的意義就是「如果檔案還沒有讀到結尾,就繼續執行迴圈」。另外第6行的gets命令可以一次讀取一行檔案裡的內容,而讀取到的內容被我們用puts命令輸出顯示。

§ 關於檔案操作

檔案操作在以後的章節會更詳細的說明,看不懂的話請不要太在意。讀檔案是個很常見的動作,正常的情況下還沒有讀取之前,我們不容易知道它總共有多少行,上方的程式示範了while在不確定迴圈數的使用時機,這個例子裡不管檔案有多少行都會被正確的讀出。

特別注意!!下方的程式範例會造成迴圈永遠不會結束的無窮迴圈,請小心的避開它。

上方程式的第2行使用了雙引號包夾迴圈條件,根據代換規則$i會在while執行前先被代換為0,相當於如下的程式,不管變數i的值如何改變都不會影響到while的迴圈條件,而造成不會停止的無窮迴圈。

7.2 for迴圈命令

for迴圈是語法比較複雜一點的命令,但拿來執已知次數的迴圈非常的好用,它的語法如下:
for start test next body
start在for命令執行時會無條件先執行一次的程式碼。
test迴圈的條件式用來決定body是否執行。
next每次執行完body命令時會緊接著執行一次的程式碼。
body當條件式test為「真」時要被執行的程式碼。
當for命令開始執行時start會無條件先執行一次,目的是用來初始化迴圈變數。接下來for命令判斷條件式test,如果結果是邏輯的「真」就依序執行body及next,否則結束迴圈。然後就一直重覆著判斷條件式,然後執行body及next的動作,一直到條件式為假才結束迴圈。其中next的目的是用來設定迴圈變數的增量。for迴圈的運作流程可以看成這樣:
step1 : 程式執行到for命令。 step2 : 視情況先處理start、test、next、body的代換動作。 step3 : 無條件執行一次start中的程式碼。 step4 : 運算條件式test。如果結果為「真」跳到step5,否執跳到step6。 step5 : 依序執行body(先)及next(後)的內容,執行完成跳回step4。 step6 : for命令執行結束。
下面的程式用了很笨的方法來計算1加到100,但用來展示for迴圈的用法還滿適合的。 程式的第1行建立了一個變數sum並設定初值是0,我們用它來儲存1累加到100的結果。第2行的start程式碼建立了初值為1的變數i,迴圈的條件test設定為「i的值在小於等於100」時都要執行body及next程式區塊。第3行body程式區塊內,使用incr命令把i的值累加到sum變數裡,next程式區塊則在每次sum累加完成後把i累加1。最後程式的第5行把累加後的結果顯示出來。 在這個程式裡body程式區塊總共會執行100次,i的值會對應目前正執行到第幾次,每次迴圈執行incr會不斷的把i的值累加到sum裡面。

7.3 foreach迴圈命令

foreach是專為清單設計的迴圈命令,它可以讓你很方便的逐一處理清單中的項目,其語法如下:
foreach varName list body
foreach將清單list中的項目值逐一取出,並設定給varName指定的變數,每設定一次varName指定的變數,就緊接著執行一次body程式碼。如下是一個簡單的示範:

這個程式的第1行建立一個9個項目的清單,每個項目都代表一個方位。第2行中list1的項目會逐一設定給變數item,而且每設定一次item第3行就跟著被執行一次。label命令可以用來建立文字標籤,它的-anchor參數可以用來指定文字標籤內部對齊的方位,-bd用來指定邊框大小,-relief用來指定邊框的樣式。程式執行的畫面如下:

圖7-2 程式執行畫面

7.4 break及continue命令

break及continue命令都可以用來改變迴圈執行的流程,它們通常出現在while、for或foreach等迴圈命令的body程式區塊內。如果在body內遇到break命令,break命令會強迫結束目前執行中的迴圈命令。如果在body內遇到continue那麼程式會當做目前一圈的迴圈已執行完成,然後繼續執行下一次圈的動作。如下是break命令的範例:

程式的第2行使用if命令讓變數i的值為5的時候執行break,遇到break程式會立刻跳出迴圈由第5行繼續執行。它的執行結果如下:
0
1
2
3
4
迴圈結束
以下是continue的使用範例:

程式的第2行if命令會讓item值為banana的時候跳過第3行程式。換句話說只要item值為banana時第3行都不會被執行。它的執行結果如下:

apple
bery
tomato
cherry

7.4 本章回顧

迴圈命令可以讓某些程式碼有條件的反覆執行,而Tcl提供了for、foreach及while等3個迴圈命令,其中for常用來執行已知次數的迴圈,while常用來執行未知次數的迴圈,foreach則是專門用來串繞清單的迴圈,它們的語法如下:

while test body

說明:

step1 : 直譯器執行到while命令。
step2 : 如果需要的話先處理test及body的代換動作。
step3 : 運算條件式test, 如果執行結果是「真」就執行step4,否則就跳到step5。
step4 : 執行body的內容,執行完跳回step3。
step5 : 結束while迴圈。

產生10個按鈕的使用範例:

for start test next body
說明:
step1 : 程式執行到for命令。 step2 : 視情況先處理start、test、next、body的代換動作。 step3 : 無條件執行一次start中的程式碼。 step4 : 運算條件式test。如果結果為「真」跳到step5,否執跳到step6。 step5 : 依序執行body(先)及next(後)的內容,執行完成跳回step4。 step6 : for命令執行結束。
計算1加到100的笨方法:
foreach varName list body
說明: foreach將清單list中的項目值逐一取出,並設定給varName指定的變數,每設定一次varName指定的變數,就緊接著執行一次body程式碼。 使用範例:

break及continue命令可以用來改變迴圈執行的流程,如果在迴圈的body內遇到break命令,break命令會強迫結束目前執行中的迴圈命令,如果在迴圈的body內遇到continue那麼程式會當做目前一圈的迴圈已執行完成,然後繼續執行下一次圈的動作。

33 個意見

匿名 | 2010年3月17日 晚上9:12

Dear Dai 大:
我又來問問題了.

在 範例 7-1
set i 0
while {$i < 10} {
button .btn$i -text "按鈕$i" -command "puts $i"
pack .btn$i
incr i
}

請問為什麼 commnad 後面的 puts $i 一定要用 "" 括起來.
如果用 -command { puts $i } 會印出 10. 理由是什麼?
謝謝
Hunt

dai | 2010年3月18日 上午10:31

hi

這是因為Tcl代換規則,產生的特性,使用雙引號的話,在$i的值會在每一次迴圈中被代換成對應的值,但如果用大括號的話,$i的值就只會在按下按鈕時才會被代換成值,也就是說你在按按鈕時其實迴圈早就跑完了,所以你印出來的都是10.

isPeter | 2010年11月9日 下午4:09

有錯字:
第5行的while條件式會判斷讀案是否讀取完畢
讀案 => 檔案

dai | 2010年11月20日 上午9:34

已修正了,謝謝。

匿名 | 2011年3月14日 下午5:35

7.4(第二行最後少一個"{")
set sum 0
for {set i 1} {$i < = 100} {incr i}
incr sum $i
}
puts $sum

dai | 2011年3月15日 上午10:39

匿名的朋友 錯誤修正了,謝謝。

五香乖乖 | 2012年2月9日 下午3:37

Dear Dai 大您好:
感謝您寫的文章,解說加上範例的操作,讓我很容易吸收,今天再練習 7-1 while 範例時,我將範例內容改成以下兩種,就無法運作,想請問是什麼原因造成,可以幫小弟解惑一下嗎?

原始檔如下 範例 7-1
set i 0
while {$i < 10} {
button .btn$i -text "按鈕$i" -command "puts $i"
pack .btn$i
incr i
}

*****
test1
*****
set i 0
while {$i < 10} {
button .btn$i -text "按鈕$i" -command "puts $i" pack .btn$i
incr i
}

*****
test2
*****
set i 0
while {$i < 10} {button .btn$i -text "按鈕$i" -command "puts $i" pack .btn$i incr i }

dai | 2012年2月11日 上午9:49

set i 0
while {$i < 10} {
button .btn$i -text "按鈕$i" -command [subst {
puts $i
pack .btn$i
}]
incr i
}

五香乖乖 | 2012年2月13日 上午10:22

Dear Dai大 您好:
又來麻煩您了,因為第一次接觸tcl語法,所以我可能有表達不清楚的地方,因為看到while命令的述敍為
while test body我是否可以將語法以一行方式寫完變成右例形式像是 while {test}{body},第一個
大括號內容表示test,第二個大括號內容表示body實際寫完如下:

set i 0
while {$i < 10} {button .btn$i -text "按鈕$i" -command "puts $i" pack .btn$i incr i }

因為原始檔沒有錯是可以執行的,但我將程序改成一行後產生不能執行的狀態,不知原因發生在哪,是因為有迴圈所以需有一定架構,需寫成斷行才能順利產生還是其他問題所引發,這是讓我不解的地方,還請 Dai大 有空的時候幫我開釋一下,謝謝!

dai | 2012年2月13日 上午10:52

抱歉~我誤會你的意思了 ~

如果一行裡面要放多個敘述(命令),只要每個敘述中間用「分號」隔起來就好了,例如:

set i 0
while {$i < 10} {button .btn$i -text "按鈕$i" -command "puts $i" ; pack .btn$i ; incr i }

五香乖乖 | 2012年2月13日 下午2:11

Dear Dai大 您好:

謝謝您的回答,問題已解決,因為您的網頁寫得很讓人明瞭,所以我目前每天都會利用一些時間參考您的網頁逐章做一個學習,之後有問題時可能會不時發問,還請您不要覺得麻煩!^^.

ps:謝謝您提供這樣的環境讓大家能夠學習,感恩!

匿名 | 2012年3月7日 下午6:41

Dear Dai大 您好:
想請問一下 7-11的範例
set i 0
while {$i < 10} {
button .btn$i -text "按鈕$i" -command "puts $i"
pack .btn$i
incr i
}
我想改成 在command 做不只一件事 就舉例來說 按下 按鈕1 會puts 1
但我還想要說 一直累加 看我總共按了幾次按鈕 在按下某按鈕時 也可順便顯示出總共次數
這樣應該要怎麼寫?
我把它改成 command{} 就會有問題 拜託交一下 :)
best regard, Steve.

dai | 2012年3月7日 晚上8:23

那就...改成類似這樣

proc foo {i} {
# 所有你要做的事寫在這
}

然後把button改成類似這樣

button .btn$i -text "按鈕$i" -command [list foo $i]

匿名 | 2012年3月7日 晚上8:50

Dear dai大:
ㄜ 謝謝你的回應 不過我大概想做這種感覺
set i 0
set j 0

proc foo {i} {
# 所有你要做的事寫在這
puts "total: $j"
j++
}
#然後把button改成類似這樣
#button .btn$i -text "按鈕$i" -command [list foo $i]

while {$i < 10} {
button .btn$i -text "按鈕$i" -command [list foo $i; "puts $i"]
pack .btn$i
incr i
}
可是這樣有bug 最近才開始學tcl 不太懂要怎麼改?

匿名 | 2012年3月7日 晚上8:58

我j++ 有改成incr j 了

dai | 2012年3月10日 晚上10:24

那改成這試試


while {$i < 10} {
button .btn$i -text "按鈕$i" -command [subst {foo $i; "puts $i"}]
pack .btn$i
incr i
}

Bravo | 2012年4月14日 晚上11:57

Dear Dai:
目前接觸不久tcl語法,想請教關於若是我有ijk等等變數,我希望當我i增加一個單位時,下一次為j增加一單位,再下一次為k增加一單位,
for {set i 0.2} {$i<=1} {set i [expr $i+0.2]} {
}
for {set j 0.2} {$i<=1} {set j [expr $j+0.1]} {
}
for {set k 0.2} {$i<=70} {set k [expr $k+5]} {
}
想請教您該如何修改,才能達到我需要的想法,不好意思打擾您了!!

Bravo | 2012年4月15日 上午11:09

Dear Dai:
目前剛接觸TCL語法不久,想請教您一些問題,由於我的目標式由ijk影響並輸出,假設我有ijk等變數,我希望當i增加一個值之後,下一次為j增加一個單位值,再下一次為k增加一個單位值,依序反覆此動作,想請叫我應該如何修改可以得到上述我需要的變動,感謝您的指導
for {set i 0} {$i < 1} {incr 0.1} {
}
for {set j 0} {$j < 1} {incr 0.1} {
}
for {set k 30} {$j < 70} {incr 5} {
}

dai | 2012年4月15日 中午12:56

不知道有沒有錯錯意,你是想要這樣嗎??

for {set i 0} {$i < 1} {incr 0.1} {
for {set j $i} {$j < 1} {incr 0.1} {
for {set k $j} {$j < 70} {incr 5} {

}
}
}

Bravo | 2012年4月17日 晚上10:45

Dear dai:
感謝您的解答,你的回覆讓我了解其更多的應用,不過我目前的疑惑為另外一個意思,其初始i,j,k這三個變數可以讓我輸出不同值,各有各的參數範圍,故我希望先條動i看出並輸出值,輸出的目標式我已經寫好,下一步為調動j的值再輸出,再下一步為調動k以後的值並輸出,依序反覆的動作可以使我在我所調動的i,j,k之下得到我的輸出值,一個i,j,k值增加的循環,不過希望是以i先增加之後換j再之後換k一直循環到我設的邊界,想請教這在tcl是可以做到的嗎
感謝您的不吝指導,by Bravo.

Bravo | 2012年4月18日 上午10:45

Dear dai:
感謝您的回覆使我了解到其更多的應用,上述使我了解到額外的應用,但我原意為,其原先有i,j,k三個變數,其每個變數增加時都會影響到其結果, step1.先看i增加一個單位時的結果, step2.當i增加一單位之後換j增加一單位得到其結果, step3.當i和j增加一單位候k增加一單位並得到結果,i>j>k>i>j>k>.....循環到我的上限,想請教老師tcl語法是否可以做到此效果,感謝您的不吝指導 by Bravo.

dai | 2012年4月18日 中午12:20

抱歉!! 有點看不懂你的問題...請問有例子可以說明? 或是實際的問題?

但由你的結果來看 i > j > k > i ....

像是需要用 負量的增量 讓 i j k 一直變小??

匿名 | 2012年4月18日 下午1:13

是這樣嗎
set i 0
set j 0
set k 30
set ind 0
puts "init:\ti=$i j=$j k=$k"
while {$i<1 || $j<1 || $k<70} {
switch [expr $ind%3] {
"0" {
if {$i<1} {
set i [expr $i+0.1]
puts -nonewline "change i:\t"
puts "i=$i j=$j k=$k"
}
}
"1" {
if {$j<1} {
set j [expr $j+0.1]
puts -nonewline "change j:\t"
puts "i=$i j=$j k=$k"
}
}
"2" {
if {$k<70} {
incr k 5
puts -nonewline "change k:\t"
puts "i=$i j=$j k=$k"
}
}
}
incr ind
}

Bravo | 2012年4月18日 下午3:12

Dear dai:
老師不好意思 一直都口語話描述好我的問題,目前有三個變數再變動都會影響到我的輸出值,其三個變數我定義為i,j and k,
i的範圍為0~1, j範圍為0~1 ,k範圍為30~70,其各個值都會有不同的輸出值,我希望能將他一次輸出我想要的,
例如:原始值為 i=0, j=0, k=30 ,i跟j的增加量為0.1& k增加量為5,
故下次參數組合為 i=0.1, j=0 ,k=30 (i增加0.1)
再下次參數組合為 i=0.1, j=0.1,k=30 (j增加0.1)
再下次參數組合為 i=0.1, j=0.1,k=35 (k增加5)
再下次參數組合為 i=0.2 ,j=0.1,k=35 (i增加0.1)
........................................

依序是我之前提到i j k 這些變數的循環,目前還沒辦法正確的將其想法以迴圈的方式寫出來,再煩請您指導
感謝匿名的不吝指導, 目前正在測試, 謝謝

dai | 2012年4月18日 下午5:20

了解了,請試試下面的程式:

set cut 0
set vars [list 0 0 30]
set incrs [list 0.1 0.1 5]

while {1} {
lset vars $cut [expr [lindex $vars $cut] + [lindex $incrs $cut]]
set cut [expr [incr cut] %3]
lassign $vars i j k
puts [format "i = %.1f , j = %.1f , k = %d" $i $j $k]
if {$k >= 70} {break}
}

Bravo | 2012年4月20日 下午2:20

Dear dai :
感謝您的解答, 正在測試其結果

JZ | 2012年5月20日 晚上9:15

Dai大大你好,我想請問說呀,在foreach指令中,
我想先設定一個清單,然後在將這個清單內的成員一個個依序取出來
ex:
set list [list 1 2 3 4 5]
foreach abc {$list} {
puts $abc
}
但這樣的方式是行不通的= =
我想請問Dai大大說,是不是只能用for迴圈來完成類似這樣的操作
謝謝你:D

JZ | 2012年5月20日 晚上9:22

哈哈 Dai大大,拍謝拍謝
我看到你上面打的教學了XD
謝謝你:D

匿名 | 2015年1月12日 晚上9:51

DAI你好

最近寫一個回圈可是之後用spawn就無法回到for 回圈
可以請你執導一下嗎
for {set i 1} {$i < 10} {incr i} {
.......
.......
.......
spawn plink $host -ssh
expect "login:" {send "admin\r"}
expect "password:" {send "admin\r exit\r"}
}

匿名 | 2015年1月13日 上午11:36

不好意思,expect我沒有在用,要看有沒有熱心人士幫忙回答了!!

匿名 | 2015年1月14日 晚上7:54

謝謝

Unknown | 2015年1月15日 下午5:34

留言好像有問題= =?

匿名 | 2016年1月11日 晚上10:45

修正了,謝謝~

留下您的意見

Theme Design by devolux.org. Converted by Wordpress To Blogger for WP Blogger Themes. Sponsored by iBlogtoBlog
This template is brought to you by : allblogtools.com | Blogger Templates