この例では、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 を使用してオブジェクトの現在の状態を表す文字列をビルドします。
PropertyVisitor を継承する DumpObjectVisitor クラスを作成します。
このクラスに StringBuilder フィールドを追加します。
StringBuilder を消去しインデントレベルをリセットする Reset メソッドを追加します。
オブジェクトの現在の状態を表す文字列を返す 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 の実装にメンバーは必要ありません。デフォルトでは、各プロパティにアクセスするだけでアクションは実行しません。
DumpObjectVisitor クラス内に、以下のオーバーライド VisitProperty メソッドを追加します。
protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
{
m_Builder.AppendLine($"- {property.Name}");
}
最小限のビジターを作成したら、ユーティリティメソッドを実装できます。DebugUtilities クラスで、新しい DumpObjectVisitor インスタンスを作成してからそれを使用して指定オブジェクトのプロパティを訪問するように PrintObjectDump メソッドを更新します。
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
次に、コレクションアイテムのプロパティ名や、各プロパティのタイプと値を取得します。
特にコレクションアイテムを扱う場合、特定のプロパティには特別な名前があります。プロパティ名の規則は以下のとおりです。
この区別をより明確にするには、プロパティ名を角かっこで囲みます。
DumpObjectVisitor クラス内に、以下のメソッドを追加します。
private static string GetPropertyName(IProperty property)
{
return property switch
{
// You can also treat `IListElementProperty`, `IDictionaryElementProperty`, and `ISetElementProperty` separately.
ICollectionElementProperty => $"[{property.Name}]",
_ => property.Name
};
}
TypeUtility.GetTypeDisplayName を使用して特定タイプの表示名を取得するように VisitProperty メソッドを更新します。
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] として表示されます。表示される情報量を減らすには、TypeTraits.IsContainer ユーティリティメソッドを使用して、サブプロパティを含まないタイプ (プリミティブ、enum、文字列など) 場合にのみ値を表示するように VisitProperty を更新します。
DumpObjectVisitor クラス内で、TypeTraits.IsContainer を使用して値がコンテナタイプかどうかを判断するように VisitProperty メソッドを更新します。値がコンテナタイプである場合は、その値なしでタイプ名を表示します。値がコンテナタイプでない場合は、タイプ名と値を表示します。
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}");
}
}
データに対してビジターを実行すると、デフォルトでは、指定されたオブジェクトに対して直接、アクセスを開始します。プロパティビジターに対して、オブジェクトのサブプロパティに対するアクセスを開始するには、PropertyContainer.Accept メソッドに PropertyPath を渡します。
任意の PropertyPath を実行するように DebugUtilities メソッドを更新します。
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());
}
}
Data オブジェクトを使用して PrintObjectDump メソッドを呼び出します。必要な出力を取得します。