본문 바로가기
C#

[C#] 값 형식과 참조 형식, 박싱과 언박싱(힙 메모리), is 연산자와 as 연산자

by RucA 2024. 4. 25.
728x90
반응형

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

 

728x90
반응형