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_OK和S_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 語句時, 已知 pItem 和 pFileOpen 都是有效的。
- 清楚何時釋放介面指標和其他資源。 您會在 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++物件來管理資源,例如記憶體或檔句柄。
- 需要充分瞭解如何撰寫例外狀況安全程序代碼。
下一步