若要在 Delphi 中計算刻意的循環參照,HotXLS 在其 XLSX 引擎上提供了疊代計算功能:將 TXLSXWorkbook.Iterate 設定為 True,則 TXLSXWorkbook.Recalculate 會走訪偵測到的每個參照循環以逼近固定點——最多執行 IterateCount 次,或直到每個儲存格的變化量小於 IterateDelta——而不是直接傳回 #REF! 並宣告放棄
這種差別比單一布林值所暗示的更為重要。偵測到參照循環並拒絕在其中進行無休止迴圈的同一個引擎,在切換了某個屬性後,會刻意評估該循環直到其數值穩定。釐清這兩種行為——何時將循環視為應回報的缺陷,以及何時將其視為要求解的模型——正是本文的主題
為什麼循環參照預設會回報錯誤?
預設情況下,HotXLS 將任何參照循環視為撰寫錯誤並加以回報,而非進行計算。TXLSXWorkbook.Recalculate 建立公式相依性圖表,按拓撲順序評估每個公式儲存格,並在發現循環(即在拓撲排序期間無法釋放的節點)時立即傳回 lxErrorRef。這些循環成員會保留其先前的快取值;循環之外的每個公式仍會正常評估。該圖表的機制以及為何跳過循環成員而非對其進行迴圈,已在隨附文章增量公式重新計算與相依性圖表中介紹
預設行為是安全的,編譯為大多數循環都是 Bug:例如加總行不小心被包含在其自身的 SUM 範圍內,或者複製貼上時將參照轉向了自身。在重新計算時出現明確的錯誤代碼正是您對這些情況所期望的。然而,有一類特定且重要的模型刻意採用了循環結構。複利計算時程表、部門之間循環的成本或間接費用分配,以及由餘額驅動的費用計算,都描述了數值合法地回饋至其自身輸入的過程——且 Excel 只有在使用者勾選「檔案」→「選項」→「公式」→「啟用疊代計算」時才會計算它們
如何在 HotXLS 中啟用疊代計算?
HotXLS 透過 TXLSXWorkbook 上的三個屬性來對應 Excel 的該核取方塊:Iterate 用於開啟該模式,當它為 True 時,偵測到的循環會被交給疊代求解器,而非產生 lxErrorRef。以典型的複利儲存格組為例,其中期末餘額取決於利息,而利息又取決於餘額:
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Model');
Sheet.Cells[1, 2].Value := 1000; // B1: 期初本金
Sheet.Cells[2, 2].Value := 0.05; // B2: 當期利率
Sheet.Cells[3, 2].Formula := 'B1+B4'; // B3: 餘額 = 本金 + 利息
Sheet.Cells[4, 2].Formula := 'B3*B2'; // B4: 利息 = 餘額 * 利率
Book.Iterate := True; // 選擇加入疊代計算
if Book.Recalculate = lxOk then
// B3 收斂至 1052.63..., B4 至 52.63...
Report(Sheet.Cells[3, 2].Value);
finally
Book.Free;
end;
end;
B3 參照 B4,而 B4 參照 B3,因此相依性圖表會回報一個雙節點循環。在 Iterate 保持其預設值 False 時,該儲存格組將傳回 lxErrorRef,且兩個儲存格的值都不會穩定。若將其設定為 True,Recalculate 會以當前快取值作為循環的初值,並逐次重新評估其成員,將每次計算的輸出作為下一次的輸入,直到數值不再變動。在這裡,其數學封閉形式為 principal / (1 - rate),因此餘額穩定在 1052.63,利息穩定在 52.63——這與 Excel 在啟用疊代時產生的資料完全相同
是什麼讓疊代停止?
兩個獨立的停止條件約束了求解器,理解這兩者能防止模型出錯或陷入無休止計算。IterateCount 是重新評估循環成員次數的硬性上限,預設為 100(與 Excel 一致)。IterateDelta is the convergence threshold: after each pass the solver measures the largest numeric change across all cycle cells, and once that maximum change drops below IterateDelta — default 0.001 — the pass loop breaks early. Whichever condition is met first ends the iteration
Book.Iterate := True;
Book.IterateCount := 1000; // 硬性上限:對循環最多進行 1000 次計算
Book.IterateDelta := 0.0001; // 收斂:一旦每個儲存格的變動量 < 0.0001 則停止
case Book.Recalculate of
lxOk:
// 循環已收斂,或者達到了 1000 次的上限並保留了
// 上一次疊代的值 -- 當 Iterate 為 True 時,這兩條路徑都會回傳 lxOk
SaveWorkbook(Book);
lxErrorRef:
// 僅在 Iterate = False 時可達:循環已被回報,而非求解
LogWarning('疊代已停用下的循環參照');
end;
有一個後果值得清楚說明,因為這是該功能實際的限制。當達到上限但變動量未低於 IterateDelta 時,SolveCycleIteratively 不會引發錯誤——它會傳回 lxOk 並使儲存格保留最後一次疊代的值,正如 Excel 在達到其疊代限制且未收斂時寫入最後計算的數值一樣。因此,在疊代模式下 Recalculate 傳回成功代碼意味著「求解器已執行」,而不是「求解器已收斂」。回饋迴圈發散或震盪的模型會悄悄耗盡所有 IterateCount 次數,並傳回完全不是固定點的數值,且沒有任何異常會標記此區別
該設定如何儲存至 XLSX 和 XLS 檔案中?
疊代計算設定會保留在兩種試算表格式中,因此在 Excel 中開啟的活頁簿會依照您的 Delphi 程式碼所設定的方式運作。在 XLSX 端,寫入器僅在 Iterate 為 True 時輸出 OOXML <calcPr> 元素,並省略任何仍處於預設值的屬性以保持最小的輸出:使用預設值的活頁簿僅寫入 <calcPr iterate="1"/>,而 iterateCount 僅在其不為 100 時出現,iterateDelta 僅在其不為 0.001 時出現。開啟檔案時,TXLSXWorkbook 會讀回相同的這三個屬性,因此雙向往返是對稱的
舊版的 BIFF8(.xls)引擎 TXLSWorkbook 則透過一組不同的三個屬性,在三個獨立的記錄中承載對等的狀態。EnableIteration 對應至 CalcIter 記錄($0011, [MS-XLS] §2.4.33),MaxIterations 對應至 CalcCount 記錄($000C, [MS-XLS] §2.4.31),而 MaxIterationChange 對應至 CalcDelta 記錄($0010, [MS-XLS] §2.4.32)。Setter 方法強制執行規格範圍——CalcCount 必須介於 1..32767 之間,因此限制了 MaxIterations,且負的 MaxIterationChange 會還原為 0.001 的預設值。在載入的 .xls 活頁簿上設定它們,這三個計算記錄將在儲存時被忠實寫入
var
Book: TXLSWorkbook; // BIFF8 (.xls) 引擎
begin
Book := TXLSWorkbook.Create;
try
Book.Open('model.xls');
Book.EnableIteration := True; // CalcIter 記錄 $0011
Book.MaxIterations := 500; // CalcCount 記錄 $000C(限制在 1..32767 之間)
Book.MaxIterationChange := 0.0001; // CalcDelta 記錄 $0010
Book.SaveAs('model.xls'); // 這三個計算記錄進行雙向往返
finally
Book.Free;
end;
end;
請注意刻意分開的命名方式:XLSX 引擎使用 Iterate / IterateCount / IterateDelta(OOXML 詞彙),而 BIFF8 引擎使用 EnableIteration / MaxIterations / MaxIterationChange(與 [MS-XLS] 記錄名稱一致)。這兩組屬性都描述了相同的這三個旋鈕——開啟/關閉開關、疊代次數上限和收斂差值,並具有相同的預設值(關閉、100 和 0.001)
何時循環參照是 Bug 而非模型?
啟用疊代並非讓循環參照警告消失的手段,那樣做會陷入陷阱。全域開啟 Iterate 會將每個無意間造成的循環(預設錯誤代碼原本要捕獲的錯誤)轉換為默默收斂或默默非收斂的數值。正確的規範正好相反:在一般模式下保持 Iterate 為 False,以便真實的撰寫錯誤仍能以 lxErrorRef 的形式浮現,且僅在循環性是經過設計和理解的活頁簿上啟用疊代功能
當循環確實出現且您不確定是哪種類型時,公式評估追蹤器是可以區分它們的工具:追蹤可疑的公式,折疊回自身的參照鏈就會逐步顯現,因此您可以決定它是編碼了真實的回饋迴圈還是零星的自我參照。這也有助於記住,循環儲存格可以在迴圈中呼叫任何內建函式——解決工程或複數公式的同一個計算器會在每次疊代中評估循環成員——因此發散的模型通常是迴圈內部的公式問題,而非疊代設定問題
實用的檢查清單很短。在依賴疊代之前,確認迴圈具有真實的固定點;保持 IterateDelta 足夠緊密,使「已收斂」代表您模型所需要的意義;並在預期會收斂的 Recalculate 之後,對已知輸出進行合理性檢查,而不是僅僅信任 lxOk,因為該代碼無法區分收斂與達到疊代上限的差別
循環參照的疊代計算是 HotXLS Delphi Excel 元件 中 XLSX 引擎的一部分,連同它所建立的增量相依性圖表重新計算,以及將設定寫入您建立的每個檔案中的 OOXML 和 BIFF8 持久性。對於刻意採用循環的財務和工程模型,這就是錯誤代碼與答案之間的差別