[Unity] 나는 Boxing이 뭔지 알고 있을까

KYO
KYO
∙∙조회 14
[Unity] 나는 Boxing이 뭔지 알고 있을까

정의

Boxing is the process of converting a value type to the type object or to any interface type implemented by this value type. When the common language runtime (CLR) boxes a value type, it wraps the value inside a System.Object instance and stores it on the managed heap.

박싱은 값 타입을 object 타입 또는 그 값 타입이 구현한 인터페이스 타입으로 변환하는 과정이다. CLR은 값을 System.Object 인스턴스로 감싸 managed heap에 저장한다

1. 클래스의 필드로 구조체

csharp
using UnityEngine;

[System.Serializable]
public struct EnemyData
{
    public int id;
    public int biome;
    public EnemyData(int id, int biome)
    {
        this.id = id;
        this.biome = biome;
    }
}

public class Enemy : MonoBehaviour
{
    [SerializeField] private EnemyData data;
}
  • EnemyData data는 박싱되지 않았다.
  • EnemyData data는 힙에 있다.

2. 딕셔너리의 키로 구조체

csharp
using UnityEngine;
using System.Collections.Generic;

[System.Serializable]
public struct EnemyData
{
    public int id;
    public int biome;
    public EnemyData(int id, int biome)
    {
        this.id = id;
        this.biome = biome;
    }
}

public class EnemyManager : MonoBehaviour
{
    private readonly Dictionary<EnemyData, int> enemies = new(); // 뭔가 들어있다고 가정
    public bool IsExist(int id, int biome)
    {
        return enemies.TryGetValue(new EnemyData(id, biome), out var cnt) && cnt > 0;
    }
}
  • 조회시 ValueType.GetHashCode 호출돼서 박싱.
  • 동일 해시값끼리 비교할 때 Equals(object)에서 박싱.
  • EnemyData가 IEquatable 를 구현하고 GetHashCode()를 override 하면 두 박싱을 막을 수 있다.

3. 인터페이스로 받은 구조체

csharp
public struct EnemyData : IData { /* ... */ }
public static void SomeMethod(IData d) // SomeMethod(new EnemyData(...))
{
    //...
}
  • EnemyData -> IData 변환에서 박싱된다.

4. 제네릭 제약 조건으로 받은 구조체

csharp
public struct EnemyData : IData { /* ... */ }
public static void SomeMethod<T>(T data) where T : IData  // SomeMethod(new EnemyData(...))
{
    data.SomeInterfaceMethod();
}
  • 박싱 없다. 다음 포스트에서 자세히 다룰 예정.

5. Debug.Log(값타입)

  • 박싱 된다. Debug.Log(object message)이므로 값 타입 인수가 object로 박싱된다.

6. 값 타입의 람다 캡처(클로저)

  • 박싱 없다.
  • 대신 DisplayClass 인스턴스가 생길 때 할당은 있다.
    • DisplayClass 인스턴스는 캡처된 변수가 선언된 스코프에 진입할 때 생성된다.
  • 캡처된 값 타입은 DisplayClass의 필드다.

7. 코루틴 안에 값타입 변수 선언

csharp
private IEnumerator FadeOut(float duration)
{
    float elapsed = 0f;
    while (elapsed < duration)
    {
        elapsed += Time.deltaTime;
        //...
        yield return null;
    }
    gameObject.SetActive(false);
}
  • FadeOut을 호출하는 순간 힙 할당이 발생한다.
    • 정확히는 상태머신 클래스의 인스턴스가 생긴다.
    • StartCoroutine(FadeOut(0.3f))도 결국 FadeOut(0.3f)가 먼저 호출되므로 마찬가지.
    • elapsed와 duration은 Stack에 없다. Managed Heap에 있다. 그러나 박싱은 아니다.
      • 위의 상태머신 클래스 안에 필드로 elapsed와 duration이 있다.
      • gameObject.SetActive(false) 때문에 this 참조도 필드로 들어간다. this.gameObject로 접근해야하기 때문이다.

 

정리

#

상황

박싱

힙 할당

할당되는 것

1

클래스의 필드인 구조체

X

X (추가 할당 없음)

없음. Enemy 인스턴스 내부에 포함

2

IEquatable<T> 미구현 구조체를 Dictionary 키로 조회

O

O

박스 (조회마다)

3

인터페이스 타입 매개변수로 전달

O

O

박스 (호출마다)

4

제네릭 제약 조건 매개변수로 전달

X

X

없음

5

Debug.Log(값 타입)

O

O

박스 (호출마다)

6

람다의 값 타입 캡처

X

O

DisplayClass 인스턴스, delegate 인스턴스

7

코루틴 안의 값 타입 지역 변수

X

O

상태 머신 인스턴스 (FadeOut 호출마다)