版本:Unity 6 (6000.0)
语言:English
属性路径
使用低级 API 创建属性访问器

使用 PropertyVisitor 创建属性访问器

此示例演示如何使用 PropertyVisitor 基类创建属性访问器。有关使用 IPropertyBagVisitorIPropertyVisitor 接口的等效示例,请参阅 使用低级 API 创建属性访问器

示例概述

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

假设您有以下类型

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

创建访问器

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

  1. 创建一个从 PropertyVisitor 继承的 DumpObjectVisitor 类。

  2. 向类添加一个 StringBuilder 字段。

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

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

    完成后的类如下所示

    // `PropertyVisitor` is an abstract class that you must subclass from it. 
    public class DumpObjectVisitor: 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();
        }
    }
    

获取顶级属性

DumpObjectVisitor 类中,重写 VisitProperty 方法以访问对象的每个属性并记录属性名称。 PropertyVisitor 不需要实现任何成员,默认情况下,它只访问每个属性而不执行任何操作。

  1. DumpObjectVisitor 类中,添加以下重写 VisitProperty 方法

    protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
    {
        m_Builder.AppendLine($"- {property.Name}");
    }
    
  2. 现在您有了最小的访问器,您可以实现实用程序方法。更新 DebugUtilities 类中的 PrintObjectDump 方法以创建一个新的 DumpObjectVisitor 实例并使用它来访问给定对象的属性

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

    这将获得以下输出

    - Name
    - Vec2
    - Colors
    - Dict
    

获取子属性

上一节的输出表明,当您重写 VisitProperty 方法时,它不会自动访问对象的子属性。要获取子属性,请使用 PropertyContainer.Accept 方法递归地将访问器应用于每个值。

  • DebugUtilities 类中,更新 VisitProperty 方法以递归地将访问器应用于要嵌套的值

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

    这将获得以下输出

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

显示有关每个属性的更多详细信息

接下来,让我们获取集合项的属性名称以及每个属性的类型和值。

某些属性具有特殊的名称,尤其是在处理集合项时。以下是属性名称的约定

  • 对于列表项,名称对应于索引。
  • 对于字典,名称源自键值的字符串版本。
  • 对于集合,名称基于值的字符串版本。

为了使这种区别更明确,请将属性名称用方括号括起来。

  1. DumpObjectVisitor 类中,添加以下方法

    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 实用程序方法仅显示不包含子属性的类型的值,例如基元、枚举和字符串。

DumpObjectVisitor 类中,更新 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 特化

protected override void VisitCollection<TContainer, TCollection, TElement>(Property<TContainer, TCollection> property, ref TContainer container, ref TCollection value) {}
protected override void VisitList<TContainer, TList, TElement>(Property<TContainer, TList> property, ref TContainer container, ref TList value) {}
protected override void VisitDictionary<TContainer, TDictionary, TKey, TValue>(Property<TContainer, TDictionary> property, ref TContainer container, ref TDictionary value) {}
protected override void VisitSet<TContainer, TSet, TValue>(Property<TContainer, TSet> property, ref TContainer container, ref TSet value) {}

这些类似于 VisitProperty 方法,但它们公开了各自集合类型的泛型参数。

添加每类型重写

最后,添加每类型重写以更简洁的方式显示 Vector2Color 类型。

PropertyVisitorIVisitPropertyAdapter 一起使用。每当为给定类型注册适配器时,如果在访问期间遇到目标类型,则会调用适配器而不是 VisitProperty 方法

DumpObjectVisitor 类中,为 Vector2Color 添加 IVisitPropertyAdapter

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

完成的 DumpObjectVisitor 类如下所示

public class DumpObjectVisitor
: 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 DumpObjectVisitor()
    {
        // Constructor, it initializes the DumpObjectVisitor 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}");
    }
}

打印子属性的当前状态

当您在数据上运行访问器时,默认情况下,它会直接在给定对象上开始访问。对于任何属性访问器,要在对象的子属性上开始访问,请将 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 方法。这将获得 所需输出

其他资源

属性路径
使用低级 API 创建属性访问器