這很重要
使用 Windows 應用程式 SDK 建置? 本文程式碼使用 UWP (Windows.UI.Xaml) 命名空間。 如果你的專案目標是 WinUI 3(Windows 應用程式 SDK),請全文將 Microsoft.UI.Xaml(以及相關的 Microsoft.UI.* 命名空間)一律替換為對應項目。 完整地圖與 UI 遷移指南請參閱「將 UWP API 映射到 Windows 應用程式 SDK」以獲得更多細節。
Windows 執行階段 是一種採用參考計數的系統;而在這種系統中,了解強參考與弱參考的重要性,以及兩者之間的區別(還有既不屬於這兩者的參考,例如隱式的 this 指標),是很重要的。 正如你將在本主題中看到的,了解如何正確管理這些參照,可能就是可靠且運作順暢的系統與會無法預測地崩潰的系統之間的差別。 透過提供在語言投影中獲得深度支援的輔助函式,C++/WinRT 可在你建構更複雜系統時助你一臂之力,讓你能更簡單且正確地完成這項工作。
Note
除了少數例外,你在 C++/WinRT 中使用或撰寫的 Windows 執行階段 類型預設會開啟弱參考支援。 Windows.UI.Composition 和 Windows.Devices.Input.PenDevice 是例外,也就是說,對於這些類型,其所在的命名空間未啟用弱參考支援。 也可以看看 你的自動撤銷代理人是否未註冊。
如果你在撰寫類型,請參考本主題中 C++/WinRT 中的弱參考 部分。
安全存取類別成員協程中的 此 指標
欲了解更多協程及程式碼範例,請參閱 C++/WinRT 的並行與非同步操作。
以下程式碼列出了一個典型的協程範例,該協程是類別的成員函式。 你可以將此範例複製貼上到新的 Windows 主控台應用程式(C++/WinRT)專案中指定的檔案中。
// pch.h
#pragma once
#include <iostream>
#include <winrt/Windows.Foundation.h>
// main.cpp : Defines the entry point for the console application.
#include "pch.h"
using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;
struct MyClass : winrt::implements<MyClass, IInspectable>
{
winrt::hstring m_value{ L"Hello, World!" };
IAsyncOperation<winrt::hstring> RetrieveValueAsync()
{
co_await 5s;
co_return m_value;
}
};
int main()
{
winrt::init_apartment();
auto myclass_instance{ winrt::make_self<MyClass>() };
auto async{ myclass_instance->RetrieveValueAsync() };
winrt::hstring result{ async.get() };
std::wcout << result.c_str() << std::endl;
}
MyClass::RetrieveValueAsync 花了一些時間工作,最終回傳了一份資料成員的 MyClass::m_value 副本。 呼叫 RetrieveValueAsync 會建立一個非同步物件,而該物件具有一個隱含的 this 指標(最終會透過該指標存取 m_value)。
請記住,在協同程式中,執行會一直維持同步,直到第一個暫停點為止,此時控制權會返回給呼叫端。 在 RetrieveValueAsync 中,第一個 co_await 是第一個懸掛點。 當協程恢復執行時(在這個例子中,大約是五秒後),我們用來存取 m_value 的隱含 this 指標可能已經發生了任何情況。
以下是整個事件的經過。
- 在 main 中,會建立一個 MyClass 執行個體(
myclass_instance)。 -
async物件已建立,其 this 會指向myclass_instance。 - winrt::Windows::Foundation::IAsyncAction::get 函式會觸發第一個暫停點,阻塞幾秒鐘,然後回傳 RetrieveValueAsync 的結果。
-
RetrieveValueAsync 會回傳 的
this->m_value值。
步驟 4 只有在 此項 持續有效的情況下才是安全的。
但如果類別實例在非同步操作完成前就被摧毀了怎麼辦? 類別實例在非同步方法完成前,可能會有各種方式脫離作用範圍。 但我們可以透過將類別實例設定為 nullptr來模擬。
int main()
{
winrt::init_apartment();
auto myclass_instance{ winrt::make_self<MyClass>() };
auto async{ myclass_instance->RetrieveValueAsync() };
myclass_instance = nullptr; // Simulate the class instance going out of scope.
winrt::hstring result{ async.get() }; // Behavior is now undefined; crashing is likely.
std::wcout << result.c_str() << std::endl;
}
在我們銷毀類別實例之後,看起來我們不會再直接參考它。 但非同步物件當然有一個指向它的 this 指標,並試圖透過該指標複製儲存在類別實例內的值。 協程是一個成員函式,並且它預期可以毫無顧忌地使用其 this 指標。
在程式碼變更後,我們在第 4 步遇到問題,因為類別實例已被銷毀, 這個問題 不再有效。 當非同步物件嘗試存取類別實例內的變數時,它就會崩潰(或做出完全未定義的操作)。
解決方案是賦予非同步操作——協程——對類別實例的強參考。 依目前的撰寫方式,協程實際上持有一個原始的 this 指標指向類別實例;但這還不足以讓類別實例存活。
為了讓類別實例繼續運作,請將 RetrieveValueAsync 的實作改為如下所示。
IAsyncOperation<winrt::hstring> RetrieveValueAsync()
{
auto strong_this{ get_strong() }; // Keep *this* alive.
co_await 5s;
co_return m_value;
}
C++/WinRT 類別直接或間接源自 winrt::implements 模板。 因此,C++/WinRT 物件可以呼叫其 implements::get_strong 受保護的成員函式,以取得其 this 指標的強式參考。 請注意,其實不需要在上述程式碼範例中使用變 strong_this 數;只要呼叫 get_strong ,就會增加 C++/WinRT 物件的參考計數,並保持其隱含的 這個 指標有效。
這很重要
由於 get_strong 是 winrt::implements struct 模板的成員函式,你只能從直接或間接源自 winrt::implements 的類別呼叫它,例如 C++/WinRT 類別。 關於從 winrt::implements 衍生的更多資訊及範例,請參見 C++/WinRT 的作者 API。
這解決了我們之前進入第四步時遇到的問題。 即使所有其他對該類別實例的參照都消失了,協程也已預先確保其相依物保持穩定。
如果強參考不合適,那麼你可以改為呼叫 implements::get_weak,以取得對 this 的弱參考。 只要確認你能取得強力參考資料再存取 這些資料。 同樣地, get_weak 是 winrt::implements struct 模板的成員函式。
IAsyncOperation<winrt::hstring> RetrieveValueAsync()
{
auto weak_this{ get_weak() }; // Maybe keep *this* alive.
co_await 5s;
if (auto strong_this{ weak_this.get() })
{
co_return m_value;
}
else
{
co_return L"";
}
}
在上述例子中,當沒有強參考時,弱參考並不能阻止類別實例被銷毀。 但它提供了一種方式,讓你可以在存取成員變數之前,先檢查是否能取得強參照。
如何安全地使用事件處理代理存取 此 指標
情境
關於事件處理的一般資訊,請參見 C++/WinRT 中的「使用代理處理事件」。
前一節強調了協程和並發中可能存在的生命週期問題。 但如果你是使用物件的成員函式來處理事件,或是在物件成員函式內部的 lambda 函式中處理事件,那麼你就需要考慮事件接收者(處理事件的物件)與事件來源(引發事件的物件)兩者生命週期的相對關係。 讓我們來看看一些程式碼範例。
以下程式碼列出的第一個定義是一個簡單的 EventSource 類別,該類別會產生一個通用事件,由已加入的代理處理。 這個範例事件恰巧使用 Windows::Foundation::EventHandler 的代理類型,但這裡的問題與解決方法適用於任何代理類型。
接著, EventRecipient 類別以 lambda 函式的形式,為 EventSource::Event 事件提供處理程序。
// pch.h
#pragma once
#include <iostream>
#include <winrt/Windows.Foundation.h>
// main.cpp : Defines the entry point for the console application.
#include "pch.h"
using namespace winrt;
using namespace Windows::Foundation;
struct EventSource
{
winrt::event<EventHandler<int>> m_event;
void Event(EventHandler<int> const& handler)
{
m_event.add(handler);
}
void RaiseEvent()
{
m_event(nullptr, 0);
}
};
struct EventRecipient : winrt::implements<EventRecipient, IInspectable>
{
winrt::hstring m_value{ L"Hello, World!" };
void Register(EventSource& event_source)
{
event_source.Event([&](auto&& ...)
{
std::wcout << m_value.c_str() << std::endl;
});
}
};
int main()
{
winrt::init_apartment();
EventSource event_source;
auto event_recipient{ winrt::make_self<EventRecipient>() };
event_recipient->Register(event_source);
event_source.RaiseEvent();
}
其模式是事件接收者擁有一個 lambda 事件處理程序,且其指標依賴於 此 指標。 當事件接收者的生命週期長於事件來源時,它也會比那些相依關係存續得更久。 而在這些常見情況下,這種模式運作得很好。 其中一些情況很明顯,例如當 UI 頁面處理由該頁面上的控制項所引發的事件。 頁面的生命週期比按鈕長,所以處理常式的生命週期也比按鈕長。 這點適用於任何接收者擁有來源(例如資料成員),或任何接收者與來源是兄弟姊妹且直接由其他物件擁有時。
當你確定在某種情況下,處理常式不會比它所依賴的 this 活得更久時,就可以像平常一樣捕捉 this,而不必考慮強或弱的生命週期。
但仍有一些情況,this 的存活時間不會長於它在處理常式中的使用期間(包括由非同步動作與作業引發的完成事件和進度事件處理常式),因此,了解如何處理這些情況很重要。
- 當事件來源以同步方式引發事件時,你可以撤銷你的事件處理常式,並確信自己不會再收到任何事件。 但對於非同步事件,即使已解除註冊(尤其是在解構函式內解除註冊時),仍在傳遞中的事件,仍可能在物件開始解構後才抵達該物件。 在銷毀前找到一個退訂的地方可能會緩解這個問題,但請繼續閱讀以獲得一個穩健的解決方案。
- 如果你是在撰寫協程來實作非同步方法,那是可行的。
- 在某些罕見情況下,對於某些 XAML UI 框架物件(例如 SwapChainPanel),如果接收者在未向事件來源取消註冊的情況下遭到終結,就可能發生此情況。
問題
這個 主要函式 的下一個版本模擬事件接收者被銷毀(或許超出範圍)時的狀況,而事件來源仍在引發事件。
int main()
{
winrt::init_apartment();
EventSource event_source;
auto event_recipient{ winrt::make_self<EventRecipient>() };
event_recipient->Register(event_source);
event_recipient = nullptr; // Simulate the event recipient going out of scope.
event_source.RaiseEvent(); // Behavior is now undefined within the lambda event handler; crashing is likely.
}
事件接收者已被銷毀,但其中的 Lambda 事件處理常式仍然訂閱了 Event 事件。 當該事件被觸發時,lambda 會嘗試解參考 this 指標,但該指標在那個時間點已經無效。 因此,之所以發生存取違規,是因為處理常式(或協程的接續)中的程式碼嘗試使用它。
這很重要
如果你遇到這種情況,你需要思考 這個 物件的壽命;以及被捕獲 的 物體是否能存活於捕獲之後。 如果不能,則使用強參考或弱參考其中之一來捕獲它,如下方所示。
或者,如果這樣做符合你的情境,而且執行緒方面的考量也允許這麼做,那麼另一個選項是在接收者處理完該事件後,或在接收者的解構函式中撤銷處理常式。 請參見 撤銷註冊代理人。
這就是我們登記操作員的方式。
event_source.Event([&](auto&& ...)
{
std::wcout << m_value.c_str() << std::endl;
});
Lambda 會自動透過參考捕捉任何本地變數。 所以,在這個例子中,我們同樣可以這麼寫。
event_source.Event([this](auto&& ...)
{
std::wcout << m_value.c_str() << std::endl;
});
在這兩種情況下,我們只是捕捉 這個 指標的原始內容。 這對參考計數沒有影響,所以沒有什麼能阻止當前物件被銷毀。
解決方案
解決方法是取得強參照(或者,如稍後會看到的,如果那樣更合適,則使用弱參照)。 強參考 確實會 增加參考數量,並且 會 讓目前物件保持存活。 你只要宣告一個捕捉變數(此例中呼叫 strong_this ),並以 implements::get_strong 初始化它,這樣就能取得對 此 指標的強參考。
這很重要
由於 get_strong 是 winrt::implements struct 模板的成員函式,你只能從直接或間接源自 winrt::implements 的類別呼叫它,例如 C++/WinRT 類別。 關於從 winrt::implements 衍生的更多資訊及範例,請參見 C++/WinRT 的作者 API。
event_source.Event([this, strong_this { get_strong()}](auto&& ...)
{
std::wcout << m_value.c_str() << std::endl;
});
你甚至可以省略對目前物件的自動擷取,改為透過擷取變數來存取資料成員,而不是透過隱含的 this。
event_source.Event([strong_this { get_strong()}](auto&& ...)
{
std::wcout << strong_this->m_value.c_str() << std::endl;
});
如果強參考不合適,那麼你可以改為呼叫 implements::get_weak,以取得對 this 的弱參考。 弱參考 無法 讓當前物件存活。 所以,只要確認你在存取成員之前,還是能從弱參考中取得強參考。
event_source.Event([weak_this{ get_weak() }](auto&& ...)
{
if (auto strong_this{ weak_this.get() })
{
std::wcout << strong_this->m_value.c_str() << std::endl;
}
});
如果你捕捉的是原始指標,就必須確保該指標所指向的物件持續存活。
如果你使用成員函式作為代理
除了 lambda 函數外,這些原則也適用於將成員函數作為代理。 語法不同,讓我們來看看一些程式碼。 首先,這是可能不安全的成員函式事件處理常式,使用原始的 this 指標。
struct EventRecipient : winrt::implements<EventRecipient, IInspectable>
{
winrt::hstring m_value{ L"Hello, World!" };
void Register(EventSource& event_source)
{
event_source.Event({ this, &EventRecipient::OnEvent });
}
void OnEvent(IInspectable const& /* sender */, int /* args */)
{
std::wcout << m_value.c_str() << std::endl;
}
};
這是標準且慣用的方式來指稱物件及其成員功能。 為了確保安全,你可以在 Windows SDK 版本 10.0.17763.0(Windows 10,版本 1809)起,在處理程序註冊的那個點建立強或弱參考。 此時,事件接收物件已知仍存活。
若要使用強參考,只要以 get_strong 取代原始的 this 指標即可。 C++/WinRT 確保產生的代理持有對當前物件的強參考。
event_source.Event({ get_strong(), &EventRecipient::OnEvent });
捕捉強參考意味著你的物件只有在處理者取消註冊且所有未完成回撥回傳後才有資格銷毀。 然而,該保證僅在事件提出時有效。 如果你的事件處理器是非同步的,那麼你必須在第一個暫停點之前,給協程一個強引用類別實例(詳情與程式碼請參考本主題前述類別 成員協程章節中的「安全存取 此 指標 」)。 但這會在事件來源和你的物件之間形成循環參考,所以你需要明確地透過撤銷事件來打破這個循環。
若要取得弱參考,請呼叫 get_weak。 C++/WinRT 確保產生的代理持有一個弱參考。 在最後一刻,且在幕後,代表會嘗試將弱參照解決為強參照,只有成功時才呼叫成員函式。
event_source.Event({ get_weak(), &EventRecipient::OnEvent });
如果代理 真的呼叫 你的成員函式,C++/WinRT 會讓你的物件保持存活,直到你的處理器回來。 然而,如果你的處理常式是非同步的,它會在暫停點處返回,因此你必須在第一個暫停點之前,讓你的協程持有對該類別執行個體的強參考。 同樣,如需更多資訊,請參閱本主題前面「在類別成員協程中安全存取 this 指標」一節。
如果成員函式不屬於 Windows 執行階段 類型
當你無法使用get_strong方法(你的型別不是Windows 執行階段型態)時,你可以使用下方程式碼範例中展示的技巧。 這裡顯示一個一般的 C++ 類別(名為 ConsoleNetworkWatcher)正在處理 NetworkInformation.NetworkStatusChanged 事件。
#include <winrt/Windows.Networking.Connectivity.h>
using namespace winrt;
using namespace Windows::Networking::Connectivity;
class ConsoleNetworkWatcher
{
/* any constructor, and instance methods, here*/
static void Initialize(std::shared_ptr<ConsoleNetworkWatcher> instance)
{
auto weakPointer{ std::weak_ptr{ instance } };
instance->m_statusChangedRevoker =
NetworkInformation::NetworkStatusChanged(winrt::auto_revoke,
[weakPointer](winrt::Windows::Foundation::IInspectable const& sender)
{
auto sharedPointer{ weakPointer.lock() };
if (sharedPointer)
{
sharedPointer->NetworkStatusChanged(sender);
}
});
}
void NetworkStatusChanged(winrt::Windows::Foundation::IInspectable const& sender){/* handle event here */};
private:
NetworkInformation::NetworkStatusChanged_revoker m_statusChangedRevoker;
};
一個使用 SwapChainPanel::CompositionScaleChanged 的弱參考範例
在這個程式碼範例中,我們藉由弱式參考的另一個示例來使用 SwapChainPanel::CompositionScaleChanged 事件。 程式碼會用一個 lambda 註冊事件處理程式,該 lambda 會捕捉到對接收者的弱參考。
winrt::Microsoft::UI::Xaml::Controls::SwapChainPanel m_swapChainPanel;
winrt::event_token m_compositionScaleChangedEventToken;
void RegisterEventHandler()
{
m_compositionScaleChangedEventToken = m_swapChainPanel.CompositionScaleChanged([weak_this{ get_weak() }]
(Microsoft::UI::Xaml::Controls::SwapChainPanel const& sender,
Windows::Foundation::IInspectable const& object)
{
if (auto strong_this{ weak_this.get() })
{
strong_this->OnCompositionScaleChanged(sender, object);
}
});
}
void OnCompositionScaleChanged(Microsoft::UI::Xaml::Controls::SwapChainPanel const& sender,
Windows::Foundation::IInspectable const& object)
{
// Here, we know that the "this" object is valid.
}
在 lambda 擷取子句中,會建立一個暫存變數,表示對 this 的弱參考。 在 lambda 主體中,若能取得對 this 的強式參考,就會呼叫 OnCompositionScaleChanged 函式。 這樣一來,在 OnCompositionScaleChanged 裡面,就能安全使用 這個功能 。
C++/WinRT 中的弱參考
上文我們看到使用了較弱的參考資料。 一般來說,它們很適合用來打破循環參照。 例如,對於以 XAML 為基礎的 UI 架構的原生實作而言,因為該架構在歷史上的設計,C++/WinRT 中的弱式參考機制有其必要,以處理循環參考。 不過在 XAML 之外,你大概不需要使用弱參考(雖然它們本身並沒有什麼 XAML 特有的特性)。 更確切地說,通常你應該能夠以某種方式設計自己的 C++/WinRT API,從而避免需要循環參考和弱參考。
對於任何你宣告的類型,C++/WinRT 並無法立即明確得知是否需要弱式參考,以及何時需要弱式參考。 因此,C++/WinRT 會自動在結構模板 winrt::imples 上提供弱參考支援,而你的 C++/WinRT 類型直接或間接就是從這個模板衍生出來的。 這是一種用到才需付出成本的機制,也就是說,除非真的有人查詢你的物件是否支援 IWeakReferenceSource,否則你不需要付出任何代價。 而且你可以明確選擇 退出該支援。
程式碼範例
winrt::weak_ref struct 模板是取得對類別實例弱參考的一種選項。
Class c;
winrt::weak_ref<Class> weak{ c };
或者,你也可以使用 winrt::make_weak 輔助函式。
Class c;
auto weak = winrt::make_weak(c);
建立弱參考不會影響物件本身的參考計數;它只是讓控制區塊被分配。 這個控制區塊負責實作弱參考語意。 接著你可以嘗試將弱參考提升為強參考,若成功,再加以利用。
if (Class strong = weak.get())
{
// use strong, for example strong.DoWork();
}
只要仍有其他強參考存在, weak_ref::get call 會遞增引用計數並回傳強引用給呼叫者。
停用弱參照支援
弱參考支援是自動的。 但你可以明確選擇退出該支援,方法是將 winrt::no_weak_ref 標記結構作為範本引數傳遞給你的基底類別。
如果你直接衍生自 winrt::implements。
struct MyImplementation: implements<MyImplementation, IStringable, no_weak_ref>
{
...
}
如果你正在撰寫執行時類別。
struct MyRuntimeClass: MyRuntimeClassT<MyRuntimeClass, no_weak_ref>
{
...
}
無論標記結構出現在可變參數包中的哪個位置,都不重要。 如果你對已選擇退出的型別要求弱引用,編譯器就會提示你「這僅用於支援弱引用」。