HotXLS 是原生 Delphi 與 C++Builder 的 Excel 程式庫,透過 TryGetCachedFormulaValue 與 IXLSFormulaCacheReader 讀取 Excel 早已存放在公式旁的那個值。這兩個進入點都不會呼叫計算器、不會反組譯公式語彙、不會更新髒旗標,也不會把任何東西寫回模型,所以您唯讀開啟的活頁簿會與開啟時完全一致
促成這件事的情境很枯燥,卻極為常見。一個夜間作業開啟幾百個別人產生的活頁簿,從每個活頁簿抽出一欄總計,再把數字推進資料倉儲。這些總計早已躺在檔案裡——Excel 算好並存下來了。但作業一向公式儲存格詢問值的那一刻,對這個問題只有一種答案的程式庫就會建立相依性圖、評估整張工作表,於是一個理應受 I/O 限制的作業變成了計算效能測試
為什麼讀一個公式儲存格要付出完整重算的代價?
因為在公式儲存格上取值,等於是要求產生一個值,而唯一普遍正確的產生方式就是評估該公式。對於會編輯活頁簿的應用程式,這是正確的預設行為;對於只擷取資料的管線,這卻是錯誤的預設。更糟的是,評估並非沒有副作用:它會把結果寫回儲存格、翻轉髒旗標,而且當某個函式不受支援或外部參考斷裂時,它算出的結果可能與當初產生檔案的應用程式不同。您跟維運團隊描述成唯讀的作業,會悄悄產生一個與磁碟上不再一致的活頁簿;若之後有任何東西把它存檔,磁碟上的檔案也就跟著改了
讀取快取值則是這份契約的另一半。它回答一個更窄的問題——當初產生檔案的應用程式在這裡存了什麼?——並拒絕回答其他任何問題。當您真的想要最新的數字時,HotXLS 仍提供由相依性圖驅動的增量重算;重點是擷取與評估應該是兩個不同的呼叫,而不是一個有兩種心情的呼叫
關於一個儲存格的三件正交事實
先講結論:一個公式快取值承載三件獨立的事實,把它們壓成單一 Variant 會丟掉您需要的資訊。TXLSFormulaCacheInfo 把三者分開保存為 State、Kind 與 Value。TXLSFormulaCacheState 以五種狀況記錄出處——xlfcsNotFormula、xlfcsMissing、xlfcsLoaded、xlfcsCalculated 與 xlfcsInvalidated——TXLSFormulaCacheValueKind 則把承載內容分類為 xlfcvBlank、xlfcvNumber、xlfcvDateTime、xlfcvString、xlfcvBoolean 或 xlfcvError。正是這種分離讓「是否存在」能被誠實回報:快取的空白、快取的空字串、快取的 False、快取的零與快取的錯誤都是真實的值,所以「存在」永遠不能從 VarIsEmpty 或 VarIsNull 推斷。TryGetCachedFormulaValue 只在 xlfcsLoaded 與 xlfcsCalculated 時回傳 True,回傳 False 時仍會填入可供診斷的狀態
var
Book: TXLSXWorkbook;
Info: TXLSFormulaCacheInfo;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('quarterly-model.xlsx');
// 這裡的 SheetIndex、Row 與 Col 全部是一基
if Book.TryGetCachedFormulaValue(1, 12, 5, Info) then
Writeln('cached value: ', VarToStr(Info.Value))
else
Writeln('no usable cache, state ordinal ', Ord(Info.State));
finally
Book.Free;
end;
end;
為什麼快取值會缺失?
TryGetCachedFormulaValue 回傳 False 的原因恰好有四個,而狀態會告訴您是哪一個。xlfcsNotFormula 表示該儲存格放的是字面值或根本沒有東西,座標超出範圍也會歸併成同樣的答案。xlfcsMissing 表示該儲存格確實是公式,但產生端沒有為它存放值承載——產生器只寫公式、讓 Excel 在首次開啟時填入結果,常見的結局就是如此。xlfcsInvalidated 表示公式文字在載入後被取代,所以原本在那裡的值描述的是一個已不存在的運算式。相對地,xlfcsCalculated 是成功案例:它標記的是您自己的程式碼或 HotXLS 評估器在本次工作階段產生的值,與來自檔案的 xlfcsLoaded 相對
對缺失快取誠實,比把它粉飾過去更重要。HotXLS 拒絕捏造值,儲存時也同樣嚴格——只有 xlfcsLoaded 與 xlfcsCalculated 會輸出快取值,xlfcsMissing 與 xlfcsInvalidated 則只寫公式,而不會把過時的數字凍結進檔案。這讓您在管線中有三種合理的回應:跳過該列並記下缺口、刻意對那一個活頁簿重算並接受其成本,或評估後再對帳。若評估出的數字與當初產生檔案的應用程式會寫入的不一致,公式評估追蹤器就是找出兩次計算在哪裡分歧的工具,而不是憑結果猜測
一個讀取器橫跨傳統、OOXML 與 ODF 三種引擎
管線不該在意它剛開啟的檔案是 BIFF、OOXML 還是 ODF。IXLSFormulaCacheReader 是三者共用的唯一唯讀進入點:TXLSWorkbook.CreateFormulaCacheReader 與 TXLSXWorkbook.CreateFormulaCacheReader 都會在各引擎既有的稀疏儲存格查詢之上,回傳一個輕量型配接器,並使用相同的一基工作表、列與欄座標。活頁簿類別刻意不自己實作這個介面——對活頁簿的介面參考會改變其擁有權語義,讓呼叫端溜過生命週期租用。相反地,終結活頁簿會清除該租用內的原始指標,您的程式碼仍持有的任何讀取器會在下次查詢時擲出 EXLSFormulaCacheReaderInvalidated,而不是去解參考已釋放的記憶體。這是速敗式的生命週期檢查,不是並行保證
var
Reader: IXLSFormulaCacheReader;
Info: TXLSFormulaCacheInfo;
Row, Missing, Errors: Integer;
Total: Double;
begin
Reader := Book.CreateFormulaCacheReader;
Total := 0;
Missing := 0;
Errors := 0;
for Row := 2 to LastRow do
if Reader.TryGetCachedFormulaValue(1, Row, 7, Info) then
begin
case Info.Kind of
xlfcvNumber: Total := Total + Double(Info.Value);
xlfcvError: Inc(Errors);
end;
end
else if Info.State = xlfcsMissing then
Inc(Missing);
// 沒有計算器執行,沒有髒旗標變動,Book 維持不變
end;
快取位元組實際住在哪裡
對於傳統 .xls 檔案,快取就是 Formula 記錄的 FormulaValue 欄位,即 [MS-XLS] §2.5.133 描述的八個位元組。當高位字組等於 $FFFF 時,承載內容不是 IEEE 754 雙精度浮點數,而是一個帶標記的變體,而且其版面很容易出細微錯誤:變體型別放在 val[0],布林或 BErr 承載放在 val[2],val[1] 則未定義。HotXLS 先前從 val[1] 讀取承載,這是那種只會在「快取的是布林或錯誤而非數字」的特定檔案上浮現的差一錯誤。讀取器與共用公式寫入器現在採用一致的偏移,所以快取的 TRUE 能在載入與儲存後完好倖存,而不是衰變成雜訊
封裝格式中的型別保真則是另一個帶有自身陷阱的問題。在 OOXML 中,快取值掛在 c 項目下作為 <v>,型別由 t 屬性依 ECMA-376 Part 1 §18.3.1.4 命名。HotXLS 把 t="e" 直接讀成 varError Variant,並在儲存時對映回標準錯誤文字,所以錯誤絕不會偽裝成普通整數——但 Delphi RTL 在這裡幫不上忙,因為 VarAsType(Integer, varError) 會擲出轉換例外。可行的建構方式是直接設定 TVarData.VType 與 TVarData.VError。日期則以相反方向遵循同一份紀律:t="d" 與 ODF 的日期值型別是明確的型別宣告,會成為 varDate;而 BIFF 數值快取完全不帶日期旗標,因此維持 Double。HotXLS 絕不從儲存格的數值格式猜測日期,因為數值格式是呈現,快取才是資料。ODF 還多了一個值得知道的狀況——office:value-type="void" 表達一個「存在但不帶值」的快取,而且由於 ODF 沒有錯誤值型別,看起來像錯誤的文字會以文字保留,而不是被提升為錯誤
function DescribeCache(const Info: TXLSFormulaCacheInfo): string;
begin
case Info.State of
xlfcsNotFormula: Result := 'not a formula cell';
xlfcsMissing: Result := 'formula stored with no cached value';
xlfcsInvalidated: Result := 'formula replaced since load';
else
case Info.Kind of
xlfcvError: Result := 'error code ' + IntToStr(TVarData(Info.Value).VError);
xlfcvDateTime: Result := DateTimeToStr(VarToDateTime(Info.Value));
xlfcvBoolean: Result := BoolToStr(Info.Value, True);
xlfcvNumber: Result := FloatToStr(Double(Info.Value));
xlfcvString: Result := VarToStr(Info.Value);
else
Result := 'present but blank';
end;
end;
end;
共用公式會共用它們的快取值嗎?
不會,而假設會,就是一次掃描最後對整欄回報同一個數字的原因。OOXML 共用公式只共用公式運算式與儲存最佳化;每個成員儲存格仍擁有自己的 <v>。因此 HotXLS 絕不把根成員的快取傳播給一個到來時沒有值的從屬成員,而以 xlfcsMissing 載入的從屬成員在儲存並重新開啟後仍回報 xlfcsMissing。如果您正在理解這個群組當初是如何儲存與展開的,共用公式 si 屬性及其展開的機制另有專文說明;就快取讀取而言,規則可化約為一行——詢問每個儲存格,不相信任何您沒親自詢問得來的東西
快取值讀取、統一的跨引擎讀取器,以及您可以選擇不呼叫的重算引擎,都隨適用於 Delphi 與 C++Builder 的標準 HotXLS Delphi 試算表元件出貨,不依賴 Excel,也不依賴任何 OLE 自動化伺服器;產品頁面附有本文所示活頁簿與讀取器進入點的完整 API 參考