撰寫檔案時的最佳實踐方法

重要的應用程式介面

開發者在使用 FileIO 和 PathIO 類別的 Write 方法執行檔案系統 I/O 操作時,有時會遇到一組常見問題。 例如,常見的問題包括:

  • 檔案只寫入了一部分。
  • 應用程式在呼叫其中一個方法時會收到例外。
  • 這些操作留下了。TMP 檔案,檔名與目標檔名相似。

FileIO 與 PathIO 類別的 Write 方法包括以下幾項:

  • WriteBufferAsync
  • 寫入字節同步
  • WriteLinesAsync
  • WriteTextAsync

本文將詳細說明這些方法的運作方式,讓開發者更了解何時以及如何使用它們。 本文提供指引,並不試圖解決所有可能的檔案輸入輸出問題。

備註

 本文將重點介紹 FileIO 方法的範例與討論。 然而,PathIO 的方法遵循類似模式,本文大部分指引也適用於這些方法。

便利與控制

StorageFile 物件不是像原生 Win32 程式設計模型中的檔案控制代碼。 StorageFile 是檔案的表示形式,具有操作其內容的方法。

理解這個概念在使用 StorageFile 進行 I/O 時非常有用。 例如, 「寫入檔案 」章節介紹了三種寫入檔案的方法:

前兩種情境是應用程式最常用的。 透過單一操作寫入檔案不僅使程式碼更易於編寫和維護,還能免除應用程式需處理許多檔案 I/O 複雜性的責任。 然而,這種便利也帶來代價:失去對整個操作的控制權,以及在特定點發現錯誤的能力。

交易模型

FileIO 與 PathIO 類別的 Write 方法將上述第三個寫入模型的步驟包裝,並附加一層。 此層被封裝在儲存交易中。

為了保護原始檔案的完整性,以防寫入資料時出現問題,Write 方法採用交易模型,透過使用 OpenTransactedWriteAsync 開啟檔案。 此過程會建立 一個 StorageStreamTransaction 物件。 建立此交易物件後,API 會依照檔案 存取 範例或 StorageStreamTransaction 文章中程式碼範例的方式撰寫資料。

下圖說明了 WriteTextAsync 方法在成功寫入操作中執行的底層任務。 此圖示提供了操作的簡化視角。 例如,它會跳過不同執行緒的文字編碼和非同步補全等步驟。

用於寫入檔案的 WinRT API 呼叫序列圖

使用 FileIO 和 PathIO 類別的 Write 方法,而非使用更複雜的四步驟串流模型,其優點包括:

  • 一個 API 呼叫來處理所有中間步驟,包括錯誤。
  • 如果出了問題,原始檔案會被保留。
  • 系統狀態會盡量保持乾淨。

然而,由於有這麼多可能的中間故障點,故障機率也增加了。 當發生錯誤時,可能很難判斷程序失敗的地點。 以下章節將介紹你在使用 Write 方法時可能遇到的一些失敗案例,並提供可能的解決方案。

FileIO 與 PathIO 類別 Write 方法常見的錯誤代碼

本表呈現應用程式開發者在使用 Write 方法時常見的錯誤代碼。 表格中的步驟對應於前一圖中的步驟。

錯誤名稱(值) Steps 原因 解決方案
ERROR_ACCESS_DENIED(0X80070005) 5 原始檔案可能會被標記為刪除,可能是來自先前操作。 重試操作。
確保檔案存取已同步。
ERROR_SHARING_VIOLATION(0x80070020) 5 原始檔案會被另一個獨佔寫入開啟。 重試操作。
確保檔案存取已同步。
ERROR_UNABLE_TO_REMOVE_REPLACED(0x80070497) 19-20 原始檔案(file.txt)無法被替換,因為它正在使用中。 另一個程序或操作在檔案被替換前取得存取權。 重試操作。
確保檔案存取已同步。
ERROR_DISK_FULL(0x80070070) 7, 14, 16, 20 交易中的模型會產生一個額外的檔案,這會消耗額外的儲存空間。
ERROR_OUTOFMEMORY(0x8007000E) 14, 16 這可能是因為多個未完成的 I/O 操作或檔案大小過大所導致。 透過更細緻的控制串流方法或許能解決錯誤。
E_FAIL(0x80004005) 任意 其他 重試操作。 如果還是失敗,可能是平台錯誤,應用程式應該會因為狀態不一致而終止。

檔案狀態的其他考量可能會引起錯誤。

除了 Write 方法回傳的錯誤外,以下是應用程式在寫入檔案時可以預期的指引。

當且僅當操作完成時,資料才會寫入檔案

在寫入操作進行時,應用程式不應該對檔案中的資料做出任何假設。 嘗試在操作完成前存取檔案可能會導致資料不一致。 你的應用程式應該負責追蹤未完成的輸入輸出。

讀者

如果被寫入的檔案同時也被禮貌讀取器使用(即以 FileAccessMode.Read 開啟),後續讀取將因錯誤 ERROR_OPLOCK_HANDLE_CLOSED (0x80070323) 而失敗。 有時候應用程式會在寫入操作進行時重新嘗試開啟檔案進行讀取。 這可能導致一個競賽條件,當嘗試覆寫原始檔案時,寫入最終失敗,因為原始檔案無法被替換。

來自已知資料夾的檔案

你的應用程式可能不是唯一嘗試存取位於 已知資料夾中檔案的應用程式。 無法保證如果操作成功,應用程式寫入檔案的內容下次嘗試讀取時會保持不變。 此外,在這種情況下,分享或存取被拒絕的錯誤會變得更常見。

衝突的輸入輸出

如果我們的應用程式在本地資料中對檔案使用 Write 方法,可以降低並發錯誤的機會,但仍需謹慎。 如果同時有多個寫入操作傳送到檔案,無法保證檔案中會包含哪些資料。 為了減輕這個問題,我們建議你的應用程式將寫入操作序列化到檔案中。

~TMP 檔案

有時候,如果操作被強制取消(例如,應用程式被作業系統暫停或終止),交易可能未能被妥善提交或關閉。 這可能會留下副檔名為 (.~TMP) 的檔案。 在處理應用程式啟用時,考慮刪除這些暫存檔案(如果它們存在於應用程式的本地資料中)。

基於檔案類型的考量

某些錯誤會因檔案類型、存取頻率及檔案大小而更為常見。 一般來說,你的應用程式可以存取三種檔案類別:

  • 使用者在你應用程式的本地資料資料夾中建立並編輯的檔案。 這些檔案只會在使用你的應用程式時建立和編輯,且只存在於應用程式內。
  • 應用程式的元資料。 你的應用程式會用這些檔案來追蹤自己的狀態。
  • 檔案系統中應用程式宣告可存取權限的其他檔案。 這些檔案通常位於已知 資料夾之一。

你的應用程式對前兩類檔案擁有完全控制權,因為它們是應用程式套件檔案的一部分,且只有你的應用程式能存取。 對於最後一類檔案,你的應用程式必須知道其他應用程式和作業系統服務可能會同時存取這些檔案。

根據應用程式不同,存取檔案的頻率會有所不同:

  • 非常低。 這些檔案通常是在應用程式啟動時開啟一次,暫停應用程式時會被儲存。
  • 低。 這些檔案是使用者特別執行動作的檔案(例如儲存或讀取)。
  • 中等或高。 這些檔案是應用程式必須不斷更新資料的檔案(例如自動儲存功能或持續追蹤元資料)。

關於檔案大小,請參考以下圖表中 WriteBytesAsync 方法的效能資料。 此圖表比較了在受控環境中,每檔案大小平均 10000 次操作的操作完成時間與檔案大小。

WriteBytesAsync 效能

圖中故意省略了縱軸的時間值,因為不同硬體與配置會產生不同的絕對時間值。 然而,我們在測試中持續觀察到以下趨勢:

  • 對於非常小的檔案(<= 1 MB):完成操作的時間持續快速。
  • 對於較大檔案(> 1 MB):完成操作的時間會呈指數成長。

應用程式暫停期間的 I/O

如果你想保留狀態資訊或元資料以供後續使用,你的應用程式必須設計成能處理暫停狀態。 關於應用程式暫停的背景資訊,請參閱 應用程式生命週期 及 此部落格文章。

除非作業系統授權你的應用程式延長執行時間,當你的應用程式被暫停時,它有 5 秒的時間釋放所有資源並儲存資料。 為了最佳的可靠性與使用者體驗,務必假設你處理懸吊任務的時間有限。 請在5秒內遵守以下處理懸吊任務的指引:

  • 盡量減少 I/O 數量,以避免因沖洗和釋放操作所造成的競爭狀況。
  • 避免撰寫需要數百毫秒甚至更長時間才能寫入的檔案。
  • 如果你的應用程式使用 Write 方法,請記得這些方法所需的所有中間步驟。

如果你的應用程式在暫停期間只處理少量狀態資料,大多數情況下你可以用 Write 方法來清除資料。 不過,如果你的應用程式使用大量狀態資料,可以考慮用串流直接儲存資料。 這有助於減少 Write 方法中交易模型所帶來的延遲。

舉例可參考 BasicSuspension 範例。

其他範例與資源

以下是針對特定情境的幾個範例和其他資源。

重試檔案 I/O 的範例程式碼

以下是一個偽代碼範例,說明如何重試寫入(C#),假設寫入是在使用者選擇儲存檔案後進行:

Windows.Storage.Pickers.FileSavePicker savePicker = new Windows.Storage.Pickers.FileSavePicker();
savePicker.FileTypeChoices.Add("Plain Text", new List<string>() { ".txt" });
Windows.Storage.StorageFile file = await savePicker.PickSaveFileAsync();

Int32 retryAttempts = 5;

const Int32 ERROR_ACCESS_DENIED = unchecked((Int32)0x80070005);
const Int32 ERROR_SHARING_VIOLATION = unchecked((Int32)0x80070020);

if (file != null)
{
    // Application now has read/write access to the picked file.
    while (retryAttempts > 0)
    {
        try
        {
            retryAttempts--;
            await Windows.Storage.FileIO.WriteTextAsync(file, "Text to write to file");
            break;
        }
        catch (Exception ex) when ((ex.HResult == ERROR_ACCESS_DENIED) ||
                                   (ex.HResult == ERROR_SHARING_VIOLATION))
        {
            // This might be recovered by retrying, otherwise let the exception be raised.
            // The app can decide to wait before retrying.
        }
    }
}
else
{
    // The operation was cancelled in the picker dialog.
}

同步存取檔案

《 Parallel Programming with .NET》部落格 是關於平行程式設計的優秀資源。 特別是關於 AsyncReaderWriterLock 的文章 ,描述如何在允許同時讀取的同時,維持對檔案的專屬寫入存取。 請記得序列化 I/O 會影響效能。

另請參閱