
В интернетах полно статей про паттерны. Но реальных примеров из живых проектов встречается немного. Понятно, что в случае с Прототипом, есть довольно популярный проектик 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
и их блупринтовые наследники
Автор: TheSkull