Skip to content

Глава 1.9: Приведение типов и рефлексия ​


Цель: Освоить безопасное приведение типов, проверки типов во время выполнения и API рефлексии Enforce Script для динамического доступа к свойствам.


Оглавление ​


Зачем нужно приведение типов ​

Иерархия сущностей DayZ глубока. Большинство API движка возвращают обобщённый базовый тип (Object, Man, Class), но вам нужен конкретный тип (PlayerBase, ItemBase, CarScript) для доступа к специализированным методам. Приведение типов преобразует базовую ссылку в производную — безопасно.

Class (корень)
  └─ Object
       └─ Entity
            └─ EntityAI
                 ├─ InventoryItem → ItemBase
                 ├─ DayZCreatureAI
                 │    ├─ DayZInfected
                 │    └─ DayZAnimal
                 └─ Man
                      └─ DayZPlayer → PlayerBase

Вызов метода, которого нет у базового типа, вызывает крах во время выполнения — ошибки компиляции не будет, поскольку Enforce Script разрешает виртуальные вызовы во время выполнения.


Class.CastTo — безопасное приведение вниз ​

Class.CastTo — предпочтительный метод приведения типов в DayZ. Это статический метод, который записывает результат в out-параметр и возвращает bool.

c
// Сигнатура:
// static bool Class.CastTo(out Class target, Class source)

Object obj = GetSomeObject();
PlayerBase player;

if (Class.CastTo(player, obj))
{
    // Приведение успешно — player валиден
    string name = player.GetIdentity().GetName();
    Print("Найден игрок: " + name);
}
else
{
    // Приведение не удалось — obj не является PlayerBase
    // player равен null
}

Почему предпочтителен:

  • Возвращает false при неудаче вместо краша
  • out-параметр устанавливается в null при неудаче — безопасно для проверки
  • Работает по всей иерархии классов (не только Object)

Паттерн: приведение и продолжение ​

В циклах используйте неудачу приведения для пропуска нерелевантных объектов:

c
array<Object> nearObjects = new array<Object>;
array<CargoBase> proxyCargos = new array<CargoBase>;
GetGame().GetObjectsAtPosition(pos, 50.0, nearObjects, proxyCargos);

foreach (Object obj : nearObjects)
{
    EntityAI entity;
    if (!Class.CastTo(entity, obj))
        continue;  // Пропускаем объекты, не являющиеся EntityAI (здания, ландшафт и т.д.)

    // Теперь безопасно вызывать методы EntityAI
    if (entity.IsAlive())
    {
        Print(entity.GetType() + " жив на " + entity.GetPosition().ToString());
    }
}

Type.Cast — альтернативное приведение ​

Каждый класс имеет статический метод Cast, который возвращает результат приведения напрямую (или null при неудаче).

c
// Синтаксис: TargetType.Cast(source)

Object obj = GetSomeObject();
PlayerBase player = PlayerBase.Cast(obj);

if (player)
{
    player.DoSomething();
}

Это однострочная запись, совмещающая приведение и присваивание, но вы обязаны проверить результат на null.

Приведение примитивов и Param ​

Type.Cast также используется с классами Param (активно применяемыми в RPC и событиях):

c
override void OnEvent(EventType eventTypeId, Param params)
{
    if (eventTypeId == ClientReadyEventTypeID)
    {
        Param2<PlayerIdentity, Man> readyParams = Param2<PlayerIdentity, Man>.Cast(params);
        if (readyParams)
        {
            PlayerIdentity identity = readyParams.param1;
            Man player = readyParams.param2;
        }
    }
}

CastTo vs Type.Cast — когда что использовать ​

СвойствоClass.CastToType.Cast
Тип возвратаboolЦелевой тип или null
Null при неудачеДа (out-параметр устанавливается в null)Да (возвращает null)
Лучше дляif-блоков с ветвлением логикиОднострочных присваиваний
Используется в ванильном DayZПовсеместноПовсеместно
Работает с не-ObjectДа (любой Class)Да (любой Class)

Практическое правило: Используйте Class.CastTo, когда нужно ветвление по результату. Используйте Type.Cast, когда просто нужна типизированная ссылка с последующей проверкой на null.

c
// CastTo — ветвление по результату
PlayerBase player;
if (Class.CastTo(player, obj))
{
    // обработка игрока
}

// Type.Cast — присвоить и проверить позже
PlayerBase player = PlayerBase.Cast(obj);
if (!player) return;

obj.IsInherited — проверка типа во время выполнения ​

IsInherited проверяет, является ли объект экземпляром данного типа без выполнения приведения. Принимает аргумент typename.

c
Object obj = GetSomeObject();

if (obj.IsInherited(PlayerBase))
{
    Print("Это игрок!");
}

if (obj.IsInherited(DayZInfected))
{
    Print("Это зомби!");
}

if (obj.IsInherited(CarScript))
{
    Print("Это транспортное средство!");
}

IsInherited возвращает true для точного типа и любых родительских типов в иерархии. Объект PlayerBase вернёт true для IsInherited(Man), IsInherited(EntityAI), IsInherited(Object) и т.д.


obj.IsKindOf — строковая проверка типа ​

IsKindOf выполняет ту же проверку, но с строковым именем класса. Полезно, когда имя типа приходит из данных (например, из файлов конфигурации).

c
Object obj = GetSomeObject();

if (obj.IsKindOf("ItemBase"))
{
    Print("Это предмет");
}

if (obj.IsKindOf("DayZAnimal"))
{
    Print("Это животное");
}

Важно: IsKindOf проверяет всю цепочку наследования, как и IsInherited. Mag_STANAG_30Rnd вернёт true для IsKindOf("Magazine_Base"), IsKindOf("InventoryItem"), IsKindOf("EntityAI") и т.д.

IsInherited vs IsKindOf ​

СвойствоIsInherited(typename)IsKindOf(string)
АргументТип на этапе компиляцииСтроковое имя
СкоростьБыстрее (сравнение типов)Медленнее (строковый поиск)
Когда использоватьТип известен на этапе компиляцииТип приходит из данных/конфигурации

obj.Type — получение типа во время выполнения ​

Type() возвращает typename фактического класса объекта во время выполнения — не объявленный тип переменной.

c
Object obj = GetSomeObject();
typename t = obj.Type();

Print(t.ToString());  // например, "PlayerBase", "AK101", "LandRover"

Используйте для логирования, отладки или динамического сравнения типов:

c
void ProcessEntity(EntityAI entity)
{
    typename t = entity.Type();
    Print("Обработка сущности типа: " + t.ToString());

    if (t == PlayerBase)
    {
        Print("Это игрок");
    }
}

typename — хранение ссылок на типы ​

typename — полноценный тип в Enforce Script. Его можно хранить в переменных, передавать как параметры и сравнивать.

c
// Объявление переменной typename
typename playerType = PlayerBase;
typename vehicleType = CarScript;

// Сравнение
typename objType = obj.Type();
if (objType == playerType)
{
    Print("Совпадение!");
}

// Использование в коллекциях
array<typename> allowedTypes = new array<typename>;
allowedTypes.Insert(PlayerBase);
allowedTypes.Insert(DayZInfected);
allowedTypes.Insert(DayZAnimal);

// Проверка принадлежности
foreach (typename t : allowedTypes)
{
    if (obj.IsInherited(t))
    {
        Print("Объект соответствует разрешённому типу: " + t.ToString());
        break;
    }
}

Создание экземпляров из typename ​

Можно создавать объекты из typename во время выполнения:

c
typename t = PlayerBase;
Class instance = t.Spawn();  // Создаёт новый экземпляр

// Или строковый подход:
Class instance2 = GetGame().CreateObjectEx("AK101", pos, ECE_PLACE_ON_SURFACE);

Примечание: typename.Spawn() работает только для классов с конструктором без параметров. Для сущностей DayZ используйте GetGame().CreateObject() или CreateObjectEx().


API рефлексии ​

Enforce Script предоставляет базовую рефлексию — возможность инспектировать и модифицировать свойства объекта во время выполнения без знания его типа на этапе компиляции.

Инспекция переменных ​

Type() каждого объекта возвращает typename, предоставляющий метаданные переменных:

c
void InspectObject(Class obj)
{
    typename t = obj.Type();

    int varCount = t.GetVariableCount();
    Print("Класс: " + t.ToString() + " имеет " + varCount.ToString() + " переменных");

    for (int i = 0; i < varCount; i++)
    {
        string varName = t.GetVariableName(i);
        typename varType = t.GetVariableType(i);

        Print("  [" + i.ToString() + "] " + varName + " : " + varType.ToString());
    }
}

Доступные методы рефлексии на typename:

МетодВозвращаетОписание
GetVariableCount()intКоличество переменных-членов
GetVariableName(int index)stringИмя переменной по индексу
GetVariableType(int index)typenameТип переменной по индексу
ToString()stringИмя класса в виде строки

EnScript.GetClassVar / SetClassVar ​

EnScript.GetClassVar и EnScript.SetClassVar позволяют читать/записывать переменные-члены по имени во время выполнения. Это аналог динамического доступа к свойствам в Enforce Script.

c
// Сигнатура:
// static void EnScript.GetClassVar(Class instance, string varName, int index, out T value)
// static bool EnScript.SetClassVar(Class instance, string varName, int index, T value)
// 'index' — индекс элемента массива — используйте 0 для не-массивных полей.

class MyConfig
{
    int MaxSpawns = 10;
    float SpawnRadius = 100.0;
    string WelcomeMsg = "Hello!";
}

void DemoReflection()
{
    MyConfig cfg = new MyConfig();

    // Чтение значений по имени
    int maxVal;
    EnScript.GetClassVar(cfg, "MaxSpawns", 0, maxVal);
    Print("MaxSpawns = " + maxVal.ToString());  // "MaxSpawns = 10"

    float radius;
    EnScript.GetClassVar(cfg, "SpawnRadius", 0, radius);
    Print("SpawnRadius = " + radius.ToString());  // "SpawnRadius = 100"

    string msg;
    EnScript.GetClassVar(cfg, "WelcomeMsg", 0, msg);
    Print("WelcomeMsg = " + msg);  // "WelcomeMsg = Hello!"

    // Запись значений по имени
    EnScript.SetClassVar(cfg, "MaxSpawns", 0, 50);
    EnScript.SetClassVar(cfg, "SpawnRadius", 0, 250.0);
    EnScript.SetClassVar(cfg, "WelcomeMsg", 0, "Welcome!");
}

Предупреждение: GetClassVar/SetClassVar молча завершаются неудачей, если имя переменной неверно или тип не совпадает. Всегда валидируйте имена переменных перед использованием.


Реальные примеры ​

Поиск всех транспортных средств в мире ​

c
static array<CarScript> FindAllVehicles()
{
    array<CarScript> vehicles = new array<CarScript>;
    array<Object> allObjects = new array<Object>;
    array<CargoBase> proxyCargos = new array<CargoBase>;

    // Поиск на большой площади (или используйте логику, специфичную для миссии)
    vector center = "7500 0 7500";
    GetGame().GetObjectsAtPosition(center, 15000.0, allObjects, proxyCargos);

    foreach (Object obj : allObjects)
    {
        CarScript car;
        if (Class.CastTo(car, obj))
        {
            vehicles.Insert(car);
        }
    }

    Print("Найдено " + vehicles.Count().ToString() + " транспортных средств");
    return vehicles;
}

Безопасный хелпер для объектов с приведением ​

Этот паттерн используется повсеместно в моддинге DayZ — утилитарная функция, безопасно проверяющая, жив ли Object, через приведение к EntityAI:

c
// Object.IsAlive() НЕ существует у базового класса Object!
// Нужно сначала привести к EntityAI.

static bool IsObjectAlive(Object obj)
{
    if (!obj)
        return false;

    EntityAI eai;
    if (Class.CastTo(eai, obj))
    {
        return eai.IsAlive();
    }

    return false;  // Объекты, не являющиеся EntityAI (здания и т.д.) — считаем «не живыми»
}

Система конфигурации на основе рефлексии ​

Этот паттерн (используемый в MyMod Core) создаёт универсальную систему конфигурации, где поля читаются/записываются по имени, позволяя админ-панелям редактировать любую конфигурацию без знания её конкретного класса:

c
class ConfigBase
{
    // Поиск индекса переменной-члена по имени
    protected int FindVarIndex(string fieldName)
    {
        typename t = Type();
        int count = t.GetVariableCount();
        for (int i = 0; i < count; i++)
        {
            if (t.GetVariableName(i) == fieldName)
                return i;
        }
        return -1;
    }

    // Получение значения любого поля в виде строки
    string GetFieldValue(string fieldName)
    {
        if (FindVarIndex(fieldName) == -1)
            return "";

        int iVal;
        EnScript.GetClassVar(this, fieldName, 0, iVal);
        return iVal.ToString();
    }

    // Установка значения любого поля из строки
    void SetFieldValue(string fieldName, string value)
    {
        if (FindVarIndex(fieldName) == -1)
            return;

        int iVal = value.ToInt();
        EnScript.SetClassVar(this, fieldName, 0, iVal);
    }
}

class MyModConfig : ConfigBase
{
    int MaxPlayers = 60;
    int RespawnTime = 300;
}

void AdminPanelSave(ConfigBase config, string fieldName, string newValue)
{
    // Работает для ЛЮБОГО подкласса конфигурации — никакого типоспецифичного кода
    config.SetFieldValue(fieldName, newValue);
}

Типобезопасная диспетчеризация событий ​

Используйте typename для создания диспетчера, направляющего события в правильный обработчик:

c
class EventDispatcher
{
    protected ref map<typename, ref array<ref EventHandler>> m_Handlers;

    void EventDispatcher()
    {
        m_Handlers = new map<typename, ref array<ref EventHandler>>;
    }

    void Register(typename eventType, EventHandler handler)
    {
        if (!m_Handlers.Contains(eventType))
        {
            m_Handlers.Insert(eventType, new array<ref EventHandler>);
        }

        m_Handlers.Get(eventType).Insert(handler);
    }

    void Dispatch(EventBase event)
    {
        typename eventType = event.Type();

        array<ref EventHandler> handlers;
        if (m_Handlers.Find(eventType, handlers))
        {
            foreach (EventHandler handler : handlers)
            {
                handler.Handle(event);
            }
        }
    }
}

Лучшие практики ​

  • Всегда проверяйте на null после каждого приведения — и Class.CastTo, и Type.Cast возвращают null при неудаче, и использование результата без проверки вызывает крах.
  • Используйте Class.CastTo, когда нужно ветвление по успеху/неудаче; используйте Type.Cast для краткого однострочного присваивания с последующей проверкой на null.
  • Предпочитайте IsInherited(typename) вместо IsKindOf(string), когда тип известен на этапе компиляции — это быстрее и ловит опечатки на этапе компиляции.
  • Приводите к EntityAI перед вызовом IsAlive() — базовый класс Object не имеет этого метода.
  • Валидируйте имена переменных через GetVariableCount/GetVariableName перед использованием EnScript.GetClassVar — он молча завершается неудачей при неверных именах.

Замечено в реальных модах ​

Паттерны подтверждены изучением исходного кода профессиональных модов DayZ.

ПаттернМодПодробности
Class.CastTo + continue в циклах по сущностямCOT / ExpansionКаждый цикл по массивам Object использует приведение и continue для пропуска несовпадающих типов
IsKindOf для проверок типов из конфигурацииExpansion MarketКатегории предметов, загруженные из JSON, используют строковый IsKindOf, потому что типы — это данные
EnScript.GetClassVar/SetClassVar для админ-панелейDabs FrameworkУниверсальные редакторы конфигураций читают/записывают поля по имени, так что один UI работает для всех классов конфигурации
obj.Type().ToString() для логированияVPP AdminОтладочные логи всегда включают entity.Type().ToString() для идентификации обработанного объекта

Теория vs практика ​

КонцепцияТеорияРеальность
Object.IsAlive()Ожидается на ObjectДоступен только на EntityAI и подклассах — вызов на Object вызывает крах
EnScript.SetClassVar возвращает boolДолжен указывать успех/неудачуМолча возвращает false при неверном имени поля без сообщения об ошибке — легко пропустить
typename.Spawn()Создаёт экземпляр любого классаРаботает только для классов с конструктором без параметров; для игровых сущностей используйте CreateObject

Частые ошибки ​

1. Забыли проверку на null после приведения ​

c
// НЕПРАВИЛЬНО — крашится, если obj не PlayerBase
PlayerBase player = PlayerBase.Cast(obj);
player.GetIdentity();  // КРАХ, если приведение не удалось!

// ПРАВИЛЬНО
PlayerBase player = PlayerBase.Cast(obj);
if (player)
{
    player.GetIdentity();
}

2. Вызов IsAlive() у базового Object ​

c
// НЕПРАВИЛЬНО — Object.IsAlive() не существует
Object obj = GetSomeObject();
if (obj.IsAlive())  // Ошибка компиляции или крах в рантайме!

// ПРАВИЛЬНО
EntityAI eai;
if (Class.CastTo(eai, obj) && eai.IsAlive())
{
    // Безопасно
}

3. Использование рефлексии с неверным именем переменной ​

c
// ТИХАЯ НЕУДАЧА — нет ошибки, возвращается ноль/пустое значение
int val;
EnScript.GetClassVar(obj, "NonExistentField", 0, val);
// val равен 0, ошибки нет

Всегда валидируйте через FindVarIndex или GetVariableCount/GetVariableName.

4. Путаница между Type() и литералом typename ​

c
// Type() — возвращает РАНТАЙМ-тип экземпляра
typename t = myObj.Type();  // например, PlayerBase

// Литерал typename — ссылка на тип на этапе компиляции
typename t = PlayerBase;    // Всегда PlayerBase

// Они сравнимы
if (myObj.Type() == PlayerBase)  // true, если myObj ЯВЛЯЕТСЯ PlayerBase

Итоги ​

ОперацияСинтаксисВозвращает
Безопасное приведение внизClass.CastTo(out target, source)bool
Инлайн-приведениеTargetType.Cast(source)Целевой тип или null
Проверка типа (typename)obj.IsInherited(typename)bool
Проверка типа (строка)obj.IsKindOf("ClassName")bool
Получение рантайм-типаobj.Type()typename
Количество переменныхobj.Type().GetVariableCount()int
Имя переменнойobj.Type().GetVariableName(i)string
Тип переменнойobj.Type().GetVariableType(i)typename
Чтение свойстваEnScript.GetClassVar(obj, name, 0, out val)void
Запись свойстваEnScript.SetClassVar(obj, name, 0, val)bool

Released under CC BY-SA 4.0 | Code examples under MIT License