版本:Unity 6 (6000.0)
语言:英语
使用 PropertyVisitor 创建属性访问器
使用向量移动对象

使用底层 API 创建属性访问器

此示例演示了如何使用底层 API 与 IPropertyBagVisitorIPropertyVisitor 接口来创建属性访问器。此示例等效于使用 PropertyVisitor 基类创建属性访问器的 示例

示例概述

此示例包括创建属性访问器的分步说明,该访问器会将对象的当前状态打印到控制台。

假设您具有以下类型

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"}};
}

创建如下 DebugUtilities 实用方法

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

使用 Data 对象调用 PrintObjectDump 方法,如下所示

DebugUtilities.PrintObjectDump(new 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

创建访问器

首先,创建一个实现 IPropertyBagVisitorDumpObjectVisitor 类。在类中,使用 StringBuilder 来构建表示对象当前状态的字符串。

  1. 创建实现 IPropertyBagVisitor 接口的 DumpObjectVisitor 类。

  2. 向类添加 StringBuilder 字段。

  3. 添加一个 Reset 方法,该方法清除 StringBuilder 并重置缩进级别。

  4. 添加一个 GetDump 方法,该方法返回对象的当前状态的字符串表示形式。

    您的 DumpObjectVisitor 类如下所示

    public class DumpObjectVisitor
        : 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();
        }
    }
    

获取属性

DumpObjectVisitor 类中,重写 IPropertyBagVisitor.Visit 方法以循环遍历容器对象的属性。在对象转储访问器中,显示值并将访问委托给属性。

要使用 this 对属性调用 Accept 方法,请实现 IPropertyVisitor 接口。此接口允许您指定访问属性时的访问行为,类似于 VisitProperty PropertyVisitor 类的 方法。

  1. DumpObjectVisitor 类中,添加重写 IPropertyBagVisitor.VisitIPropertyVisitor.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. PropertyVisitor 基类一起使用的 IVisitPropertyAdapter 适配器需要访问访问器的内部状态,因此它们不能在该类之外使用。但是,您可以定义具有必要信息的特定于域的适配器。在 DumpObjectVisito 类中,更新 IPropertyVisitor 的实现以首先使用适配器

    // Create the following 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);
    }
    
    public class DumpObjectVisitor
        : IPropertyBagVisitor
        , IPropertyVisitor
        , IPrintValue<Vector2>
        , IPrintValue<Color>
    {
        public IPrintValue Adapter { get; set; }
            
        public DumpObjectVisitor()
        {
            // 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)})");
        }
    }
    

完成的代码如下所示

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(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);
}

// DumpObjectVisitor class that implements various interfaces for property visiting and value printing
private class DumpObjectVisitor : IPropertyBagVisitor, IPropertyVisitor, IPrintValue<Vector2>, IPrintValue<Color>
{
    // (Other members are omitted for brevity)

    public IPrintValue Adapter { get; set; }

    public DumpObjectVisitor()
    {
        // The Adapter property is set to this instance of DumpObjectVisitor
        // This means the current DumpObjectVisitor can be used as a print value adapter for Vector2 and Color.
        Adapter = this;
    }

    // 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, Indent, 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($"{Indent}- {propertyName} {{{typeName}}}");
        else
            m_Builder.AppendLine($"{Indent}- {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)})");
    }
}

打印子属性的当前状态

当您在数据上运行访问器时,默认情况下,它会直接在给定对象上开始访问。对于任何属性访问器,要在一个对象的子属性上开始访问,请将 PropertyPath 传递给 PropertyContainer.Accept 方法。

  1. 更新 DebugUtilities 方法以接受可选的 PropertyPath

    public static class DebugUtilities
    {
        private static readonly DumpObjectVisitor 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. 使用 Data 对象调用 PrintObjectDump 方法。这将获得 所需输出

其他资源

使用 PropertyVisitor 创建属性访问器
使用向量移动对象