Unity 6.3
0 онлайн 42 гостей 3 в системе
Вход

Создание визитора свойств с помощью класса PropertyVisitor

Этот пример показывает, как использовать PropertyVisitor базовый класс, чтобы создать визитор свойств. Эквивалентный пример с использованием IPropertyBagVisitor и IPropertyVisitor интерфейсы, см. Используйте низкоуровневый APIs для создания посетителя свойства.

Создание классов данных и полезных классов

Создайте простой класс данных и вспомогательный класс, который с помощью визитора обходит и печатает его свойства:

  1. Создайте следующий класс C# с именем Data с свойствами, которые можно посетить:

    using System.Collections.Generic;
    using UnityEngine;
    
    public class Data
    {
        public string Name = "Henry";
        public Vector2 Vec2 = Vector2.one;
        public List<Color> Colors = new List<Color> { Color.green, Color.red };
        public Dictionary<int, string> Dict = new Dictionary<int, string> { { 5, "zero" } };
    }
    
  2. Создайте следующий вспомогательный класс с именем DebugUtilities:

    using UnityEngine;
    
    public static class DebugUtilities
    {
        public static void PrintObjectDump<T>(T value)
        {
        // Magic goes here.
        }
    }
    

Создать посетителя

Создайте класс-посетитель так:

  1. Создайте класс с именем ObjectVisitor который наследует от PropertyVisitor.

  2. Добавить StringBuilder поле в класс. Это можно использовать для построения строки, представляющей текущее состояние объекта.

  3. Добавить метод Reset, который очищает StringBuilder и сбросает уровень отступов.

  4. Добавить метод GetDumpкоторый возвращает строковое представление текущего состояния объекта.

    ObjectVisitor класс теперь выглядит так:

    // `PropertyVisitor` is an abstract class that you must derive from. 
    public class ObjectVisitor: PropertyVisitor
    {
        private const int k_InitialIndent = 0;
        private readonly StringBuilder m_Builder = new StringBuilder();
            
        private int m_IndentLevel = k_InitialIndent;
            
        private string Indent => new (' ', m_IndentLevel * 2);
            
        public void Reset()
        {
            m_Builder.Clear();
            m_IndentLevel = k_InitialIndent;
        }
    
        public string GetDump()
        {
            return m_Builder.ToString();
        }
    }
    

Получение свойств верхнего уровня

Внутри ObjectVisitor класс, переопределите VisitProperty метода для посещения каждого свойства объекта и регистрации имени свойства. PropertyVisitor не требует каких-либо членов, чтобы реализовать и по умолчанию, он просто посещает каждое свойство и не выполняет никаких действий.

  1. Внутри ObjectVisitor класс, добавьте следующее переопределение VisitProperty метод:

    protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
    {
        m_Builder.AppendLine($"- {property.Name}");
    }
    
  2. Теперь, когда у вас есть минимальный посетитель, вы можете реализовать метод утилиты. PrintObjectDump метод в DebugUtilities класс, чтобы создать новый ObjectVisitor экземпляр и использовать его для посещения свойств данного объекта:

    public static class DebugUtilities
    {
        private static readonly ObjectVisitor s_Visitor = new();
            
        public static void PrintObjectDump<T>(T value)
        {
            s_Visitor.Reset();
                
            // This is the main entry point to run a visitor.
            PropertyContainer.Accept(s_Visitor, ref value);
            Debug.Log(s_Visitor.GetDump());
        }
    }
    
  3. Из соответствующего места в коде вызовите метод PrintObjectDump и передайте ему объект Data. Например, из простого компонента MonoBehaviour это может выглядеть следующим образом:

    using UnityEngine;
    
    public class MyMonoBehaviour : MonoBehaviour
    {
        
        void Start()
        {
        DebugUtilities.PrintObjectDump(new Data());
        }
    
    }
    

    В консоль выводится следующий вывод:

    - Name
    - Vec2
    - Colors
    - Dict
    

Получить подсвойства

Как показывает вывод из предыдущего раздела, когда вы переопределяете VisitProperty метода, он не посещает автоматически подсвойства объекта. Чтобы получить подсвойства, используйте метод PropertyContainer.Accept метод для применения visitor на каждое из значений рекурсивно.

В течение ObjectVisitor класс, обновите VisitProperty метод для применения visitor рекурсивно к значению, в которое нужно вложить:

protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
{
    m_Builder.AppendLine($"{Indent}- {property.Name}");
        
    ++m_IndentLevel;
    // Apply this visitor recursively on the value to nest in.
    if (null != value)
        PropertyContainer.Accept(this, ref value);
    --m_IndentLevel;
}

Вывод консоли теперь выглядит следующим образом:

- Name
- Vec2
- x
- y
- Colors
- 0
    - r
    - g
    - b
    - a
- 1
    - r
    - g
    - b
    - a 
- Dict
- 5
    - Key
    - Value

Отображение дополнительной информации о каждом свойстве

Для дальнейшего улучшения печатного вывода, получайте имя свойства элементов коллекции, а также тип и значение каждого свойства. Некоторые свойства имеют специальные имена, особенно при работе с элементами коллекции. Конвенции для имен свойств следующие:

  • Для элементов списка имя соответствует индексу.
  • Для словарей имя производится из строчной версии значения ключа.
  • Для множеств имя основано на строковой версии значения.

Чтобы сделать это различие более четким, поместите имя свойства в квадратные скобки.

  1. Внутри ObjectVisitor класс, добавьте следующий метод:

    private static string GetPropertyName(IProperty property)
    {
        return property switch
        {
            // You can also treat `IListElementProperty`, `IDictionaryElementProperty`, and `ISetElementProperty` separately.
            ICollectionElementProperty => $"[{property.Name}]",
            _ => property.Name
        };
    }
    
  2. Обновить метод VisitProperty, чтобы использовать TypeUtility.GetTypeDisplayName для получения отображаемого имени заданного типа.

    protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
    {
        var propertyName = GetPropertyName(property);
            
        // Get the concrete type of the property or its declared type if value is null.
        var typeName = TypeUtility.GetTypeDisplayName(value?.GetType() ?? property.DeclaredValueType());
            
        m_Builder.AppendLine($"{Indent}- {propertyName} = {{{typeName}}} {value}");
            
        ++m_IndentLevel;
        if (null != value)
            PropertyContainer.Accept(this, ref value);
        --m_IndentLevel;
    }
    

    Вывод консоли теперь выглядит следующим образом:

    - Name = {string} Henry
    - Vec2 = {Vector2} (1.00, 1.00)
    - x = {float} 1
    - y = {float} 1
    - Colors = {List<Color>} System.Collections.Generic.List`1[UnityEngine.Color]
    - [1] = {Color} RGBA(0.000, 1.000, 0.000, 1.000)
        - r = {float} 0
        - g = {float} 1
        - b = {float} 0
        - a = {float} 1
    - [1] = {Color} RGBA(1.000, 0.000, 0.000, 1.000)
        - r = {float} 1
        - g = {float} 0
        - b = {float} 0
        - a = {float} 1
    - Dict = {Dictionary<int, string>} System.Collections.Generic.Dictionary`2[System.Int32,System.String]
    - [5] = {KeyValuePair<int, string>} [5, five]
        - Key = {int} 5
        - Value = {string} five
    

Отображать меньше сведений для типов коллекций

Потому что List<T> не переопределяет ToString() метод, значение списка отображается как System.Collections.Generic.List1[UnityEngine.Color]. Чтобы уменьшить объем отображаемой информации, обновите VisitProperty для использования TypeTraits.IsContainer полезный метод для отображения значения только для типов, которые не содержат подсвойств, таких как примитивы, энумы и строки.

В течение ObjectVisitor класс, обновите VisitProperty метод использования TypeTraits.IsContainer для определения того, является ли значение типом контейнера. Если да, отображается имя типа без значения. В противном случае отображается имя типа и значение:

protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
{
    var propertyName = GetPropertyName(property);

    var type = value?.GetType() ?? property.DeclaredValueType();
    var typeName = TypeUtility.GetTypeDisplayName(type);
    
    // Only display the values for primitives, enums and strings.
    if (TypeTraits.IsContainer(type))
        m_Builder.AppendLine($"{Indent}- {propertyName} {{{typeName}}}");
    else
        m_Builder.AppendLine($"{Indent}- {propertyName} = {{{typeName}}} {value}");
    
    ++m_IndentLevel;
    if (null != value)
        PropertyContainer.Accept(this, ref value);
    --m_IndentLevel;
}

Вывод консоли теперь выглядит следующим образом:

- Name = {string} Henry
- Vec2 {Vector2}
- x = {float} 1
- y = {float} 1
- Colors {List<Color>}
- [0] {Color}
    - r = {float} 0
    - g = {float} 1
    - b = {float} 0
    - a = {float} 1
- [1] {Color}
    - r = {float} 1
    - g = {float} 0
    - b = {float} 0
    - a = {float} 1
- Dict {Dictionary<int, string>}
- [5] {KeyValuePair<int, string>}
    - Key = {int} 5
    - Value = {string} five

Совет: чтобы сократить объём отображаемой информации, можно также воспользоваться следующими методами для переопределения Visit Специализация по типам коллекций:

Они похожи на методы ,VisitPropertyи, но они показывают общие параметры их соответствующих типов коллекций.

Добавить переопределения типа

Добавлены переопределения типов для отображения типов Vector2 и Color в более компактном виде.

Используйте PropertyVisitor вместе с IVisitPropertyAdapter. Когда адаптер зарегистрирован для определенного типа, если целевой тип встречается во время посещения, то адаптер вызывается вместо метода VisitProperty.

В течение ObjectVisitor класс, добавьте IVisitPropertyAdapter для Vector2 и Color:

public class DumpObjectVisitor
    : PropertyVisitor
    , IVisitPropertyAdapter<Vector2>
    , IVisitPropertyAdapter<Color>
{
    public DumpObjectVisitor()
    {
        AddAdapter(this);
    }
    
    void IVisitPropertyAdapter<Vector2>.Visit<TContainer>(in VisitContext<TContainer, Vector2> context, ref TContainer container, ref Vector2 value)
    {
        var propertyName = GetPropertyName(context.Property);
        m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Vector2)}}} {value}");
    }

    void IVisitPropertyAdapter<Color>.Visit<TContainer>(in VisitContext<TContainer, Color> context, ref TContainer container, ref Color value)
    {
        var propertyName = GetPropertyName(context.Property);
        m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Color)}}} {value}");
    }
}

Начать посещение суб-собственностей

Когда вы запускаете посетителя на данных, по умолчанию он начинает посещение на объекте верхнего уровня. Для любого посетителя свойства, чтобы начать посещение на подсвойствах объекта, передайте PropertyPath к Генеральной Ассамблее PropertyContainer.Accept метод.

  1. Обновить метод DebugUtilities, чтобы взять необязательный PropertyPath:

    public static class DebugUtilities
    {
        private static readonly ObjectVisitor s_Visitor = new();
    
        public static void PrintObjectDump<T>(T value, PropertyPath path = default)
        {
            s_Visitor.Reset();
            if (path.IsEmpty)
                PropertyContainer.Accept(s_Visitor, ref value);
            else
                PropertyContainer.Accept(s_Visitor, ref value, path);
            Debug.Log(s_Visitor.GetDump());
        }
    }
    
  2. Вызов метода PrintObjectDump с объектом Data. Вывод консоли теперь выглядит следующим образом:

    - Name {string} = Henry
    - Vec2 {Vector2} = (1.00, 1.00)
    - Colors {List<Color>}
      - [0] = {Color} RGBA(0.000, 1.000, 0.000, 1.000)
      - [1] = {Color} RGBA(1.000, 0.000, 0.000, 1.000)
    - Dict {Dictionary<int, string>}
      - [5] {KeyValuePair<int, string>}
        - Key {int} = 5
        - Value {string} = five
    

Полный код посетителя

Полный код класса-визитора теперь выглядит так:

using System.Text;
using Unity.Properties;
using UnityEngine;

public class ObjectVisitor
: PropertyVisitor
, IVisitPropertyAdapter<Vector2>
, IVisitPropertyAdapter<Color>
{
    private const int k_InitialIndent = 0;

    // StringBuilder to store the dumped object's properties and values.
    private readonly StringBuilder m_Builder = new StringBuilder();
    private int m_IndentLevel = k_InitialIndent;

    // Helper property to get the current indentation.
    private string Indent => new(' ', m_IndentLevel * 2);

    public ObjectVisitor()
    {
        // Constructor, it initializes the ObjectVisitor and adds itself as an adapter
        // to handle properties of type Vector2 and Color.
        AddAdapter(this);
    }

    // Reset the visitor, clearing the StringBuilder and setting indentation to initial level.
    public void Reset()
    {
        m_Builder.Clear();
        m_IndentLevel = k_InitialIndent;
    }

    // Get the string representation of the dumped object.
    public string GetDump()
    {
        return m_Builder.ToString();
    }

    // Helper method to get the property name, handling collections and other property types.
    private static string GetPropertyName(IProperty property)
    {
        return property switch
        {
            // If it's a collection element property, display it with brackets
            ICollectionElementProperty => $"[{property.Name}]",
            // For other property types, display the name as it is
            _ => property.Name
        };
    }

    // This method is called when visiting each property of an object.
    // It determines the type of the value and formats it accordingly for display.
    protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
    {
        var propertyName = GetPropertyName(property);

        // Get the type of the value or property.
        var type = value?.GetType() ?? property.DeclaredValueType();
        var typeName = TypeUtility.GetTypeDisplayName(type);

        // Only display the values for primitives, enums, and strings, and treat other types as containers.
        if (TypeTraits.IsContainer(type))
            m_Builder.AppendLine($"{Indent}- {propertyName} {{{typeName}}}");
        else
            m_Builder.AppendLine($"{Indent}- {propertyName} = {{{typeName}}} {value}");

        // Increase indentation level before visiting child properties (if any).
        ++m_IndentLevel;
        if (null != value)
            PropertyContainer.Accept(this, ref value);
        // Decrease indentation level after visiting child properties.
        --m_IndentLevel;
    }
    
    // This method is a specialized override for Vector2 properties.
    // It displays the property name and its value as a Vector2.
    void IVisitPropertyAdapter<Vector2>.Visit<TContainer>(in VisitContext<TContainer, Vector2> context, ref TContainer container, ref Vector2 value)
    {
        var propertyName = GetPropertyName(context.Property);
        m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Vector2)}}} {value}");
    }

    // This method is a specialized override for Color properties.
    // It displays the property name and its value as a Color.
    void IVisitPropertyAdapter<Color>.Visit<TContainer>(in VisitContext<TContainer, Color> context, ref TContainer container, ref Color value)
    {
        var propertyName = GetPropertyName(context.Property);
        m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Color)}}} {value}");
    }
   
}

Дополнительные ресурсы