共用方式為


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

重要的應用程式介面

開發人員有時會在使用 FileIOPathIO 類別的 Write 方法來執行檔案系統 I/O 作業時遇到一組常見問題。 例如,常見問題包括:

  • 檔案已部分寫入。
  • 呼叫其中一個方法時,應用程式會收到例外狀況。
  • 操作會留下檔名類似於目標檔案的 .TMP 檔案。

FileIOPathIO 類別的 Write 方法包括:

  • WriteBufferAsync
  • WriteBytesAsync
  • WriteLinesAsync
  • WriteTextAsync

本文提供這些方法運作方式的詳細數據,讓開發人員更瞭解何時及如何使用它們。 本文提供指導方針,且不會嘗試為所有可能的檔案 I/O 問題提供解決方案。

備註

 本文著重於範例和討論中的 FileIO 方法。 不過, PathIO 方法遵循類似的模式,本文中的大部分指引也適用於這些方法。

便利性與控制

StorageFile 物件不是像是原生 Win32 程式設計模型的檔案句柄。 相反地,StorageFile 是檔案的表徵,具有操控其內容的方法。

使用 StorageFile 執行 I/O 時,瞭解此概念很有用。 例如,寫入檔案部分介紹了三種寫入檔案的方法:

前兩個案例是應用程式最常使用的案例。 在一次性操作中寫入檔案會更容易編寫和維護,並且也可以減輕應用程式處理許多檔案 I/O 複雜性的責任。 不過,這種便利性帶來的代價是失去對整個作業的控制,以及無法在特定點精確捕捉錯誤的能力。

交易式模型

FileIOPathIO 類別的 Write 方法將上述第三個寫入模型中的步驟進行封裝,並增加一個額外的層。 此層被封裝在儲存交易中。

若要保護源檔的完整性,以防寫入數據時發生問題, Write 方法會使用交易式模型,方法是使用 OpenTransactedWriteAsync 開啟檔案。 此程式會建立 StorageStreamTransaction 物件。 在建立此交易對象之後,API 會以類似 檔案存取 範例或 StorageStreamTransaction 文章中的程式代碼範例的方式,將資料寫入。

下圖說明 WriteTextAsync 方法在成功寫入作業中執行的基礎工作。 此圖提供作業的簡化檢視。 例如,它會略過不同線程上的文字編碼和異步完成等步驟。

UWP API 呼叫順序圖,以便寫入檔案

使用 Write 方法、FileIOPathIO 類別的優點是,它們可以取代使用數據流的更複雜的四步驟模型:

  • 一個 API 呼叫來處理所有中繼步驟,包括錯誤。
  • 如果發生問題,則會保留源檔。
  • 系統狀態會盡量保持乾淨。

不過,由於有這麼多可能的中繼失敗點,失敗的機會會增加。 發生錯誤時,可能很難了解進程失敗的位置。 下列各節說明使用 Write 方法並提供可能的解決方案時,可能會遇到的一些失敗。

FileIO 和 PathIO 類別之 Write 方法的常見錯誤碼

下表提供應用程式開發人員在使用 Write 方法時遇到的常見錯誤碼。 數據表中的步驟會對應至上圖中的步驟。

錯誤名稱(值) 步驟 原因 解決方案
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 方法所返回的錯誤之外,以下是應用程式在寫入檔案時可以預期的一些指導原則。

只有在作業完成時,才會將數據寫入檔案

當寫入作業進行時,您的應用程式不應該對檔案中的數據進行任何假設。 嘗試在作業完成之前存取檔案可能會導致數據不一致。 您的應用程式應該負責追蹤尚未完成的 I/O 作業。

讀者

如果正在被寫入的檔案同時也被符合規範的讀取器使用(也就是說,以 FileAccessMode.Read開啟),後續讀取將會失敗,並出現錯誤ERROR_OPLOCK_HANDLE_CLOSED(0x80070323)。 有時候,當 寫入 作業進行時,應用程式會重試開啟檔案以供讀取。 這可能會導致一個競爭條件,即在嘗試覆寫原始檔案時,寫入最終失敗,因為原始檔案無法被取代。

來自 KnownFolders 的檔案

您的應用程式可能不是唯一嘗試存取位於任何 KnownFolders 上的檔案的應用程式。 不保證如果作業成功,應用程式寫入檔案的內容會在下次嘗試讀取檔案時維持不變。 此外,在此案例中,共用或拒絕存取錯誤變得更加常見。

I/O 衝突 (輸入/輸出衝突)

如果我們的應用程式使用本機數據中檔案的 Write 方法,可能會降低並行錯誤的可能性,但仍需要一些謹慎。 如果同時將多個 寫入 作業傳送至檔案,則無法保證檔案中最終會有哪些數據。 若要解決此問題,我們建議您的應用程式將 寫入操作序列化到檔案。

~TMP 檔案

有時候,如果作業被強制取消(例如,如果應用程式被操作系統暫停或終止),則交易可能無法適當地被認可或關閉。 這可能會留下擴展名為 (.~TMP) 的檔案。 處理應用程式啟用時,請考慮刪除這些暫存盤(如果它們存在於應用程式的本機數據中)。

根據檔案類型的考慮

根據檔案類型、存取錯誤的頻率,以及其檔案大小,某些錯誤可能會變得更加普遍。 一般而言,您的應用程式可以存取的檔案有三種類別:

  • 使用者在您應用程式的本機數據資料資料夾中建立和編輯的檔案。 這些只會在使用您的應用程式時建立和編輯,而且它們只存在於應用程式內。
  • 應用程式元數據。 您的應用程式會使用這些檔案來追蹤自己的狀態。
  • 應用程式已宣告可存取之檔案系統位置的其他檔案。 這些最常位於 KnownFolders 的其中一個 中。

您的應用程式可完全控制前兩個類別的檔案,因為它們是您應用程式的套件檔案的一部分,而且是由您的應用程式獨佔存取。 對於上一個類別中的檔案,您的應用程式必須注意其他應用程式和OS服務可能會同時存取檔案。

視應用程式而定,對檔案的存取可能會因頻率而異:

  • 非常低。 這些通常是在應用程式啟動時開啟一次的檔案,並在應用程式暫停時儲存。
  • 低。 這些是用戶特別在上採取動作的檔案(例如儲存或載入)。
  • 中等或高。 這些檔案是應用程式必須持續更新資料的檔案(例如,自動儲存功能或常數元數據追蹤)。

針對檔案大小,請考慮 WriteBytesAsync 方法的下列圖表中的效能數據。 此圖表會比較完成作業與檔案大小的時間,以及受控制環境中每個檔案大小 10000 個作業的平均效能。

WriteBytesAsync 效能

Y 軸上的時間值會刻意從此圖表中省略,因為不同的硬體和組態會產生不同的絕對時間值。 不過,我們在測試中一直觀察到這些趨勢:

  • 對於非常小的檔案(<= 1 MB):完成作業的時間始終很快。
  • 對於較大的檔案 (> 1 MB):完成作業的時間會開始以指數方式增加。

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

如果您想要保留狀態資訊或元數據,以供稍後會話使用,您的應用程式必須設計為處理暫停。 如需應用程式暫停的背景資訊,請參閱 應用程式生命週期此部落格文章

除非操作系统將延長執行時間授予您的應用程式,否則當應用程式被暫停時,您有 5 秒鐘的時間來釋放其所有資源並儲存其資料。 為了獲得最佳可靠性和用戶體驗,請一律假設您必須處理暫停工作的時間有限。 請記住下列指導方針,在處理暫停工作的 5 秒期間:

  • 請嘗試將 I/O 保持在最小值,以避免排清和釋放作業所造成的競爭狀況。
  • 避免寫入需要數百毫秒甚至更長時間的檔案。
  • 如果您的應用程式使用 Write 方法,請記住這些方法所需的所有中繼步驟。

如果您的 app 在暫停期間對少量的狀態數據運作,在大部分情況下,您可以使用 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.
}

同步存取檔案

使用 .NET 的平行程序設計部落格是有關平行程序設計之指引的絕佳資源。 特別是,文章關於 AsyncReaderWriterLock 的描述中說明了如何在允許併發讀取存取的同時,維持對檔案的寫入操作的獨佔存取權。 請記住,串行化 I/O 會影響效能。

另請參閱