文档

Unity Engine

Unity Engine

使用 PropertyVisitor 创建属性访问者

Follow this example to create a property visitor with the PropertyVisitor base class.
阅读时间10 分钟最后更新于 1 个月前

此示例演示如何使用
PropertyVisitor
基类来创建属性访问者。有关使用
IPropertyBagVisitor
和
IPropertyVisitor
接口的对应示例,请参阅使用低级 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. 现在有了一个最基础的访问者,就可以实现 Utility 方法了。更新
    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
Utility 方法仅显示不包含子属性的类型(例如图元、枚举和字符串)的值。
在
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
方法类似,但它们公开了各自集合类型的通用参数。

添加每个类型的覆盖

最后,添加每个类型的覆盖,以更紧凑的方式显示
Vector2
和
Color
类型。
将
PropertyVisitor
与
IVisitPropertyAdapter
结合使用。每当为特定类型注册适配器时,如果在访问期间遇到目标类型,则会调用适配器而不是
VisitProperty
方法:
在
DumpObjectVisitor
类中,为
Vector2
和
Color
添加
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
    方法。这将获得所需输出。

其他资源