C#에서 데이터 형식은 값에 접근하는 방식에 따라 값 형식(value type)과 참조 형식(reference type)으로 나눌 수 있다. 또한 데이터를 전달하는 과정에서 값 형식과 참조 형식을 서로 번갈아 가며 형 변환해야 할 필요가 생길 수 있다. 이때 박싱과 언박싱이 발생한다. 이 내용은 제네릭 포스팅에서 간단히 언급했었는데, 왜 박싱, 언박싱이라고 표현하는지, 왜 추가적인 자원을 소모하는지를 공식 문서의 그림과 함께 메모리의 관점에서 좀 더 자세히 다뤄보겠다.
값 형식(value type)과 참조 형식(reference type)
- 값 형식 : 개체에 값 자체를 담고 있는 구조. int, double 등의 자료형은 기본적으로 값 형식의 데이터 구조이다.
- 값 형식으로 데이터를 전달한다는 것은 변수의 복사본을 전달한다는 의미이다.
- 구조체는 값 형식이다.
- 참조 형식 : 개체에 값을 담고 있는 또 다른 개체를 포인터로 가리키는 구조. 즉, 값을 가진 다른 개체의 주소를 가진다. object형은 대표적인 참조 형식이다.
- 참조 형식으로 데이터를 전달한다는 것은 변수에 대한 액세스를 전달한다는 의미다.
- 클래스 인스턴스는 참조 형식이다.
참조 형식의 필요성
값 형식으로 변수를 저장하고 그 변수를 활용해 데이터를 전달하고자 한다면, 기본적으로 변수의 복제본을 활용해 연산이 이루어지기 때문에 타겟 변수를 의도한 것처럼 변경할 수 없다. 따라서 주소를 통해 그 변수로 액세스하는 참조 형식이 필요하다.
- 값 형식과 참조 형식의 데이터 전달 방식의 차이
//공식 문서 코드
class TheClass
{
public string? willIChange;
}
struct TheStruct
{
public string willIChange;
}
class TestClassAndStruct
{
static void ClassTaker(TheClass c)
{
c.willIChange = "Changed";
}
static void StructTaker(TheStruct s)
{
s.willIChange = "Changed";
}
public static void Main()
{
TheClass testClass = new TheClass();
TheStruct testStruct = new TheStruct();
//메인 스택에서 생성 및 초기화
testClass.willIChange = "Not Changed";
testStruct.willIChange = "Not Changed";
//타 함수에서 재할당
ClassTaker(testClass);
StructTaker(testStruct);
//값 확인
Console.WriteLine("Class field = {0}", testClass.willIChange);
Console.WriteLine("Struct field = {0}", testStruct.willIChange);
}
}
//클래스는 참조 형식이므로 실제 변수를 참조해 변경했다.
//구조체는 값 형식이므로 복제된 변수를 변경했고, 원본 변수는 변경되지 않았다.
/* Output:
Class field = Changed
Struct field = Not Changed
*/
따라서 만약 특정한 데이터를 지속적으로 추적해 변경하고 싶다면, 이를 참조 형식의 변수에 저장하거나 값 형식 변수를 참조 형식의 변수로 변환해주어야 한다. 다만 값 형식과 참조 형식을 서로 변경할 때는 유의해야 할 점이 있다. 박싱과 언박싱에 대한 내용이다.
박싱(boxing)과 언박싱(unboxing)
- 박싱 : 값 형식의 데이터를 참조 형식의 데이터로 변환하는 작업. 박싱은 암시적으로 이루어진다.
int i = 123;
//암시적으로 박싱을 진행한다
object o = i;
값 형식인 int형 변수를 참조 형식인 object형으로 변환했으므로, 위 예제는 박싱이다. 그런데 왜 값 형식을 참조 형식으로 변환하는 것을 박싱이라 할까? 이는 변수를 변환하는 과정을 메모리의 관점에서 볼 필요가 있다.
- 박싱의 메모리 상의 과정
값 형식의 데이터를 참조 형식으로 변환할 때, 우선 값 형식의 데이터를 복사해서 힙 메모리에 저장한다. 그 다음, 데이터가 저장된 힙 메모리 영역의 주소를 반환해서 스택 메모리에 있는 참조 형식의 변수에 저장한다. 이를 통해 참조 형식의 데이터로 변환한 것처럼 보이는 것이다. 메모리의 관점에서 값 형식의 데이터가 힙 메모리에 저장되고, 이를 가리키는 구조가 되며 한 겹의 과정이 쌓였기 때문에, 박스에 포장을 한 것과 비슷한 구조가 된다. 런타임 중에 힙 메모리에 동적으로 할당할 필요가 있기 때문에, 추가적인 많은 자원을 소모하게 된다.
- 언박싱 : 참조 형식의 데이터를 값 형식의 데이터로 변환하는 작업. 언박싱은 명시적으로 이루어진다.
int i = 123;
//암시적으로 박싱을 진행한다
object o = i;
//명시적으로 언박싱을 진행한다
int j = (int)o
언박싱의 경우 object형 데이터를 원하는 값 형식으로 캐스팅하겠다는 것을 명시적으로 작성해주어야 한다. 언박싱 또한 메모리 관점에서의 표현이라고 볼 수 있다.
- 언박싱의 메모리 상의 과정
언박싱은 변환하고자 하는 참조 형식의 데이터가 존재하고(null 체크), 지정한 값 형식을 박싱한 것인지(캐스팅 체크) 체크한 후, 그 값을 값 형식 변수에 복사한다. 만약 언박싱하려고 하는 개체가 null인 경우 NullReferenceException이 발생하며, 호환되지 않는 경우(위의 경우 int형으로 캐스팅할 수 없는 타입) InvalidCastException이 발생한다. 이때, 형식이 적절한지 체크하기 위해 is 연산자를 사용하고, 빠르게 원하는 형식으로 변환하기 위해 as 연산자를 사용할 수 있다.
is 연산자와 as 연산자
- is 연산자 : 특정 개체가 특정 형식인지 검사할 때 사용한다.
- [개체].GetType() == typeof([형식])의 줄임 표현.
class ReferencePractice
{
static void Main()
{
object nullVal = null;
object realInt = 7;
object realString = "It's real!";
//null은 int가 아니다 : False
Console.WriteLine(nullVal is int);
//7은 int이다 : True
Console.WriteLine(realInt is int);
//It's real!은 string이다 : True
Console.WriteLine(realString is string);
}
}
//출력 : False True True
- as 연산자 : 특정 데이터를 특정 데이터 형식으로 변환할 때 사용한다.
- 해당 데이터 형식이면 변환, 아니면 null을 반환하는 것에 주의
class ReferencePractice
{
static void Main()
{
object intVal = 7;
object strVal = "string";
//intVal은 문자열이 아닌 정수형이므로 null이 반환
Console.WriteLine((intVal as string) is null);
//올바르면 형 변환
Console.WriteLine(strVal as string);
}
}
//출력 : True string
'C#' 카테고리의 다른 글
[C#] 이벤트(Event) (0) | 2024.04.26 |
---|---|
[C#] Action, Func, Predicate 제네릭 대리자(델리게이트; Delegate)와 매개변수에 메서드 전달 (0) | 2024.04.26 |
[C#] 대리자(Delegate; 델리게이트)와 무명 메서드(+람다식 기초) (1) | 2024.04.26 |
[C#] 참조 매개 변수, ref와 out의 차이점 (0) | 2024.04.25 |
[C#] 다차원 배열과 가변 배열(C/C++ 문법과 차이점) (0) | 2024.04.25 |
[C#] 널(NULL) 관련 형식[Nullable<T>] 및 연산자[??, ?.] (1) | 2024.04.24 |
[C#] 제네릭(Generic) 클래스 (0) | 2024.04.24 |
[C#] 컬렉션(Collection) 클래스 (0) | 2024.04.24 |