winrt::implements 結構範本 是您自己的 C++/WinRT 實作(執行階段類別和啟動工廠)直接或間接衍生的基礎。
本主題討論 winrt::implements C++/WinRT 2.0 中的擴充點。 您可以選擇針對您的實作類型實施這些擴充點,以便自定義可檢視對象的預設行為(
這些延伸點可讓您延遲實作類型的解構、在解構期間安全地查詢,以及掛勾進入和退出您預設的方法。 本主題描述這些功能,並進一步說明何時及如何使用這些功能。
延遲破壞
在 診斷直接配置 主題中,我們提到您的實作類型不能有私有解構函式。
具有公開解構函式的優點是,它可以啟用延後解構,即具備偵測對物件的最終 IUnknown::Release 呼叫的能力,然後取得該物件的擁有權,以無限期地延遲其解構。
回想一下,傳統 COM 對象本質上會計算參考計數;參考計數是透過 IUnknown::AddRef 和 IUnknown::Release 函式來管理。 在傳統 Release實作中,一旦參考計數達到 0,就會呼叫經典 COM 物件的 C++ 解構函式。
uint32_t WINRT_CALL Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
delete this;
}
return remaining;
}
delete this; 會先呼叫物件的解構函式,再釋放 物件所佔用的記憶體。 這運作良好,前提是您不需要在解構函式中執行任何有趣的動作。
using namespace winrt::Windows::Foundation;
...
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
~Sample() noexcept
{
// Too late to do anything interesting.
}
};
有趣的是什麼意思? 首先,解構函式本質上是同步的。 您不能轉換線程,或許是為了在不同上下文中銷毀某些特定於該線程的資源。 您無法可靠地查詢物件,以獲取您可能需要的某些其他介面,從而釋放特定資源。 清單繼續。 在您的破壞並非微不足道的情況下,您需要更具彈性的解決方案。 這就是 C++/WinRT 的 final_release 函式派上用場的地方。
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static void final_release(std::unique_ptr<Sample> ptr) noexcept
{
// This is the first stop...
}
~Sample() noexcept
{
// ...And this happens only when *unique_ptr* finally deletes the object.
}
};
我們已更新 Release 的 C++/WinRT 實作,以便在物件的參考計數變為 0 時,立即呼叫您的 final_release。 在該狀態下,對象可以確信沒有進一步未解決的引用,並且它現在擁有對自身的獨佔擁有權。 因此,它可以將本身的擁有權轉移至靜態 final_release 函式。
換句話說,物品已經從支持共同擁有的狀態轉變為專屬擁有。 std::unique_ptr 具有物件的獨佔擁有權,因此作為其語意的一部分,它會自然地銷毀該物件。所以當 std::unique_ptr 超出範疇時(如果在此之前未被移動到其他地方的話),就會自然地銷毀物件,因此需要公用解構函式。 這就是關鍵。 您可以無限期地使用物件,前提是 std::unique_ptr能讓物件保持有效。 以下說明如何將物件移至別處的圖例。
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static void final_release(std::unique_ptr<Sample> ptr) noexcept
{
batch_cleanup.push_back(std::move(ptr));
}
};
此程式代碼會將 物件儲存在名為 batch_cleanup 其中一個作業的集合中,以便清除應用程式運行時間某個未來點的所有物件。
一般而言,當 std::unique_ptr 銷毀時,物件會被銷毀,但您可以呼叫 std::unique_ptr::reset來加快其銷毀,或者,您可以將 std::unique_ptr 儲存在某處來延後其銷毀。
也許更實際且更強大,您可以將 final_release 函式轉換成協程,並在一個地方處理其最終解構,同時能夠視需要暫停並切換執行緒。
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static winrt::fire_and_forget final_release(std::unique_ptr<Sample> ptr) noexcept
{
co_await winrt::resume_background(); // Unwind the calling thread.
// Safely perform complex teardown here.
}
};
暫停將會導致呼叫線程—原本起始對 IUnknown::Release 函式的呼叫—傳回,因此會向呼叫者發出訊號,指出它曾經持有的對象無法再透過該介面指標使用。 UI 架構通常需要確保物件會在最初建立物件的特定UI線程上終結。 此功能使滿足此類需求變得輕而易舉,因為銷毀與釋放物件已分開。
請注意,傳遞至 final_release 的物件只是C++物件;它不再是 COM 物件。 例如,對象的現有 COM 弱式參考無法再解析。
在銷毀期間的安全查詢
基於延後解構的概念,能夠在解構過程中安全地查詢介面。
傳統 COM 是以兩個中央概念為基礎。 第一個是參考計數,第二個是查詢介面接口。 除了 AddRef 和 Release 之外, IUnknown 介面還提供 QueryInterface。 特定的 UI 架構會大量使用該方法,例如 XAML,用於模擬其可組合的類型系統及遍歷 XAML 階層。 請考慮簡單的範例。
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
};
這可能 似乎 無害。 此 XAML 頁面想要在析構函式中清空其資料內容。 但是,
C++/WinRT 2.0 已強化以支援此功能。 以下是 C++/WinRT 2.0 版的 Release 實作,格式已簡化。
uint32_t Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
m_references = 1; // Debouncing!
T::final_release(...);
}
return remaining;
}
如您可能所預測,它會先遞減參考計數,然後只有在沒有其他參考時才會作用。 不過,在呼叫本主題稍早所述的靜態 final_release 函式之前,它會將參考計數設定為1來穩定參考計數。 我們將這稱為 去彈跳(引用一個電氣工程的術語)。 這對於防止最終參考被釋放至關重要。 發生此情況後,參考計數不穩定,且無法可靠地支持對 QueryInterface的呼叫。
呼叫 QueryInterface 在釋放最終參考之後是一種危險的行為,因為這樣可能導致參考計數無限增長。 您有責任僅呼叫那些不會延長物件存活期的已知程式碼路徑。 C++/WinRT 以確保您可以可靠地進行這些 QueryInterface 方法呼叫,從而為您減少了一半的工作。
它會藉由穩定參考計數來執行此工作。 當釋放最終參考時,實際的參考計數要麼是 0,要麼是某個極其無法預測的值。 如果涉及弱式參考,可能會發生後者的情況。 無論哪種方式,如果接下來呼叫 QueryInterface,這都將是無法持續的,因為這必然會導致引用計數暫時增加,因此要考慮防止重複執行。 將它設定為 1 可確保對 Release 的最終呼叫永遠不會在此物件上再次發生。 這正是我們想要的,因為 std::unique_ptr 現在擁有物件,但對 QueryInterface/Release 函數的有限呼叫會是安全的。
請考慮更有趣的範例。
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
static winrt::fire_and_forget final_release(std::unique_ptr<MainPage> ptr)
{
co_await 5s;
co_await winrt::resume_foreground(ptr->Dispatcher());
ptr = nullptr;
}
};
首先,會呼叫 final_release 函式,通知實作應開始清理準備工作。 在這裡,final_release 恰好是協同程式。 開始模擬第一個暫停點時,會先在線程池中等候幾秒鐘。 然後,它會繼續在頁面的調度器執行緒上運行。 最後一個步驟牽涉到查詢,因為 Dispatcher 是 DependencyObject 基類的屬性。 最後,頁面實際上會藉由將 nullptr 指派給 std::unique_ptr來刪除。 這接著會呼叫頁面的解構函式。
在解構函式內,我們會清除數據內容;如我們所知道,需要查詢 FrameworkElement 基類。
這一切都是可能的,因為 C++/WinRT 2.0 提供的參考計數抖動消除(或參考計數穩定)。
方法進入和退出掛鉤
較不常用的擴充點是 abi_guard 結構,以及函式 abi_enter 和 abi_exit。
如果您的實作類型定義了函式 abi_enter,則會在每個投影的介面方法的進入點呼叫該函式(不包括 IInspectable的方法)。
同樣地,如果您定義 abi_exit,則將會在每個這類方法結束時呼叫它;但是,若是您的 abi_enter 拋出例外,則不會呼叫它。 如果投影介面方法本身擲回例外狀況,它 仍會呼叫。
例如,當客戶端嘗試在將物件置於無法使用的狀態後使用它時,您可以使用 abi_enter 來拋出一個假設性的 invalid_state_error 例外,例如在執行 ShutDown 或 Disconnect 方法後。 如果基礎集合已變更,C++/WinRT 反覆運算器類別會使用此功能,在 abi_enter 函式中擲回無效的狀態例外狀況。
在簡單 abi_enter 和 abi_exit函式的上方,您可以定義名為 abi_guard 的巢狀類型。 在此情況下,系統會在您投影的介面方法中的每個 (非IInspectable) 上建立 abi_guard 實例,並將該對象作為其建構函數的參數。 然後,abi_guard 會在方法結束時被解構。 您可以將您想要的任何額外狀態放入 abi_guard 類型。
如果您沒有定義自己的 abi_guard,則系統會預設一個,在建構時呼叫 abi_enter,並在解構時呼叫 abi_exit。
只有在透過投影介面叫用方法
以下是程式代碼範例。
struct Sample : SampleT<Sample, IClosable>
{
void abi_enter();
void abi_exit();
void Close();
};
void example1()
{
auto sampleObj1{ winrt::make<Sample>() };
sampleObj1.Close(); // Calls abi_enter and abi_exit.
}
void example2()
{
auto sampleObj2{ winrt::make_self<Sample>() };
sampleObj2->Close(); // Doesn't call abi_enter nor abi_exit.
}
// A guard is used only for the duration of the method call.
// If the method is a coroutine, then the guard applies only until
// the IAsyncXxx is returned; not until the coroutine completes.
IAsyncAction CloseAsync()
{
// Guard is active here.
DoWork();
// Guard becomes inactive once DoOtherWorkAsync
// returns an IAsyncAction.
co_await DoOtherWorkAsync();
// Guard is not active here.
}