2018年10月4日 星期四

函數在迴圈內時,也許跟你想得不一樣?

如果你需要在程式碼中處理有特定規律且重複做的事情,通常會直覺的想要用迴圈去做。比如:我要讀取很多個有檔名規律的文字檔,這樣寫... 很一般、普通、正常吧

var: j(0), fileName("");
array: getting[11](0);

for j=1 to 10 begin
  fileName= "F:\" + NumToStr(j,0) + ".txt";
  getting[j]= _readTXT( fileName );
end;


但是在 MultiCharts,我卻發現這會跟我們想像的不大一樣~ 至少,上面那樣寫的效果,跟下面這麼寫的就不見得會相同咧(以下這種寫法才不大正常啊 XD)。

array: getting[11](0);

getting[1]= _readTXT( "F:\1.txt" );
getting[2]= _readTXT( "F:\2.txt" );
getting[3]= _readTXT( "F:\3.txt" );
getting[4]= _readTXT( "F:\4.txt" );
getting[5]= _readTXT( "F:\5.txt" );
getting[6]= _readTXT( "F:\6.txt" );
getting[7]= _readTXT( "F:\7.txt" );
getting[8]= _readTXT( "F:\8.txt" );
getting[9]= _readTXT( "F:\91.txt" );
getting[10]= _readTXT( "F:\10.txt" );

後文就是分享上述這個為何所得不相同的測試、觀察與結論。
====================


我先建一個讀取文字檔(僅一行,把讀到的內容轉成數字回傳)的函數 _readTXT,參數是檔案的路徑、名稱。函數的型態選用 Simple ,盡量避免 Series 的繼承特性。


函數的程式碼:
DefineDLLFunc: "kernel32.dll", Long, "_lopen", lpstr, Long;
DefineDLLFunc: "kernel32.dll", Long, "_lread", Long, lpstr, Long;
DefineDLLFunc: "kernel32.dll", Long, "_lclose", Long;


input: FileName(StringSimple);
var: content(""), temp(""), theFile(0), Len(50);

theFile= _lopen( FileName, 0 );


if Len=50 then begin
 temp= "";
 Len= _lread( theFile, temp, 50 );
 content= MidStr( temp, 1, Len );
end;

_lclose( theFile );

if content<>"" then
  _readTXT = StrToNum( content )
else
  _readTXT = -999999999;



接著,再開一個指標,隨便找一張圖放上去以驅動它,先產生 10個文字檔,讓後續的測試有檔案可以讀。後面就都用 Print 去觀察不同的狀況。


準備工作完成,現在讓我們一起來走過這趟測試的旅程吧~

首先,直接使用迴圈的方式來多次呼叫函數 _readTXT 依序去讀那 10個文字檔,看看會得到什麼?從這裡可以看到 filName(就是檔案名稱,下給 _readTXT 的參數),在迴圈中是有變化的,但是 getting( _readTXT 回傳的內容),卻都是 1,不是 1、2、3....、9、10,而 1 剛好就是 1.txt 的內容,這也許讓你猜想 _readTXT 似乎只執行了一次?但是它是被放在迴圈中,應該要執行 10次的啊?難道 fileName 每個 step 都有執行,函數卻跳過不執行?不會吧~


 我們先看一下,如果不是使用迴圈方式,而是寫多次函數呼叫的 code(如果我要讀100個、1000個有檔名規律的文字檔呢???)會是怎樣的回傳?我們看到 _readTXT 是可以正確回傳每個文字檔的內容。


到這裡,我們已經可以看出,在直覺上,使用迴圈與把每個動作逐行寫出,應該是一樣效果的期待,在 MultiCharts 卻得到不同的反應了 =_=

再來,讓我們試著找看看,可能的原因是什麼?否則,難道以後我打算多次執行函數,都不能用迴圈嗎?要 Call 10次、100次、1000次,就要寫 10行、100行、1000行嗎 T_T

讓我們進到函數內容這邊~先來看看 fileName 這個參數,雖然在指標的迴圈中的確是每個 step 都會變動,但它的變動到底有沒有進到 _readTXT 呢?從 print 出來的內容,看來參數的變動是有進到函數的,這表示 _readTXT 是真的有去讀 1.txt、2.txt、3.txt... 的,這也表示著,在指標迴圈中下函數,是真的有每個 step 都有執行函數的,只是...這回傳出去的卻都是 1 ?!


看來,函數放在迴圈中執行的過程,恐怕跟我想像的不一樣囉。因為接下來的測試、反應,讓我高度懷疑,在迴圈中的函數,並不是每個 step 的呼叫執行都有 renew 一般的重新執行,函數中的變數卻是有著像是 繼承 的效果?即使我已經把函數型態指定為 Simple 了。

如何佐證?讓我把 print 搬個位置,因為 _readTXT 的回傳值並不是直接去讀完檔案就丟出去,而是得經過檔案內容的解析、擷取才回傳出去的。所以,我試著把 print 放到讀取、擷取的那段裡去。

看著上圖與上上圖做比較,不知道你是否發現貓膩呢?上圖 print 所在位置先經過的判斷式是;if Len=50 then...,而往上看一直到 變數Len 宣告的地方,中間沒有任何對 Len 有變動的 code 啊,那 Len 宣告時的預設值就是 50,到了判斷是這一行時,除非在第一次讀了 1.txt 後 Len 變成不是 50了,否則沒有道理 print 只有出現 1.txt,而沒有後面的 2.txt、3.txt...

所以,我們是不是該懷疑指標迴圈內的函數中變數並不是每次呼叫都是重新執行函數全部的 code,而是第一次執行函數後,函數的變數並沒有 renew 過?但再對照另外一個逐行寫出不同檔案名稱,多次呼叫函數的寫法,卻可以正確讀到各文字檔的內容,這又表示每一行的函數呼叫,的確是有 renew 的。



結論:當函數在迴圈中被多次呼叫,其實並不是真正的<新執行>,函數內的變數被改寫過後,不會因為函數 code 的變數宣告而有像是重置的效果。但直接寫多次函數呼叫的指令(真碼農啊XD),卻是真正多次的<新執行>。


最後,改寫一下這個函數,解決問題~


本文的函數內容是從 Amin大 所分享的文章改寫而來,再經 立偉大 參與討論,分享他自己的函數給我看,才發現這個特性,感謝!

可以下載這個能讀取內容僅有一行且為50位數內的文字的函數程式碼,供你測試、學習。