# 로우레벨 API를 사용하여 프로퍼티 방문자 생성

> Learn how to use the IPropertyBagVisitor and IPropertyVisitor interfaces to create a property visitor from an example.

이 예시는 로우레벨 API를 [`IPropertyBagVisitor`](/engine/6000.0/script-reference/unity-properties/i-property-bag-visitor.md) 및 [`IPropertyVisitor`](/engine/6000.0/script-reference/unity-properties/i-property-visitor.md) 인터페이스와 함께 사용하여 프로퍼티 방문자를 만드는 방법을 보여 줍니다. 이 예시는 [`PropertyVisitor`](/engine/6000.0/script-reference/unity-properties/property-visitor.md) 기본 클래스를 사용하여 프로퍼티 방문자를 생성하는 [예시](/engine/6000.0/manual/scripting/object-oriented-development/properties/property-visitors-propertyvisitor.md)와 동일합니다.

## 개요 예시

이 예시에는 오브젝트의 현재 상태를 콘솔에 출력하는 프로퍼티 방문자를 만드는 단계별 명령이 포함되어 있습니다.

다음 유형이 있다고 가정해 봅시다.

```lang-csharp
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`를 생성합니다.

```lang-csharp
public static class DebugUtilities
{
    public static void PrintObjectDump<T>(T value)
    {
        // Magic goes here.
    }
}
```

다음과 같이 `Data` 오브젝트로 `PrintObjectDump` 메서드를 호출합니다.

```lang-csharp
DebugUtilities.PrintObjectDump(new Data());
```

그러면 콘솔에 다음이 출력됩니다.

```lang-text
- 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
```

## 방문자 생성##create-data-and-utility-classes

먼저 `IPropertyBagVisitor`를 구현하는 `DumpObjectVisitor` 클래스를 만듭니다. 클래스 내에서 [StringBuilder](https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder?view=net-7.0)를 사용하여 오브젝트의 현재 상태를 나타내는 문자열을 만듭니다.

1. `IPropertyBagVisitor` 인터페이스를 구현하는 `DumpObjectVisitor` 클래스를 만듭니다.

2. 클래스에 `StringBuilder` 필드를 추가합니다.

3. `StringBuilder`를 지우고 들여쓰기 레벨을 재설정하는 `Reset` 메서드를 추가합니다.

4. 오브젝트의 현재 상태를 나타내는 문자열을 반환하는 `GetDump` 메서드를 추가합니다.

   `DumpObjectVisitor` 클래스는 다음과 같습니다.

   ```lang-csharp
   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();
       }
   }
   ```

## 프로퍼티 가져오기##create-the-visitor

`DumpObjectVisitor` 클래스 내에서 [`IPropertyBagVisitor.Visit`](/engine/6000.0/script-reference/unity-properties/i-property-bag-visitor/visit.md) 메서드를 오버라이드하여 컨테이너 오브젝트의 프로퍼티를 반복합니다. 오브젝트 덤프 방문자에서 값을 표시하고 프로퍼티에 방문을 위임합니다.

`this`를 사용하여 프로퍼티의 [`Accept`](/engine/6000.0/script-reference/unity-properties/property-2/accept.md) 메서드를 호출하려면 [`IPropertyVisitor`](/engine/6000.0/script-reference/unity-properties/i-property-visitor.md) 인터페이스를 구현합니다. 이 인터페이스를 사용하면 [`PropertyVisitor`](/engine/6000.0/script-reference/unity-properties/property-visitor.md) 클래스의 [`VisitProperty`](/engine/6000.0/script-reference/unity-properties/property-visitor/visit-property.md) 메서드와 유사하게 프로퍼티를 방문할 때 방문 동작을 지정할 수 있습니다.

1. `DumpObjectVisitor` 클래스 내에 `IPropertyBagVisitor.Visit` 및 `IPropertyVisitor.Visit` 메서드 오버라이드를 추가합니다.

   ```lang-csharp
   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.
   }
   ```

2. [`PropertyVisitor`](/engine/6000.0/script-reference/unity-properties/property-visitor.md) 기본 클래스와 함께 사용되는 `IVisitPropertyAdapter` 어댑터는 방문자의 내부 상태에 액세스해야 하므로 해당 클래스 외부에서 사용할 수 없습니다. 하지만 필요한 정보가 포함된 도메인별 어댑터를 정의할 수 있습니다. `DumpObjectVisito` 클래스 내에서 먼저 어댑터를 사용하도록 [`IPropertyVisitor`](/engine/6000.0/script-reference/unity-properties/i-property-visitor.md)의 구현을 업데이트합니다.

   ```lang-csharp
   // 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)})");
       }
   }
   ```

완성된 코드는 다음과 같습니다.

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

## 하위 프로퍼티의 현재 상태 인쇄

기본적으로 데이터에서 방문자를 실행하면 해당 오브젝트에 대한 방문이 직접 시작됩니다. 모든 프로퍼티 방문자에 대해 오브젝트의 하위 등록 정보에 대한 방문을 시작하려면 [`PropertyContainer.Accept`](/engine/6000.0/script-reference/unity-properties/property-container/accept.md.md) 메서드에 [`PropertyPath`](/engine/6000.0/script-reference/unity-properties/property-path.md.md)를 전달합니다.

1. `DebugUtilities` 메서드를 업데이트하여 선택적 `PropertyPath`를 사용합니다.

   ```lang-csharp
   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` 메서드를 호출합니다. 이를 통해 [원하는 출력](#desired-output)을 얻습니다.

## 추가 리소스##get-the-properties

* [프로퍼티 방문자](/engine/6000.0/manual/scripting/object-oriented-development/properties/property-visitors.md)
* [프로퍼티 백](/engine/6000.0/manual/scripting/object-oriented-development/properties/property-bags.md)
* [프로퍼티 경로](/engine/6000.0/manual/scripting/object-oriented-development/properties/property-paths.md)
* [`PropertyVisitor`를 사용하여 프로퍼티 방문자 만들기](/engine/6000.0/manual/scripting/object-oriented-development/properties/property-visitors-propertyvisitor.md)
