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_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 函式,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,這表示程式使用不支援的像素格式。 MSDN 檔通常會提供方法可能會傳回的特定錯誤碼清單。 不過,您不應該將這些清單視為明確。 方法一律可以傳回檔中未列出的 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 語句中,某些不變數為 true:所有先前的呼叫都成功,而且所有取得的資源仍然有效。 在上述範例中,當程式到達最內層 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 用法具有高度結構化;程式碼永遠不會跳到目前的函式 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++ 物件來管理記憶體或檔案控制代碼等資源。
- 需要充分瞭解如何撰寫安全例外狀況的程式碼。
下一個
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應