개요
.NET에서는 여러 .NET 언어를 지원하기 위해 공용 타입 시스템(CTS; Common Type System)을 지원한다. .NET의 모든 형식은 값 타입(Value Type) 혹은 참조 타입(Reference Type)으로 구분되며 모든 타입은 기본 타입인 System.Object에서 파생된다. C#에서는 아래와 같은 계층을 가지고 있다.
값 타입
C#에서 값 타입은 구조체 / 열거형 / 그 외에 기본 제공 타입으로 구성된다. 값 타입은 아래와 같은 특징을 갖는다.
- 구조체를 제외한 모든 타입은 System.ValueType에서 파생된다.
- 스택 메모리에 직접 값이 포함된다. 다시 말해 복사가 일어난다.
- 상속이 불가능하다.
- 구조체 멤버 중에 참조 타입이 있다면 메모리 주소가 복사된다.*
* 얕은 복사(Shallow Copy)라고 한다.
참조 타입
C#에서 참조 타입은 클래스 / 대리자 / 배열 / 인터페이스가 있다. 참조 타입은 아래와 같은 특징이 있다.
- 힙 메모리에 인스턴스가 할당된다.
- 참조 타입의 변수는 인스턴스의 주소에 대한 참조를 가진다.
- 널(Null)을 할당할 수 있다.
박싱과 언박싱
박싱(Boxing)은 값 타입을 object 타입 또는 값 타입에서 구현된 임의의 인터페이스 타입으로 변환하는 프로세스며, 언박싱(Unboxing)은 박싱된 인스턴스에서 값 타입을 추출하는 프로세스다. 박싱이 일어날 땐 object 타입의 새로운 인스턴스를 생성해 값을 힙으로 복사하고, 언박싱이 이뤄질 땐 힙에서 스택으로 값이 복사된다. 또한, 박싱은 암시적으로 이뤄지며, 언박싱은 명시적으로 이뤄진다.
박싱과 언박싱은 많은 계산 과정을 필요로 하기 때문에 코드를 작성할 시 주의를 요구한다. 이를 실제로 확인하기 위해 아래와 같은 테스트 코드를 작성했다.
using System.Collections;
TimeSpan CalculateElapsedTime(Action action)
{
DateTime start = DateTime.Now;
action();
return DateTime.Now - start;
}
ArrayList arrayList = new ArrayList();
TimeSpan withBoxing = CalculateElapsedTime(() =>
{
for (int i = 0; i < 10000; ++i)
{
arrayList.Add(1);
}
});
Console.WriteLine($"박싱이 일어나는 경우에 걸린 시간 : {withBoxing.Ticks * 10} us");
List<int> list = new List<int>();
TimeSpan withoutBoxing = CalculateElapsedTime(() =>
{
for (int i = 0; i < 10000; ++i)
{
list.Add(1);
}
});
Console.WriteLine($"박싱이 일어나지 않은 경우에 걸린 시간 : {withoutBoxing.Ticks * 10} us");
TimeSpan withUnboxing = CalculateElapsedTime(() =>
{
for (int i = 0; i < arrayList.Count; ++i)
{
int element = (int)arrayList[i];
}
});
Console.WriteLine($"언박싱이 일어나는 경우에 걸린 시간 : {withUnboxing.Ticks * 10} us");
TimeSpan withoutUnboxing = CalculateElapsedTime(() =>
{
for (int i = 0; i < list.Count; ++i)
{
int element = list[i];
}
});
Console.WriteLine($"언박싱이 일어나지 않은 경우에 걸린 시간 : {withoutUnboxing.Ticks * 10} us");
|
이 코드에 대한 결과는 아래와 같다.
박싱의 경우 약 30배 정도의 차이가 있으며, 언박싱은 약 1.1배의 차이가 있다. 언박싱의 경우 int가 아닌 구조체라면 더 걸릴 것이다. 따라서 박싱과 언박싱이 일어나지 않도록 주의하자.