Version: 2023.2
언어: 한국어
프로퍼티 경로
로우레벨 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. StringBuilder를 제거하고 인덴트 레벨을 재설정하는 Reset 메서드를 추가합니다.

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

하위 프로퍼티의 현재 상태 출력

데이터에 대한 방문자를 실행하면 기본적으로 지정된 오브젝트에 대한 방문이 바로 시작됩니다. 모든 프로퍼티 방문자의 경우 오브젝트의 하위 프로퍼티에 대한 방문을 시작하려면 PropertyPathPropertyContainer.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를 사용하여 프로퍼티 방문자 생성