이 예시에서는 IPropertyBagVisitor
및 IPropertyVisitor
인터페이스와 함께 로우레벨 API를 사용하여 프로퍼티 방문자를 생성하는 방법을 설명합니다. 이 예시는 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
먼저 IPropertyBagVisitor
를 구현하는 DumpObjectVisitor
클래스를 생성합니다. 클래스 안에서 StringBuilder를 사용하여 오브젝트의 현재 상태를 나타내는 문자열을 빌드합니다.
IPropertyBagVisitor
인터페이스를 구현하는 DumpObjectVisitor
클래스를 생성합니다.
클래스에 StringBuilder
필드를 추가합니다.
StringBuilder
를 제거하고 인덴트 레벨을 재설정하는 Reset
메서드를 추가합니다.
오브젝트의 현재 상태를 나타내는 문자열을 반환하는 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
인터페이스를 구현합니다. 이 인터페이스를 사용하면 프로퍼티 방문 시 방문 동작을 지정할 수 있으며, PropertyVisitor
클래스의 VisitProperty
메서드와 유사합니다.
DumpObjectVisitor
클래스 안에서 IPropertyBagVisitor.Visit
및 IPropertyVisitor.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.
}
방문자 내부 상태에 대한 액세스 권한이 필요하므로 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
메서드에 전달하십시오.
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
메서드를 호출합니다. 이렇게 하면 원하는 출력을 얻을 수 있습니다.