본문 바로가기

C# 프로그래밍/문법 개념

[C#] 박싱과 언박싱 (Boxing & Unboxing)

반응형

박싱과 언박싱이 무엇일까?

공식 문서에는 다음과 같이 설명되어 있다.

Boxing은 값형식을 object 형식 또는 이 값 형식에서 구현된 임의의 인터페이스 형식으로 변환하는 프로세스입니다. CLR(공용 언어 런타임)은 값 형식을 boxing할 때 값을 System.Object 인스턴스 내부에 래핑하고 관리되는 힙에 저장합니다. unboxing하면 개체에서 값 형식이 추출됩니다. Boxing은 암시적이며 unboxing은 명시적입니다. Boxing 및 unboxing의 개념은 개체로 처리할 수 있는 모든 값 형식에서 형식 시스템의 C#에 통합된 뷰의 기반이 됩니다.

 

이게 무슨 말인지 하나씩 알아보면 다음과 같다.

1. Boxing은 값형식을 object 형식 또는 이 값 형식에서 구현된 임의의 인터페이스 형식으로 변환하는 프로세스...
 값 타입(int, float, long, struct...)이 object형식(모든 타입의 최상위 형식)으로 변환되는 과정을 박싱이라고 하고 반대로 값 타입으로 되돌아 오는 과정을 언박싱이라고 한다. 참조 타입이 object로 변환되는 것은 박싱이 아니다. 그냥 object형식으로의 업캐스팅이다.
2. CLR(공용 언어 런타임)은 값 형식을 boxing할 때 값을 System.Object 인스턴스 내부에 래핑하고 관리되는 힙에 저장...
박싱과 언박싱을 하는 주체는 CLR(소스코드를 .Net에서 동작하도록 해주는 가상머신)이다. 박싱 혹은 언박싱이 되는 타입은 값 타입인데, 이 값이 Object형식으로 인스턴스화 되어 저장되어 스택이 아닌 힙에 저장된다는 의미이다.
3. Boxing은 암시적이며 unboxing은 명시적...
암시적이라는것은 형변환이 암시적, 다시말해 암시적 형변환인 업캐스팅에 해당하고 형변환이 명시적이라는것은 다운캐스팅에 해당한다. 이는 object형식이 최상위 클래스이기 때문에 생기는 당연한 결과이다.
4. Boxing 및 unboxing의 개념은 개체로 처리할 수 있는 모든 값 형식에서 형식 시스템의 C#에...
object는 모든 타입의 최상위 형태(root class)이므로 이를 통해 서로 다른 형식도 object형식으로 일괄적인 처리가 가능하다.

 

정리하자면 값 형식을 오브젝트 클래스 형식으로 변환할 때 박싱이, 오브젝트 클래스에서 값 형식으로 다시 변환할 때 언박싱이 이루어지며 박싱은 암시적 형 변환, 언박싱은 명시적 형 변환을 해야 한다.


어떠한 방식으로 박싱과 언박싱이 이루어지는가?

값 타입을 오브젝트 형식으로 변환하여 박싱이 이루어지면 힙 영역에 object형식의 인스턴스가 할당되고 해당 값이 복사된다.

 

반대로 언박싱이 이루어지면 힙 영역에 할당된 object형식을 찾아서 인스턴스의 값을 형식 변수에 복사 붙여 넣기 한다.

 

float f = 0.1f;

object o = f;

f = 0.2f;

Debug.Log(f);
Debug.Log(o);

위 코드를 실행하면 순서대로 0.2f와 0.1f라는 결과를 얻는다. 왜냐면 박싱할 때 f의 값을 복사해서 o가 갖고 있기 때문이다. 따라서 f가 변경된다 하더라도 o는 기존의 값만을 갖고 있을 뿐이다.


어떻게 사용할 수 있을까?

아래의 스크립트 예시를 통해 확인하자.

public class BoxingAndUnBoxing : MonoBehaviour
{
    public List<object> myArrayList = new List<object>();
    //오브젝트 형식의 리스트 선언

    void Start()
    {
        myArrayList.Add(5); // 정수형
        myArrayList.Add("hello"); // 문자열형식
        myArrayList.Add(new MyStruct()); // 구조체 형식
        myArrayList.Add(1.2f); // 실수형
        myArrayList.Add(new int[3]{1,2,3}); //정수형 배열 형식
	// 오브젝트 리스트에 값 넣기
        
        Debug.Log(myArrayList[1]);
        Debug.Log(((System.Int32[])myArrayList[4])[0]);
	// 오브젝트 리스트에서 값 꺼내오기
        
        Debug.Log(myArrayList[1].GetType());
        Debug.Log(((System.Int32[])myArrayList[4])[0].GetType());
        // 각 값들의 타입 확인
    }
}

public struct MyStruct
{
	//something...
}

 

로그를 찍었을 때 결과는 다음과 같다.

로그 결과

myArrayList에 들어간 타입은 모두가 서로 다르지만 첫 번째와 네 번째 값을 꺼냈을 때 모두 올바르게 저장되었다가 꺼내지는 것을 확인할 수 있다. 이것은 리스트뿐만 아니라 object [] 형태의 배열로 만들었을 때에도 똑같이 동작한다. 다시 말해서 어떤 타입이던지 object클래스로 박싱이 가능하고 이를 이용해 배열이나 리스트로 한 번에 관리가 가능하다는 것이다.

 

3번째와 4번째 로그는 해당 값들의 타입을 출력한 것이다. 확인해 보면 처음 넣어주었던 string과 Int32(배열의 첫 번째 요소)의 형식이 정확히 출력되고 있다. 로그를 찍으면서 오브젝트 형식이었던 것이 다시 언박싱되어 원래 형식으로 돌아오게 된 것이다.


유의해야 할 점

공식문서에 따르면 박싱을 진행하면 참조 할당 때보다 약 20배나 더 오래 걸린다고 한다(언박싱은 4배). 박싱과 언박싱은 컬렉션(Collection) 클래스에서도 발생하게 되는데, 컬렉션의 클래스는 모든 타입의 값들을 다 박싱 해서 저장하고 언박싱해서 불러오기 때문이다. 그러나 항상 박싱과 언박싱이 발생하는 것은 아니다. "값 형식을 object 형식 또는 이 값 형식에서 구현된 임의의 인터페이스 형식으로 변환" 에서 알 수 있듯이 값 형식이 변환될 때 박싱이 이루어 지므로 값 타입이 ArrayList 같은 클래스에 담기면 실행 속도의 문제를 일으킬 수 있다.

 

따라서 박싱이 일어날 경우 일반화가 가능한 제네릭<T> 사용을 권장하고 있다. 제네릭을 사용하면 값 타입이더라도 박싱과 언박싱이 발생하지 않기 때문이다.

 

 

반응형