共用方式為


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 ;但不要讓這個事實誤導您。 方法可能會傳回其他成功碼,因此一律使用 SUCCEEDED FAILED 宏測試錯誤。 下列範例程式代碼顯示錯誤的方式,以及測試函式呼叫成功的正確方式。

// 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 值。 同樣地,請使用 SUCCEEDED FAILED 宏。 如果您測試特定錯誤碼,也包含預設案例。

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

缺點

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

級聯 ifs

在每個方法呼叫之後,請使用 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;
}

優點

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

缺點

  • 所有變數都必須在函式頂端宣告和初始化。
  • 有些程式設計人員不喜歡在程序代碼中使用 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 圖形