지난 포스트에서 C#의 기초 자료형에서 값 형식을 알아봤다. 여기서는 참조형식에 대해서 알아본다.
참조는 실제 값(들)이 저장되어 있는 장소를 가리키는 데이터이다. 값 형식의 변수에는 데이터가 하나만 저장된다. 그에 반해 참조형식에는 두개의 변수가 하나의 같은 개체를 참조(가리킴)할 수 있다. 그래서 한 변수에 대한 작업이 다른 개체의 값에 영향을 미칠 수 있다.
클래스는 C#의 대표적인 참조형식이다. 사용자는 class 키워드로 자신의 참조형식을 선언, 정의할 수 있다.
C#은 아래와 같은 기본 참조 형식을 제공한다.
name | class |
object | System.Object |
string | System.String |
dynamic | System.Object |
값 형식의 변수에는 해당 데이터가 직접 저장되고, 참조형 타입의 변수에는 데이터에 대한 참조가 저장된다. 메모리의 상황을 보면 이해가 빠르다. 아래 예제를 실행해보자.
using System;
namespace CsharpDatatype
{
class SizeofDataType
{
static void Main(string[] args)
{
int i = 42;
char ch = 'A';
bool result = true;
object obj = 42;
string str = "Hello";
byte[] bytes = { 1, 2, 3 };
Console.WriteLine($"int 형 자료 {i} 는 스택에 {sizeof(int)}바이트 값이 저장된다");
Console.WriteLine($"char 형 자료 {ch} 는 스택에 {sizeof(char)}바이트 값이 저장된다");
Console.WriteLine($"bool 형 자료 {result} 는 스택에 {sizeof(bool)}바이트 값이 저장된다");
Console.WriteLine($"object 형 자료 {obj} 는 스택에 주소가 저장되고" +
$" 힙 영역에 실제 object(int) 값이 저장된다.");
Console.WriteLine($"string 형 자료 {str} 는 스택에 주소가 저장되고" +
$" 힙 영역에 실제 string 값이 저장된다.");
Console.WriteLine($"byte 배열 자료 {bytes} 는 스택에 주소가 저장되고" +
$" 힙 영역에 실제 {sizeof(byte)} 바이트를 나열한 배열 값이 저장된다.");
}
}
}
프로그램이 실행되면 main 지역 블록의 변수들이 stack에 저장된다. 참조형인 object, string, byte는 메모리의 주소를 저장한다. 자기가 값을 가진게 아니라 있는 장소를 알기 때문에 참조라고 부른다. 이 참조의 실제 값은 힙 영역에 존재한다. 스택과 힙은 메모리의 다른 영역이다. 스택은 지역변수를 저장하는 공간이고 힙은 동적인 자료를 저장하는 곳이다.
이미지 출처: https://introprogramming.info/
이제 아래의 코드를 추가하면 메모리의 상태가 변경된다.
i = 0;
ch = 'B';
result = false;
obj = null;
str = "Bye";
bytes[1] = 0;
Stack 메모리에서 값 형식의 변수들의 내용이 바뀌었다. 주의해서 봐야할 것은 참조형이다. obj 는 null 즉 참조가 없다는 값으로 int형 참조의 연결이 끓어졌다. str 변수는 기존 Hello 문자열의 연결을 버리고 Bye 문자열로 새로 연결되었다. bytes 는 byte 배열 내부의 값을 변경하였다.
값 형식 Value Type 과 참조 형식 Reference Type의 차이점은 말로 설명하면 이해하기 어렵다. 이렇게 메모리의 구조를 도식화하여 들여다보다 보면 조금씩 이해가 된다. 메모리 구조를 이해하는 것은 컴퓨터의 동작 방식을 이해하는 길이다. 프로그래밍을 계속 한다면 더 많은 것들을 알게 될 것이다.
C#이 제공하는 기본 참조형
name | class |
object | System.Object |
string | System.String |
dynamic | System.Object |
1. string
참조 형식이 메모리에서 어떻게 작동되는지 봤으니 기본형인 string 부터 살펴보자.
string은 문자열을 말한다. 이름처럼 문자의 열이다. 기본 값 형식에 char 형이 하나의 문자만 저장한다면 string 은 여러개의 문자를 저장한다. 문자열을 만드는 키워드는 string 이다. 예제를 작성해보자.
using System;
namespace CsharpDatatype
{
class SizeofDataType
{
static void Main(string[] args)
{
string name = "Michael Jordan";
string job = "Basketball Player";
string job_name = name + " the " + job;
Console.WriteLine("His name is Mr." + name + ".");
Console.WriteLine("He is a " + job + ".");
Console.WriteLine("That's right! He is living legend " + job_name);
}
}
}
C#의 string 은 다양한 메소드를 가지고 있다. 별도의 포스트에서 다루도록 한다.
2. object
object 타입은 특별한 형식이다. .NET 프레임워크에서 모든 클래스의 부모가 된다. 클래스와 객체지향에 대하여 아직 모른다면 이는 이해하기 어려울 것이다. Java의 Object class와 비슷하다.
using System;
namespace CsharpDatatype
{
class SizeofDataType
{
static void Main(string[] args)
{
object master_ref1 = 5;
object master_ref2 = 1.2;
object master_ref3 = 8.5F;
object master_ref4 = 1.79e308;
object master_ref5 = "Reference String";
Console.WriteLine("{0,20}", master_ref1);
Console.WriteLine("{0,20}", master_ref2);
Console.WriteLine("{0,20}", master_ref3);
Console.WriteLine("{0,20}", master_ref4);
Console.WriteLine("{0,20}", master_ref5);
}
}
}
예제와 같이 어떤 타입을 사용하더라도 저장이 되는 만능 클래스이다. 모든 자료형을 다 담을 수 있다. 왜 다른 자료형을 다 사용할 있는지에 대한 의문이 있다면 클래스 챕터를 참고한다.
3. dynamic
dynamic 형은 C# 4에서 도입 되었다. 이름 처럼 다이나믹(동적인) 타입이다. 변수는 컴파일을 할 때와 실행될 때의 환경이 달라진다. 변수가 거치는 과정을 분리해서 보면 컴파일되는 컴파일 타임과 실행이 되는 런타임으로 나눌 수 있다. .NET SDK 설치시에 컴파일러가 설치되고 런타임 환경이 설치되는 것은 이 때문이다.
dynamic 키워드를 사용하면 참조형 변수의 타입이 런타임 즉 실행중에 실시간으로 변경 될 수 있다. int 형 정수가 char형 문자로 변경될 수 있다는 것을 의미한다. 예제를 보자.
using System;
namespace CsharpDatatype
{
class ObjectDynamic
{
static void Main(string[] args)
{
dynamic dyn = 1;
object obj = 1;
dyn = dyn + 3;
// obj = obj + 3;
// object 타입을 변경시 컴파일시 오류가 난다.
// Rest the mouse pointer over dyn and obj to see their
// types at compile time.
Console.WriteLine(dyn.GetType());
Console.WriteLine(obj.GetType());
dyn = "to String";
Console.WriteLine(dyn.GetType());
dyn = 1.79e308;
Console.WriteLine(dyn.GetType());
}
}
}
Type의 변화를 볼 수 있다. 실행시간 중에 Int32 -> String -> Double 형으로 자유롭게 변하고 있다. object 형이 그런 것 처럼 dynamic도 다른 타입들을 사용할 수 있다. object 형에게 똑같은 작업을 적용하면 object 형은 런타임에 변경될 수 없기 때문에 컴파일 시 오류가 난다. dynamic 은 이런 제약이 없다. 이 코드에 대한 논란이 있으나 기초 입문 단계에서는 이 정도만 이해해도 충분하다.
마지막으로 MSDN 의 예제를 실행해본다.
using System;
namespace DynamicExamples
{
class Program
{
static void Main(string[] args)
{
ExampleClass ec = new ExampleClass();
Console.WriteLine(ec.exampleMethod(10));
Console.WriteLine(ec.exampleMethod("value"));
//Console.WriteLine(ec.exampleMethod(10, 4));
dynamic dynamic_ec = new ExampleClass();
Console.WriteLine(dynamic_ec.exampleMethod(10));
//Console.WriteLine(dynamic_ec.exampleMethod(10, 4));
}
}
class ExampleClass
{
static dynamic field;
dynamic prop { get; set; }
public dynamic exampleMethod(dynamic d)
{
dynamic local = "Local variable";
int two = 2;
if (d is int)
{
return local;
}
else
{
return two;
}
}
}
}
// Results:
// Local variable
// 2
// Local variable
주석문 중에 첫번째 줄은 컴파일 에러를 낸다. 정적인 타입인 ExampleClass를 사용했기 때문이다. 오버로드를 해야한다는 에러 메시지가 나온다.
두번째 주석문을 풀고 빌드하면 컴파일 에러는 나지 않는다. dynamic 형 참조를 선언했기 때문이다. 오버로드를 했는지 검사하지 않는다. 그러나 실행시에 오류가 발생된다. 그러므로 dynamic 형을 사용하려면 적절한 예외처리를 해야한다.