원문: Anders Hejlsberg and Mads Torgersen, The C# Programming Language (Covering C# 4.0) (4th Edition) (Nov 10, 2010)

박싱(boxing)과 언박싱(unboxing)은 C#의 타입 시스템의 중추로서, 값 타입(value-types)과 참조 타입(reference-types) 둘이 서로 변환되게 해주는 역할을 한다. 이로써 박싱과 언박싱은 어떤 타입의 값도 결국 object로 간주되는 타입 시스템에 있어 통일된 관점을 제공한다.

JOSEPH ALBAHARI: C# 2.0 이전에는 박싱과 언박싱은 프로그래머가 리스트, 스택, 큐와 같은 일반화된 콜렉션(collection)을 만드는데 쓰이는 도구에 불과했다. C# 2.0이 소개된 이후, 더 나은 정적 타입 안정성과 성능을 보장하는 제네릭(generics)이 이러한 목적을 달성할 수 있는 대안으로서 제공되었다. 박싱과 언박싱은 값 복사, 간접 참조(indirection), 힙에 대한 메모리 할당을 의미하기 때문에 필연적으로 약간의 오버헤드를 요구한다.

JESSE LIBERTY 나는 더 나아가, 모든 실용적인 목적에 있어 제네릭이 박싱과 언박싱을 기존의 중요한 관심거리에서 몰아내고 값 타입을 out이나 ref 인자로 전달하는 경우에나 신경써야 하는 지엽적인 문제로 만들었다고 본다.

CHRISTIAN NAGEL 대개의 경우 박싱과 언박싱에 뒤따르는 오버헤드는 소소하지만, 거대한 콜렉션을 순회하는 작업에서는 매우 큰 오버헤드로 확대될 수 있다. 제네릭 콜렉션이 이러한 문제의 해법이 될 수 있다.

박싱 변환 (Boxing Conversion)

박싱 변환은 값 타입이 암묵적으로 참조 타입으로 변환되게 해준다. 박싱 변환에는 다음의 경우들이 있다:

  • 값 타입에서 object 타입으로.
  • 값 타입에서 System.ValueType으로.
  • non-nullable한 값 타입에서 값 타입으로 구현된 인터페이스 타입으로.
  • nullable 타입에서 nullable한 타입으로 구현된 인터페이스 타입으로.
  • enum 타입에서 System.Enum 타입으로.
  • enum 타입으로 구현된 nullable 타입에서 System.Enum 타입으로.

즉, 타입 인자로 전달되는 암묵적 형변환의 경우에는 박싱 변환으로 수행되며 런타임의 경우 값 타입에서 참조 타입으로의 변환이 유도된다는 것이다.

non-nullable한 값 타입의 값을 박싱하는 것은 새로운 객체(object) 인스턴스를 할당하고 기존 값을 새로운 인스턴스에 할당하는 동작을 동반한다.

nullable한 타입의 값을 박싱할 때, 만약 해당 값이 null 값이라면 (즉, HasValue가 true라면) null 참조를 반환하며, 만약 해당 값이 null이 아니라면 해당 값에 해당하는 결과를 반환한다.

Non-nullable한 값 타입의 박싱 과정은 제네릭한 박싱 클래스(boxing class)가 존재한다고 가정하는 것으로 가장 잘 설명할 수 있다. 이 박싱 클래스가 다음 선언처럼 작동한다고 가정하자:

sealed class Box<T>: System.ValueType
{
    T value;

    public Box(T t)
    {
        value = t;
    }
}

여기서 T 타입의 v라는 값을 박싱한다고 한다면 이는 식 new Box(v)의 실행을 포함하며 해당 타입의 object 값을 반환하게 된다. 이에 따라,

int i = 123;
object box = i;

위의 문장들은 개념적으로 다음과 같다:

int i = 123;
object box = new Box<int>(i);

물론, Box와 같은 박싱 클래스는 **실제로는 존재하지 않는다**. 그리고 박싱된 값의 동적 타입은 실제로는 클래스 타입이 아니다. 대신 T 타입의 박싱된 값은 동적 타입 T에 해당하며, is 연산자를 통한 동적 타입 체크를 통해서는 T 타입를 참조하게 된다. 예를 들어,

int i = 123;
object box = i;
if (box is int)
{
   Console.Write("Box contains an int"); 
}

위와 같은 코드는 콘솔에 “Box contains an int”라는 문장을 출력하게 된다.

박싱 변환은 박싱되는 값의 복사본(copy)을 생성하는 것을 의미한다. 이는 참조 타입에서 object 타입으로의 변환과는 다른 동작으로서, 참조 타입을 object로 변환할 결과가 여전히 동일한 인스턴스를 가리키고 있는 것과는 다르다. 이 경우에는 단지 기반 타입을 가리키는 참조일 뿐이다. 예컨대, 다음 정의에 있어서

struct Point
{
    public int x, y;
    public Point(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
}
Point p = new Point(10, 10);
object box = p;
p.x = 20;
Console.Write(((Point)box).x);

위와 같은 코드는 콘솔에 20이 아닌 10을 출력할 뿐이다. 왜냐하면 Point 타입의 p라는 객체에서 object 타입의 box라는 객체로의 변환에서 새로운 인스턴스가 복사되었기 때문이다. 만일 Point를 구조체가 아닌 클래스로 정의했다면, 위 코드에서 p와 box 객체는 모두 동일한 인스턴스를 가리킬 것이기 때문에 10이 아닌 20을 출력할 것이다.

언박싱 변환 (Unboxing Conversion)

언박싱 변환은 참조 타입에서 명시적으로 값 타입으로 변환되게 해준다. 언박싱 변환에는 다음의 경우들이 있다:

  • object 타입에서 값 타입으로.
  • System.ValueType에서 값 타입으로.
  • 인터페이스 타입에서 해당 인터페이스를 구현한 non-nullable한 값 타입으로.
  • 인터페이스 타입에서 해당 타입을 구현하는 nullable한 타입으로.
  • System.Enum에서 enum 타입으로.
  • System.Enum 타입에서 enum 타입을 가진 nullable한 타입으로.

즉, 인자로 전달된 타입으로의 명시적 변환은 언박싱 변환으로 수행되며 런타임의 경우 참조 타입에서 값 타입으로의 변환이 이루어진다는 것이다.

non-nullable한 값 타입으로의 언박싱 동작은 우선 해당 오브젝트 인스턴스가 해당 타입의 박싱된 값인지 확인한 뒤 그 이후에 해당 인스턴스로부터 값을 복사하는 동작에 해당한다.

nullable한 타입의 언박싱의 경우, 언박싱 연산을 통해 가공되는 피연산자가 null인 경우에는 null을 반환하며, 해당 피연산자가 null이 아닌 경우에는 해당 object 객체에 대한 결과를 반환한다.

박싱에 대한 절에서 언급헀던 가상의 박싱 클래스를 통해 설명하자면, 언박싱 변환은 object 타입의 box에서 T 타입의 값 타입으로의 변환은 ((Box) box).value라는 식의 실행으로 구성된다. 따라서,

object box = 123;
int i = (int)box;

위의 문장들은 개념적으로 다음과 같다:

object box = new Box<int>(123);
int i = ((Box<int>)box).value;

런타임에 non-nullable한 값 타입으로의 언박싱 변환이 성공한 경우, 해당 언박싱 연산을 통해 가공되는 피연산자의 값은 반드시 non-nullable한 값 타입의 박싱된 값에 대한 참조일 것이다. 만약 피연산자가 null이라면, System.NullReferenceException 예외가 발생한다. 만약 피연산자가 호환되지 않는(incompatible) 객체에 대한 참조라면, System.InvalidCastException 예외가 발생한다.

런타임에 nullable한 값 타임으로의 언박싱 변환이 성공한 경우, 해당 언박싱 연산을 통해 가공되는 피연산자의 값은 null이거나 nullable한 타입의 non-nullable한 값 타입에 대한 박싱된 값의 참조일 것이다. 만약 피연산자가 호환되지 않는 개체에 대한 참조라면, System.InvalidCastException 예외가 발생한다.

<끝>