이 예시는 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 클래스의 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
다음으로 컬렉션 항목의 프로퍼티 이름과 각 프로퍼티의 유형 및 값을 가져오겠습니다.
특정 프로퍼티는 특별한 이름을 가지며 특히 컬렉션 항목을 처리할 때 그렇습니다. 다음은 프로퍼티 이름에 대한 규칙입니다.
이를 더 명시적으로 구별하려면 프로퍼티 이름을 대괄호로 묶으십시오.
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 유틸리티 메서드를 사용하여 프리미티브, 열거형, 문자열과 같이 하위 프로퍼티가 포함되지 않는 유형에 대한 값만 표시하도록 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 유형을 표시합니다.
IVisitPropertyAdapter와 함께 PropertyVisitor를 사용하십시오. 특정 유형에 어댑터가 등록될 때마다 방문 중에 타겟 유형이 발견되면 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를 전달합니다.
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());
}
}
Data 오브젝트로 PrintObjectDump 메서드를 호출합니다. 이를 통해 원하는 출력을 얻습니다.