박싱은 값을 힙으로 보내는 일이다
제네릭 편에서 이 질문을 던지고 답을 미뤄 뒀습니다.
object 담기(object 값) { ... }
아무거나 담고 싶으면 object 로 받으면 되지 않나. 그때는 “꺼낼 때 캐스팅을 해야 하고 타입이 틀리면 터진다” 까지만 말했습니다.
그것 말고 하나가 더 있습니다.
값 타입을
object에 넣는 순간 그 값은 힙으로 갑니다. 이걸 박싱이라고 합니다.
왜 그래야 하나
object 는 참조 타입입니다. 참조 타입 변수 안에 들어 있는 것은 값이 아니라 주소입니다.
그런데 int n = 10 의 10 은 스택의 n 자리에 그대로 들어 있습니다. 줄 주소가 없습니다.
스택 자리의 주소를 줄 수도 없습니다. 그 자리는 메서드가 끝나면 사라지는데 object 는 더 오래 살 수 있으니까요.
그래서 방법이 하나뿐입니다. 힙에 상자를 하나 만들고, 값을 거기 복사해 넣고, 그 주소를 줍니다.
n = 20 을 했는데 (int)o 가 여전히 10 인 것에 주목할 만합니다.
박싱은 값을 복사해 넣는 것이지 원래 변수를 가리키는 것이 아닙니다.
꺼낼 때는 타입이 정확히 맞아야 한다
언박싱은 캐스팅보다 까다롭습니다.
object o = 10; // int 를 박싱
int a = (int)o; // 됩니다
long b = (long)o; // 터집니다. InvalidCastException
int 에서 long 은 평소에 그냥 되는 변환입니다. 그런데 언박싱에서는 안 됩니다.
상자에 “이건 int 다” 라고 적혀 있고, 언박싱은 그 표를 그대로 확인하기 때문입니다.
한 단계씩 나눠야 합니다.
long b = (int)o; // 먼저 int 로 꺼내고, 그다음 long 으로
어디서 몰래 일어나는가
박싱이라고 써 붙인 자리는 없습니다. 대입 하나로 조용히 일어납니다.
목록에 넣을 때. 옛날 ArrayList 는 안이 object[] 입니다.
ArrayList list = new ArrayList();
for (int i = 0; i < 10000; i++)
{
list.Add(i); // 만 번 박싱
}
List<int> 는 안이 int[] 라서 한 번도 박싱하지 않습니다. 제네릭이 있는 이유가 이것이었습니다.
제네릭 편에서 “우리가 쓰던 것들이 전부 이것이었다” 고 한 자리의 나머지 절반입니다.
인터페이스로 받을 때.
struct 점 : IComparable { ... }
점 p = new 점();
IComparable c = p; // 박싱
인터페이스는 참조 타입입니다. struct 를 인터페이스 자리에 넣으면 박싱됩니다.
문자열에 섞을 때.
Console.WriteLine("점수: " + score);
+ 로 잇는 쪽은 요즘 컴파일러가 score.ToString() 으로 바꿔 줘서 박싱이 안 일어납니다.
하지만 string.Format 이나 옛날 방식의 가변 인자는 object[] 를 만들기 때문에 박싱이 남아 있습니다.
왜 신경 쓰나
한 번은 아무것도 아닙니다. 힙에 아주 작은 상자 하나가 생길 뿐입니다.
문제는 횟수입니다. 게임 루프처럼 초에 예순 번 도는 자리에서 프레임마다 수백 번 박싱하면 힙에 쓰레기가 계속 쌓입니다. 쌓이면 GC 가 돌고, GC 가 도는 동안 프레임이 한 번 튑니다.
값 자체가 무거워서가 아니라 할당이 쌓여서 문제가 됩니다. 이건 오브젝트 풀이 푸는 문제와 정확히 같은 문제입니다.
안 하는 법
대부분 제네릭으로 해결됩니다.
ArrayList list = new ArrayList(); // 박싱
List<int> list = new List<int>(); // 안 함
Hashtable h = new Hashtable(); // 박싱
Dictionary<string, int> d = new ...(); // 안 함
제네릭은 T 자리에 int 가 들어오면 int 를 그대로 담는 코드를 따로 만들어 줍니다.
그래서 object 를 거칠 일이 없습니다.
정렬이나 비교에서는 IComparable 대신 IComparable<T> 를 씁니다.
제네릭 쪽은 인터페이스지만 struct 가 박싱 없이 구현할 수 있게 처리됩니다.
정리
- 값 타입을
object나 인터페이스 자리에 넣으면 힙에 상자가 생깁니다. 이게 박싱입니다 - 넣을 때 복사, 꺼낼 때 복사입니다. 원래 변수와는 남남이 됩니다
- 언박싱은 상자에 적힌 타입과 정확히 같아야 합니다.
int를(long)으로 못 꺼냅니다 - 한 번은 싸고 만 번은 비쌉니다. 문제는 값이 아니라 할당 횟수입니다
List<int>,Dictionary<K,V>처럼 제네릭을 쓰면 안 일어납니다
박싱은 한 번은 싸고 만 번은 비쌌습니다. 값이 무거워서가 아니라 횟수 때문이었습니다.