
В интернетах полно статей про паттерны. Но реальных примеров из живых проектов встречается немного. Понятно, что в случае с Прототипом, есть довольно популярный проектик Java Script или Lua. Но я хочу еще! Поэтому в этом посте приведу пример паттерна из Unreal Engine.
Общая информация для приличия
Прототип - шаблон проектирования, описывающий создание объектов, путем клонирования существующих, нежели их инстанцирования с нуля. Такой способ весьма пользителен, покуда инициализация новорожденных объектов обходится в копеечку, а нам требуется создать несколько экземпляров.
Этот шаблон, также, облегчает жизнь, когда мы не знаем с каким именно типом работаем. Известен только его базовый интерфейс (далекий предок), а нам страсть как хочется сделать копию объекта, который в нашем распоряжении. Таким образом мы просто вызываем его метод clone(), а уж он сам знает как себя клонировать, будьте уверены.
Использование паттерна в Unreal Engine
В Анриле объекты (UObjects) создаются через вызов функции NewObject<T>. На первый взгляд, как будто ни разу не клонирование и вообще это скорей Фабрика, нежели Прототип. Но если копнуть глубже…
Каждому классу, объявленному под макросом UCLASS и отнаследованному от UObject, соответствует некий объект типа UClass (можно получить через функцию StaticClass()), в котором содержится Class Default Object. Этот CDO и есть прототип, из которого создаются новые инстансы. В частности, из этого образцового объекта копируются дефолтные значения свойств во вновь сконструированный клон.
Скрытый текст
Что интересно, из UClass можно вытащить CDO через GetDefaultObject(), поменять там значение по умолчанию какого-нибудь свойства (а то и всех), а затем подставить этот образец в качестве параметра Template в функцию NewObject. Но при этом важно параметр bCopyTransientsFromClassDefaults поставить в true. Тогда, созданные объекты будут иметь уже измененные значения полей. Не уверен насколько часто такое в жизни нужно. Но мне было интересно.
Здесь, в отличии от академических примеров, метода clone/copy в чистом виде нет. Клонирование прототипа происходит даже не в самом UObject. Это ответственное задание делегируется классу FObjectInitializer, который передается в конструктор UObject.
Изучая код и уткнувшись в этот класс, я не сразу нашел место, где копируются поля. Путем нескольких запусков процесса создания объекта под дебугом, “место преступления” было обнаружено… в деструкторе этого самого FObjectInitializer, а именно в точке вызова PostConstructInit :
FObjectInitializer::~FObjectInitializer() { //Some code... #if USE_CIRCULAR_DEPENDENCY_LOAD_DEFERRING bool bIsPostConstructInitDeferred = false; if (!FBlueprintSupport::IsDeferredCDOInitializationDisabled()) { if (FObjectInitializer* DeferredCopy = FDeferredObjInitializationHelper::DeferObjectInitializerIfNeeded(*this)) { DeferredCopy->bIsDeferredInitializer = true; // make sure this wasn't mistakenly pushed into ObjectInitializers // (the copy constructor should have been what was invoked, // which doesn't push to ObjectInitializers) check(FUObjectThreadContext::Get().TopInitializer() != DeferredCopy); bIsPostConstructInitDeferred = true; } } if (!bIsPostConstructInitDeferred) #endif // USE_CIRCULAR_DEPENDENCY_LOAD_DEFERRING { PostConstructInit(); } // Some code... }
void FObjectInitializer::PostConstructInit() { //Some code... if (bShouldInitializePropsFromArchetype) { UClass* BaseClass = (bIsCDO && !GIsDuplicatingClassForReinstancing) ? SuperClass : Class; if (BaseClass == NULL) { check(Class==UObject::StaticClass()); BaseClass = Class; } UObject* Defaults = ObjectArchetype ? ObjectArchetype : BaseClass->GetDefaultObject(false); // we don't create the CDO here if it doesn't already exist InitProperties(Obj, BaseClass, Defaults, bCopyTransientsFromClassDefaults); } // Some code... }
void FObjectInitializer::InitProperties(UObject* Obj, UClass* DefaultsClass, UObject* DefaultData, bool bCopyTransientsFromClassDefaults) { // Some code... if (!bNeedInitialize && bCanUsePostConstructLink) { // This is just a fast path for the below in the common case that we are not doing a duplicate or initializing a CDO and this is all native. // We only do it if the DefaultData object is NOT a CDO of the object that's being initialized. CDO data is already initialized in the // object's constructor. if (DefaultData) { if (Class->GetDefaultObject(false) != DefaultData) { for (FProperty* P = Class->PropertyLink; P; P = P->PropertyLinkNext) { bool bIsTransient = P->HasAnyPropertyFlags(CPF_Transient | CPF_DuplicateTransient | CPF_NonPIEDuplicateTransient); if (!bIsTransient || !P->ContainsInstancedObjectProperty()) { if (P->IsInContainer(DefaultsClass)) { P->CopyCompleteValue_InContainer(Obj, DefaultData); } } } } else { // Copy all properties that require additional initialization (e.g. CPF_Config). for (FProperty* P = Class->PostConstructLink; P; P = P->PostConstructLinkNext) { bool bIsTransient = P->HasAnyPropertyFlags(CPF_Transient | CPF_DuplicateTransient | CPF_NonPIEDuplicateTransient); if (!bIsTransient || !P->ContainsInstancedObjectProperty()) { if (P->IsInContainer(DefaultsClass)) { P->CopyCompleteValue_InContainer(Obj, DefaultData); } } } } } } else { // As with native classes, we must iterate through all properties (slow path) if default data is pointing at something other than the CDO. bCanUsePostConstructLink &= (DefaultData == Class->GetDefaultObject(false)); UObject* ClassDefaults = bCopyTransientsFromClassDefaults ? DefaultsClass->GetDefaultObject() : NULL; check(!GEventDrivenLoaderEnabled || !bCopyTransientsFromClassDefaults || !DefaultsClass->GetDefaultObject()->HasAnyFlags(RF_NeedLoad)); for (FProperty* P = bCanUsePostConstructLink ? Class->PostConstructLink : Class->PropertyLink; P; P = bCanUsePostConstructLink ? P->PostConstructLinkNext : P->PropertyLinkNext) { if (bNeedInitialize) { bNeedInitialize = InitNonNativeProperty(P, Obj); } bool bIsTransient = P->HasAnyPropertyFlags(CPF_Transient | CPF_DuplicateTransient | CPF_NonPIEDuplicateTransient); if (!bIsTransient || !P->ContainsInstancedObjectProperty()) { if (bCopyTransientsFromClassDefaults && bIsTransient) { // This is a duplicate. The value for all transient or non-duplicatable properties should be copied // from the source class's defaults. P->CopyCompleteValue_InContainer(Obj, ClassDefaults); } else if (P->IsInContainer(DefaultsClass)) { P->CopyCompleteValue_InContainer(Obj, DefaultData); } } } // Some code... } }
Вызовы FProperty::CopyCompleteValue_InContainer и совершают акт копирования значений!
Заключение
Упрощенные примеры из обучалок - это хорошо, правильно и полезно. Но в жизни это может выглядеть иначе. Сей вариант показался мне интересным.
Вероятно, полезные ссылки:
Классная статья про то, как устроены
UObject,UClassи их блупринтовые наследники
💀
