共用方式為


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

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

常數 數值 Description
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_OKS_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 函式,CoInitializeEx 函式會傳回 S_FALSE 。 如果您需要區分 程式 代碼中的 S_OK和S_FALSE ,您應該直接測試值,但仍使用 FAILEDSUCCEEDED 來處理其餘的案例,如下列範例程式碼所示。

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,這表示程式使用不支援的像素格式。 MSDN 檔通常會提供方法可能會傳回的特定錯誤碼清單。 不過,您不應該將這些清單視為明確。 方法一律可以傳回檔中未列出的 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 之前,不會宣告 pItem。
  • 在每一個 if 語句中,某些不變數為 true:所有先前的呼叫都成功,而且所有取得的資源仍然有效。 在上述範例中,當程式到達最內層 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 用法具有高度結構化;程式碼永遠不會跳到目前的函式 call.)
  • 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 圖形