共用方式為


COM 中的錯誤處理 (開始使用 Win32 和 C++)

COM 會使用 HRESULT 值來指出方法或函式呼叫的成功或失敗。 各種 SDK 標頭會定義各種 HRESULT 常數。 WinError.h 中定義了一組常見的全系統程序代碼。 下表顯示其中一些全系統傳回碼。

常數 數值 描述
E_ACCESSDENIED 0x80070005 拒絕存取。
E_FAIL 0x80004005 未指定的錯誤。
E_INVALIDARG 0x80070057 無效的參數值。
E_OUTOFMEMORY 0x8007000E 記憶體不足。
E_POINTER 0x80004003 NULL 被用來指標值時傳遞不正確。
E_UNEXPECTED 0x8000FFFF 非預期的條件。
S_OK 0x0 成功。
S_FALSE 0x1 成功。

 

前置詞 「E_」 的所有常數都是錯誤碼。 S_OK和S_FALSE數都是成功碼。 大約 99% 的 COM 方法會在成功時傳回 S_OK ;但不要讓這個事實誤導您。 方法可能會傳回其他成功代碼,因此請務必使用 SUCCEEDEDFAILED 巨集來測試錯誤。 下列範例程式代碼顯示錯誤的方式,以及測試函式呼叫成功的正確方式。

// Wrong.
HRESULT hr = SomeFunction();
if (hr != S_OK)
{
    printf("Error!\n"); // Bad. hr might be another success code.
}

// Right.
HRESULT hr = SomeFunction();
if (FAILED(hr))
{
    printf("Error!\n"); 
}

成功程式代碼 S_FALSE 值得提及。 某些方法會使用 S_FALSE 來粗略表示不是失敗的負面狀況。 它也可以表示「空操作」—方法成功,但沒有效果。 例如,如果您第二次從同一個線程呼叫它,CoInitializeEx 函式傳回S_FALSE。 如果您需要區分程序代碼中的S_OKS_FALSE,您應該直接測試值,但仍使用 FAILED 或 SUCCEEDED 來處理其餘案例,如下列範例程式代碼所示。

if (hr == S_FALSE)
{
    // Handle special case.
}
else if (SUCCEEDED(hr))
{
    // Handle general success case.
}
else
{
    // Handle errors.
    printf("Error!\n"); 
}

某些 HRESULT 值專屬於 Windows 的特定功能或子系統。 例如,Direct2D 圖形 API 會定義錯誤碼 D2DERR_UNSUPPORTED_PIXEL_FORMAT,這表示程式使用了不支援的圖元格式。 Windows 檔通常會提供方法可能傳回的特定錯誤碼清單。 不過,您不應該考慮這些列表是明確的。 方法隨時可以傳回文件中未列出的 HRESULT 值。 再次,請使用 SUCCEEDEDFAILED 巨集。 如果您測試特定錯誤碼,也請包含預設情況。

if (hr == D2DERR_UNSUPPORTED_PIXEL_FORMAT)
{
    // Handle the specific case of an unsupported pixel format.
}
else if (FAILED(hr))
{
    // Handle other errors.
}

錯誤處理的模式

本節探討一些以結構化方式處理 COM 錯誤的模式。 每個模式都有優點和缺點。 在某種程度上,選擇是味道的問題。 如果您處理現有的專案,它可能已經有規定特定樣式的編碼指導方針。 無論您採用哪一種模式,健全的程式代碼都會遵守下列規則。

  • 針對傳回 HRESULT 的每個方法或函式,請先檢查傳回值再繼續進行。
  • 使用完資源後請釋放它們。
  • 請勿嘗試存取無效或未初始化的資源,例如 NULL 指標。
  • 在您發行資源之後,請勿嘗試使用資源。

考慮到這些規則,以下是處理錯誤的四種模式。

巢狀 ifs

傳回 HRESULT 的每個呼叫之後,請使用 if 語句來測試是否成功。 然後,將下一個方法呼叫放在 if 語句的範圍內。 更多 if 語句可以視需要進行多層次的巢狀結構。 此課程模組中的先前程式代碼範例已全部使用此模式,但這裡再次如此:

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->Show(NULL);
        if (SUCCEEDED(hr))
        {
            IShellItem *pItem;
            hr = pFileOpen->GetResult(&pItem);
            if (SUCCEEDED(hr))
            {
                // Use pItem (not shown). 
                pItem->Release();
            }
        }
        pFileOpen->Release();
    }
    return hr;
}

優點

  • 變數可以在有限的範圍內宣告。 例如,pItem 是在使用時才宣告的。
  • 在每一個 if 語句中,某些不因變數都成立:所有先前的呼叫都成功,而且所有已取得的資源仍然有效。 在上一個範例中,當程式到達最內層的 if語句時,已知 pItempFileOpen 都是有效的。
  • 清楚何時釋放介面指標和其他資源。 您會在if語句結尾釋放資源,這個語句緊接在取得資源的呼叫之後。

缺點

  • 有些人發現深層巢狀結構難以閱讀。
  • 錯誤處理與其他分支和迴圈語句混合在 中。 這可讓整體程序邏輯更難遵循。

級聯式條件判斷

在每個方法呼叫之後,請使用 if 語句來測試是否成功。 如果方法成功,請將下一個方法呼叫放在 if 區塊內。 但不要將 if 敘述進一步巢狀化,而是將每個後續 SUCCEEDED 測試放在前一個 if 區塊之後。 如果有任何方法失敗,則所有剩餘被標記為 SUCCEEDED 的測試都會失敗,直到到達函式底部為止。

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen = NULL;
    IShellItem *pItem = NULL;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));

    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->Show(NULL);
    }
    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->GetResult(&pItem);
    }
    if (SUCCEEDED(hr))
    {
        // Use pItem (not shown).
    }

    // Clean up.
    SafeRelease(&pItem);
    SafeRelease(&pFileOpen);
    return hr;
}

在此模式中,您會在函式結尾釋放資源。 如果發生錯誤,當函式結束時,某些指標可能無效。 在無效的指標上呼叫 Release 將會當機程式(或更糟),因此您必須將所有指標初始化為 NULL,並檢查它們是否為 NULL,再釋放它們。 此範例會使用函 SafeRelease 式;智慧型指標也是不錯的選擇。

如果您使用此模式,則必須小心迴圈建構。 在迴圈內,如果有任何呼叫失敗,請中斷迴圈。

優點

  • 此模式建立的巢狀結構比「巢狀 ifs」模式少。
  • 整體控制流程更容易看到。
  • 資源會在程式代碼的某個時間點發行。

缺點

  • 所有變數都必須在函式頂端宣告和初始化。
  • 如果呼叫失敗,函式會進行多個不必要的錯誤檢查,而不是立即結束函式。
  • 由於控制流程會在失敗後繼續執行函式,因此您必須在函式內容中小心,避免存取無效的資源。
  • 迴圈內的錯誤需要特別處理。

遇失敗時採取行動

在每個方法呼叫之後,測試是否失敗。 失敗時,跳到函式底部附近的標籤。 在標籤之後,但在結束函式之前,釋放資源。

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen = NULL;
    IShellItem *pItem = NULL;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFileOpen->Show(NULL);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFileOpen->GetResult(&pItem);
    if (FAILED(hr))
    {
        goto done;
    }

    // Use pItem (not shown).

done:
    // Clean up.
    SafeRelease(&pItem);
    SafeRelease(&pFileOpen);
    return hr;
}

優點

  • 整體控制流程很容易看出。
  • 在程式碼中,每次進行FAILED檢查後,如果您未跳轉到標籤,則可以保證所有先前的呼叫都已成功。
  • 資源會在程序代碼中的一個位置發行。

缺點

  • 所有變數都必須在函式頂端宣告和初始化。
  • 有些程式設計人員不喜歡在程序代碼中使用 goto 。 不過,請注意,這種goto的使用是高度結構化的,程式碼永遠不會跳出當前的函數調用。
  • goto 語句會略過初始化表達式。

失敗時擲回

當方法失敗時,您可以拋出例外,而不是跳到標籤。 如果您習慣撰寫例外安全的代碼,這樣可以產生更符合慣用表達的 C++ 程式風格。

#include <comdef.h>  // Declares _com_error

inline void throw_if_fail(HRESULT hr)
{
    if (FAILED(hr))
    {
        throw _com_error(hr);
    }
}

void ShowDialog()
{
    try
    {
        CComPtr<IFileOpenDialog> pFileOpen;
        throw_if_fail(CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
            CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen)));

        throw_if_fail(pFileOpen->Show(NULL));

        CComPtr<IShellItem> pItem;
        throw_if_fail(pFileOpen->GetResult(&pItem));

        // Use pItem (not shown).
    }
    catch (_com_error err)
    {
        // Handle error.
    }
}

請注意,此範例會使用 CComPtr 類別來管理介面指標。 一般而言,如果您的程式代碼拋出例外狀況,您應該遵循RAII(資源獲取即初始化)模式。 也就是說,每個資源都應該由一個物件來管理,而此物件的解構函式能保證資源被正確地釋放。 如果拋出例外狀況,保證解構函式會被呼叫。 否則,您的程式可能會流失資源。

優點

  • 與使用例外狀況處理的現有程序代碼相容。
  • 與能夠擲出例外的 C++ 函式庫相容,例如標準模板庫(STL)。

缺點

  • 需要C++物件來管理資源,例如記憶體或檔句柄。
  • 需要充分瞭解如何撰寫例外狀況安全程序代碼。

下一步

模組 3. Windows 圖形