Unity 6.3
0 онлайн 57 гостей 3 в системе
Вход
Программирование в Unity Шаг 47 из 210

Создание посетителя объекта с низкоуровневым APIs

В этом примере показано, как создать посетителя свойства с низкоуровневыми интерфейсами IPropertyBagVisitor и IPropertyVisitor. Результат такой же, как в примере Создание посетителя свойства с классом PropertyVisitor, но с посетителем, реализующим эти интерфейсы, а не таким, который производится из PropertyVisitor.

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

  1. Создайте те же классы Data и DebugUtilities, что и в примере Создайте посетителя свойства с классом PropertyVisitor в примере.

  2. Изменить DebugUtilities класс, чтобы использовать низкоуровневый визитор вместо PropertyVisitor-derived посетитель следующим образом:

    public static class DebugUtilities
    {
        private static readonly LowLevelVisitor 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());
        }
    }
    

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

Создайте низкоуровневый класс-посетитель так:

  1. Создать LowLevelVisitor класс, реализующий IPropertyVisitor и IPropertyBagVisitor интерфейсы.

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

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

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

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

    public class LowLevelVisitor
        : IPropertyBagVisitor
        , IPropertyVisitor
    {
        private const int k_InitialIndent = 0;
    
        private readonly StringBuilder m_Builder = new StringBuilder();
        private int m_IndentLevel = k_InitialIndent;
    
        public void Reset()
        {
            m_Builder.Clear();
            m_IndentLevel = k_InitialIndent;
        }
    
        public string GetDump()
        {
            return m_Builder.ToString();
        }
    }
    

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

Провести заседание Accept метод на свойство с использованием this, осуществление IPropertyVisitor интерфейс. Он позволяет задать поведение при обходе свойства — аналогично PropertyVisitorVisitProperty метод:

  1. В течение LowLevelVisitor класс, переопределите IPropertyBagVisitor.Visit и IPropertyVisitor.Visit методы:

    void IPropertyBagVisitor.Visit<TContainer>(IPropertyBag<TContainer> propertyBag, ref TContainer container)
    {
        foreach (var property in propertyBag.GetProperties(ref container))
        {
            property.Accept(this, ref container);
        }
    }
            
    void IPropertyVisitor.Visit<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container)
    {
        var value = property.GetValue(ref container);
        // Code goes here.
    }
    
  2. IVisitPropertyAdapter адаптеры, используемые с PropertyVisitor базовому классу нужен доступ к внутреннему состоянию визитора, поэтому вне этого класса их использовать нельзя. Однако вы можете объявить предметно-ориентированные адаптеры, у которых есть нужные сведения. В том же файле, что и LowLevelVisitor класс, определите IPrintValue и IPrintValue<in T> интерфейсов для этих адаптеров и PrintContext структуру, чтобы собрать сведения, нужные адаптерам для форматирования значения свойства, следующим образом:

    // Create the following struct with methods to encapsulate the formatting of the message and display the value.
    public readonly struct PrintContext
    {
        private StringBuilder Builder { get; }
        private string Prefix { get; }
        public string PropertyName { get; }
    
        public void Print<T>(T value)
        {
            Builder.AppendLine($"{Prefix}- {PropertyName} = {{{TypeUtility.GetTypeDisplayName(value?.GetType() ?? typeof(T))}}} {value}");
        }
            
        public void Print(Type type, string value)
        {
            Builder.AppendLine($"{Prefix}- {PropertyName} = {{{TypeUtility.GetTypeDisplayName(type)}}} {value}");
        }
    
        public PrintContext(StringBuilder builder, string prefix, string propertyName)
        {
            Builder = builder;
            Prefix = prefix;
            PropertyName = propertyName;
        }
    }
    
    public interface IPrintValue
    {
    }
    
    public interface IPrintValue<in T> : IPrintValue
    {
        void PrintValue(in PrintContext context, T value);
    }
    
  3. В течение LowLevelVisitor класс, переопределите метод интерфейса адаптера, чтобы создать адаптеры для конкретных типов — Vector2 и Color, и обновить информацию об осуществлении IPropertyVisitor для использования адаптера сначала:

    public class LowLevelVisitor
        : IPropertyBagVisitor
        , IPropertyVisitor
        , IPrintValue<Vector2>
        , IPrintValue<Color>
    {
        public IPrintValue Adapter { get; set; }
            
        public LowLevelVisitor()
        {
            // For simplicity
            Adapter = this;
        }
        void IPropertyVisitor.Visit<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container)
        {
            // Here, we need to manually extract the value.
            var value = property.GetValue(ref container);
            
            var propertyName = GetPropertyName(property);
            
            // We can still use adapters, but we must manually dispatch the calls. 
            if (Adapter is IPrintValue<TValue> adapter)
            {
                var context = new PrintContext(m_Builder, Indent, propertyName);
                adapter.PrintValue(context, value);
                return;
            }
                
            // Fallback behaviour here 
        }
            
        void IPrintValue<Vector2>.PrintValue(in PrintContext context, Vector2 value)
        {
            context.Print(value);
        }
        void IPrintValue<Color>.PrintValue(in PrintContext context, Color value)
        {
            const string format = "F3";
            var formatProvider = CultureInfo.InvariantCulture.NumberFormat;
            context.Print(typeof(Color), $"RGBA({value.r.ToString(format, formatProvider)}, {value.g.ToString(format, formatProvider)}, {value.b.ToString(format, formatProvider)}, {value.a.ToString(format, formatProvider)})");
        }
    }
    

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

Полный код низкоуровневого класса-визитора и пользовательских адаптеров для Color и Vector2 выглядит следующим образом:

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

public readonly struct PrintContext
{
    // A context struct to hold information about how to print the property.
    private StringBuilder Builder { get; }
    private string Prefix { get; }
    public string PropertyName { get; }

    // Method to print the value of type T with its associated property name.
    public void Print<T>(T value)
    {
        Builder.AppendLine($"{Prefix}- {PropertyName} = {{{TypeUtility.GetTypeDisplayName(value?.GetType() ?? typeof(T))}}} {value}");
    }

    // Method to print the value with a specified type and its associated property name.
    public void Print(System.Type type, string value)
    {
        Builder.AppendLine($"{Prefix}- {PropertyName} = {{{TypeUtility.GetTypeDisplayName(type)}}} {value}");
    }

    // Constructor to initialize the PrintContext.
    public PrintContext(StringBuilder builder, string prefix, string propertyName)
    {
        Builder = builder;
        Prefix = prefix;
        PropertyName = propertyName;
    }
}

// Generic interface IPrintValue that acts as a marker interface for all print value adapters.
public interface IPrintValue
{
}

// Generic interface IPrintValue<T> to define how to print values of type T.
// This interface is used as an adapter for specific types (Vector2 and Color in this case).
public interface IPrintValue<in T> : IPrintValue
{
    void PrintValue(in PrintContext context, T value);
}

// LowLevelVisitor class that implements various interfaces for property visiting and value printing.
public class LowLevelVisitor : IPropertyBagVisitor, IPropertyVisitor, IPrintValue<Vector2>, IPrintValue<Color>
{
    private const int k_InitialIndent = 0;

    private readonly StringBuilder m_Builder = new StringBuilder();
    private int m_IndentLevel = k_InitialIndent;

    public IPrintValue Adapter { get; set; }

    public LowLevelVisitor()
    {
        // The Adapter property is set to this instance of LowLevelVisitor.
        // This means the current LowLevelVisitor can be used as a print value adapter for Vector2 and Color.
        Adapter = 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 a property bag (a collection of properties)
    void IPropertyBagVisitor.Visit<TContainer>(IPropertyBag<TContainer> propertyBag, ref TContainer container)
    {
        foreach (var property in propertyBag.GetProperties(ref container))
        {
            // Call the Visit method of IPropertyVisitor to handle individual properties.
            property.Accept(this, ref container);
        }
    }

    // This method is called when visiting each individual property of an object.
    // It tries to find a suitable adapter (IPrintValue<T>) for the property value type (TValue) and uses it to print the value.
    // If no suitable adapter is found, it falls back to displaying the value using its type name.
    void IPropertyVisitor.Visit<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container)
    {
        // Here, we need to manually extract the value.
        var value = property.GetValue(ref container);

        var propertyName = GetPropertyName(property);

        // We can still use adapters, but we must manually dispatch the calls.
        // Try to find an adapter for the current property value type (TValue).
        if (Adapter is IPrintValue<TValue> adapter)
        {
            // If an adapter is found, create a print context and call the PrintValue method of the adapter.
            var context = new PrintContext(m_Builder, m_IndentLevel.ToString(), propertyName);
            adapter.PrintValue(context, value);
            return;
        }

        // Fallback behavior here - if no adapter is found, handle printing based on type information.
        var type = value?.GetType() ?? property.DeclaredValueType();
        var typeName = TypeUtility.GetTypeDisplayName(type);

        if (TypeTraits.IsContainer(type))
            m_Builder.AppendLine($"{m_IndentLevel}- {propertyName} {{{typeName}}}");
        else
            m_Builder.AppendLine($"{m_IndentLevel}- {propertyName} = {{{typeName}}} {value}");

        // Recursively visit child properties (if any).
        ++m_IndentLevel;
        if (null != value)
            PropertyContainer.Accept(this, ref value);
        --m_IndentLevel;
    }

    // Method from IPrintValue<Vector2> used to print Vector2 values.
    void IPrintValue<Vector2>.PrintValue(in PrintContext context, Vector2 value)
    {
        // Simply use the Print method of PrintContext to print the Vector2 value.
        context.Print(value);
    }

    // Method from IPrintValue<Color> used to print Color values.
    void IPrintValue<Color>.PrintValue(in PrintContext context, Color value)
    {
        const string format = "F3";
        var formatProvider = CultureInfo.InvariantCulture.NumberFormat;

        // Format and print the Color value in RGBA format.
        context.Print(typeof(Color), $"RGBA({value.r.ToString(format, formatProvider)}, {value.g.ToString(format, formatProvider)}, {value.b.ToString(format, formatProvider)}, {value.a.ToString(format, formatProvider)})");
    }
}

Когда у вас есть полная версия этого кода в вашем LowLevelVisitor.cs файл, вызвать DebugUtilities.PrintObjectDump метод с экземпляром Data класс, чтобы убедиться, что он выводит в консоль то же самое, что и на финальном этапе Создание визитора свойств с помощью класса PropertyVisitor пример:

using UnityEngine;

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

}

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