기본 제공 참조 형식
기본 제공 참조 형식 (C# 참조)
C#에는 미리 만들어 둔 참조 형식이 꽤 많아요. 이 형식들 중에는 .NET 라이브러리의 특정 형식을 가리키는 키워드나 연산자를 그대로 형식 이름으로 쓰는 것들도 있어요. 이번 문서에서는 object, string, delegate, dynamic 같은 대표적인 기본 참조 형식이 각각 어떤 역할을 하는지, 그리고 언제 어떻게 쓰면 좋은지 하나씩 살펴볼게요.
출처: Microsoft Learn
본문
object 형식
object 형식은 .NET의 xref:System.Object?displayProperty=nameWithType를 가리키는 별명(alias)이에요. C#의 통합 형식 체계에서는 모든 형식—미리 정의된 형식이든 사용자가 정의한 형식이든, 참조 형식이든 값 형식이든—이 직접 또는 간접적으로 xref:System.Object?displayProperty=nameWithType를 상속받아요. 그래서 ref struct만 빼고(ref struct 참고) 어떤 형식의 값이든 object 형식 변수에 담을 수 있어요. 또 null 리터럴을 아무 object 변수에나 기본값으로 할당할 수 있죠.
값 형식 변수를 object로 변환하면 그 값이 박싱(boxing) 되고, 반대로 object 변수를 값 형식으로 되돌리면 언박싱(unboxing) 되어요. 이 과정에 대한 자세한 내용은 박싱과 언박싱에서 다루고 있어요.
string 형식
string 형식은 0개 이상의 유니코드 문자로 이루어진 시퀀스(a sequence)를 나타내요. .NET에서 string은 xref:System.String?displayProperty=nameWithType의 별명이에요.
재미있는 점이 하나 있어요. string은 참조 형식인데도, 동등 연산자 ==와 !=는 string 객체의 참조가 아니라 값을 비교해요. 값 기준으로 동등성을 비교하니까 문자열이 같은지 확인하는 일이 훨씬 직관적이죠. 예를 들어 볼게요.
string a = "hello";
string b = "h";
// Append to contents of 'b'.
b += "ello";
Console.WriteLine(a == b);
Console.WriteLine(object.ReferenceEquals(a, b));
위 예제의 출력은 True 다음에 False가 나와요. 두 문자열의 내용은 서로 같지만, a와 b는 같은 문자열 인스턴스를 가리키고 있지는 않기 때문이에요.
+ 연산자는 문자열을 이어 붙여요(concatenation).
string a = "good " + "morning";
이 코드는 "good morning"이라는 내용을 담은 문자열 객체를 만들어요.
문자열은 불변(immutable) 이에요. 문자열 객체를 한 번 만들고 나면 그 내용을 바꿀 수 없어요. 예를 들어 아래 코드를 실행하면, 컴파일러는 실제로는 새로운 문자열 객체를 만들어 새 문자 시퀀스를 담고, 그 새 객체를 b에 할당해요. 그리고 원래 b가 담고 있던 "h" 문자열에 할당된 메모리는 이후 가비지 컬렉션의 대상이 되죠.
string b = "h";
b += "ello";
[] 연산자를 쓰면 문자열의 개별 문자를 읽기 전용으로 접근할 수 있어요. 유효한 인덱스 값은 0부터 시작하고, 문자열의 길이보다는 작아야 해요.
string str = "test";
char x = str[2]; // x = 's';
비슷한 방식으로 [] 연산자를 써서 문자열의 각 문자를 순회할 수도 있어요.
string str = "test";
for (int i = 0; i < str.Length; i++)
{
Console.Write(str[i] + " ");
}
// Output: t e s t
문자열 리터럴
문자열 리터럴은 형식이 string이고, raw(원시), quoted(인용), verbatim(축어) 세 가지 형태로 나뉘어요.
Raw 문자열 리터럴은 이스케이프 시퀀스 없이 임의의 텍스트를 그대로 담을 수 있어요. 공백과 줄바꿈, 따옴표, 기타 특수 문자도 포함할 수 있죠. Raw 문자열 리터럴은 최소 세 개의 큰따옴표(""")로 둘러싸요.
"""
This is a multi-line
string literal with the second line indented.
"""
심지어 큰따옴표를 세 개, 혹은 그 이상 연속으로 넣을 수도 있어요. 텍스트 안에 여러 개의 따옴표가 연속으로 들어가야 한다면, 그에 맞춰서 raw 문자열 리터럴의 시작과 끝에 따옴표를 더 많이 붙여 쓰면 돼요.
""""""
This raw string literal has four """"", count them: """" four!
embedded quote characters in a sequence. That's why it starts and ends
with five double quotes.
You could extend this example with as many embedded quotes as needed for your text.
""""""
Raw 문자열 리터럴은 보통 시작 따옴표와 끝 따옴표를 본문 텍스트와 별도 줄에 배치해요. 여러 줄로 된 raw 문자열 리터럴은 그 내용이 다시 인용 문자열인 경우도 자연스럽게 처리해 줘요.
var message = """
"This is a very important message."
""";
Console.WriteLine(message);
// output: "This is a very important message."
시작 따옴표와 끝 따옴표가 서로 다른 줄에 있으면, 여는 따옴표 바로 다음의 줄바꿈과 닫는 따옴표 바로 앞의 줄바꿈은 최종 내용에 포함되지 않아요. 그리고 닫는 따옴표 시퀀스가 문자열 리터럴의 가장 왼쪽 열을 결정해요. 그러니까 raw 문자열 리터럴을 전체 코드 들여쓰기에 맞춰서 정렬할 수 있죠.
var message = """
"This is a very important message."
""";
Console.WriteLine(message);
// output: "This is a very important message."
// The leftmost whitespace is not part of the raw string literal
닫는 따옴표 시퀀스보다 오른쪽에 있는 열은 그대로 보존돼요. 덕분에 아래 예시처럼 JSON, YAML, XML 같은 데이터 형식을 raw 문자열 하나로 편하게 담을 수 있어요.
var json= """
{
"prop": 0
}
""";
[!INCLUDEraw-string-tip]
텍스트 줄 중 하나라도 닫는 따옴표 시퀀스보다 왼쪽으로 벗어나면 컴파일러가 오류를 내요. 여는 따옴표와 닫는 따옴표를 같은 줄에 둘 수도 있는데, 이 경우 문자열 리터럴은 따옴표 문자로 시작하거나 끝나면 안 돼요.
var shortText = """He said "hello!" this morning.""";
raw 문자열 리터럴을 문자열 보간(string interpolation)과 함께 쓰면, 출력 문자열 안에 따옴표 문자와 중괄호까지 넣을 수 있어요.
Quoted(인용) 문자열 리터럴은 큰따옴표(")로 둘러싸요.
"good morning" // a string literal
문자열 리터럴에는 모든 문자 리터럴을 담을 수 있고, 이스케이프 시퀀스도 포함할 수 있어요. 아래 예제는 역슬래시를 나타내는 \\, 문자 f를 나타내는 \u0066, 줄바꿈을 나타내는 \n을 사용했어요.
string a = "\\\u0066\n F";
Console.WriteLine(a);
// Output:
// \f
// F
[!NOTE] 이스케이프 코드
\udddd(여기서dddd는 네 자리 숫자)는 유니코드 문자 U+dddd를 나타내요. 여덟 자리 유니코드 이스케이프 코드\Udddddddd도 인식돼요.
**Verbatim(축어) 문자열 리터럴**은 @로 시작하고 큰따옴표로 둘러싸요. 예를 들면 다음과 같아요.
@"good morning" // a string literal
Verbatim 문자열의 장점은 이스케이프 시퀀스를 처리하지 않는다는 거예요. 그래서 쓰기가 훨씬 편하죠. 예를 들어 아래 텍스트는 전체 경로(full path)의 Windows 파일 이름과 일치해요.
@"c:\Docs\Source\a.txt" // rather than "c:\\Docs\\Source\\a.txt"
@로 시작하는 문자열 안에 큰따옴표를 넣고 싶다면, 그 따옴표를 두 번 연속으로 쓰면 돼요.
@"""Ahoy!"" cried the captain." // "Ahoy!" cried the captain.
UTF-8 문자열 리터럴
.NET은 문자열을 UTF-16 인코딩으로 저장해요. 반면 UTF-8은 웹 프로토콜을 비롯한 중요한 여러 라이브러리에서 쓰는 표준이에요. 문자열 리터럴에 u8 접미사를 붙이면 UTF-8 인코딩을 지정할 수 있어요. 컴파일러는 UTF-8 리터럴을 ReadOnlySpan<byte> 객체로 저장해요. UTF-8 문자열 리터럴을 쓰면 아래 코드처럼 같은 내용을 xref:System.ReadOnlySpan`1?displayProperty=nameWithType으로 직접 선언하는 것보다 훨씬 명확하게 표현할 수 있어요.
ReadOnlySpan<byte> AuthWithTrailingSpace = new byte[] { 0x41, 0x55, 0x54, 0x48, 0x20 };
ReadOnlySpan<byte> AuthStringLiteral = "AUTH "u8;
UTF-8 문자열 리터럴을 배열로 저장하고 싶다면, xref:System.ReadOnlySpan`1.ToArray?displayProperty=nameWithType를 써서 리터럴이 담긴 바이트들을 변경 가능한(mutable) 배열로 복사하면 돼요.
byte[] AuthStringLiteral = "AUTH "u8.ToArray();
참고로 UTF-8 문자열 리터럴은 컴파일 타임 상수가 아니라 런타임 상수예요. 그래서 선택적 매개변수(optional parameter)의 기본값으로는 쓸 수 없어요. 또 문자열 보간과도 함께 쓸 수 없어요. 같은 문자열 식에 $ 토큰과 u8 접미사를 동시에 쓸 수 없는 거죠.
delegate 형식
delegate 형식의 선언은 메서드 시그니처와 비슷하게 생겼어요. 반환 값 하나와, 임의의 형식으로 된 매개변수를 원하는 만큼 갖죠.
public delegate void MessageDelegate(string message);
public delegate int AnotherDelegate(MyType m, long num);
.NET의 System.Action과 System.Func 형식은 흔하게 쓰는 여러 delegate의 제네릭 정의를 제공해요. 그래서 사실 직접 새 커스텀 delegate 형식을 정의할 일은 별로 없어요. 대신 이 제네릭 형식들의 인스턴스를 만들어 쓰면 되거든요.
delegate는 이름 있는 메서드나 익명 메서드를 캡슐화(encapsulate)할 수 있는 기본 제공 참조 형식이에요. 델리게이트는 C++의 함수 포인터와 비슷하지만, 형식 안전(type-safe)하고 안전(secure) 하다는 점이 달라요. 델리게이트의 활용은 델리게이트와 제네릭 델리게이트 문서에서 다루고 있고, 델리게이트는 이벤트(Events)의 기반이 되기도 해요. 델리게이트는 이름 있는 메서드나 익명 메서드 중 하나와 연결해 인스턴스화해요.
델리게이트를 인스턴스화하려면 호환되는 반환 형식과 입력 매개변수를 가진 메서드나 람다 식으로 연결해 줘야 해요. 메서드 시그니처에서 어느 정도의 분산(variance)이 허용되는지는 델리게이트의 분산에서 확인할 수 있어요. 익명 메서드와 함께 쓸 때는 델리게이트 선언과 그에 연결할 코드를 함께 선언해요.
한 가지 주의할 점이 있어요. 런타임에 관련된 delegate 형식이 분산 변환(variant conversion) 때문에 서로 달라지면, 델리게이트의 결합(combination)이나 제거(removal)가 런타임 예외로 실패할 수 있어요. 아래 예시가 바로 그런 상황을 보여줘요.
Action<string> stringAction = str => {};
Action<object> objectAction = obj => {};
// Valid due to implicit reference conversion of
// objectAction to Action<string>, but might fail
// at run time.
Action<string> combination = stringAction + objectAction;
올바른 런타임 형식을 가진 델리게이트를 만들려면 새 델리게이트 객체를 생성하면 돼요. 아래 예시는 앞선 예시에 이 우회 방법을 적용한 모습이에요.
Action<string> stringAction = str => {};
Action<object> objectAction = obj => {};
// Creates a new delegate instance with a runtime type of Action<string>.
Action<string> wrappedObjectAction = new Action<string>(objectAction);
// The two Action<string> delegate instances can now be combined.
Action<string> combination = stringAction + wrappedObjectAction;
비슷한 문법을 쓰는 함수 포인터(function pointers)도 선언할 수 있어요. 함수 포인터는 delegate 형식을 인스턴스화하고 가상 Invoke 메서드를 호출하는 대신 calli 명령을 사용해요.
dynamic 형식
dynamic 형식은 변수와 그 멤버에 대한 참조가 컴파일 타임 형식 검사를 우회한다는 뜻이에요. 대신 이런 연산들은 런타임에 해석돼요. dynamic 형식은 Office 자동화 API 같은 COM API, IronPython 라이브러리 같은 동적 API, 그리고 HTML DOM(문서 객체 모델)에 접근할 때 간단하게 만들어 줘요.
dynamic 형식은 대부분의 상황에서 object 형식처럼 동작해요. 특히 null이 아닌 모든 식은 dynamic 형식으로 변환할 수 있어요. dynamic과 object의 차이는, dynamic 형식의 식을 포함한 연산을 컴파일러가 해석하거나 형식 검사하지 않는다는 점이에요. 컴파일러는 연산에 관한 정보를 묶어서 보관하고, 그 정보를 나중에 런타임에 연산을 평가할 때 사용해요. 이 과정에서 dynamic 형식의 변수는 object 형식의 변수로 컴파일돼요. 그래서 dynamic 형식은 컴파일 타임에만 존재하고 런타임에는 존재하지 않아요.
아래 예시는 dynamic 형식 변수와 object 형식 변수의 차이를 보여줘요. 컴파일 타임에 각 변수의 형식을 확인하고 싶다면, WriteLine 문의 dyn이나 obj 위에 마우스 포인터를 올려보세요. IntelliSense가 제공되는 편집기에 다음 코드를 복사해 보면, IntelliSense는 dyn에 대해 dynamic을, obj에 대해 object를 보여줘요.
[!code-csharpcsrefKeywordsTypes#21]
xref:System.Console.WriteLine* 문은 dyn과 obj의 런타임 형식을 출력해요. 이 시점에서 둘 다 같은 형식, 그러니까 정수형이 돼요. 다음 출력이 만들어져요.
System.Int32
System.Int32
dyn과 obj의 차이를 컴파일 타임에서 직접 보고 싶다면, 앞선 예시의 선언부와 WriteLine 문 사이에 아래 두 줄을 추가해 보세요.
dyn = dyn + 3;
obj = obj + 3;
식 obj + 3에서 정수와 객체를 더하려는 시도에는 컴파일러 오류가 보고돼요. 하지만 dyn + 3에는 오류가 보고되지 않아요. dyn을 담고 있는 식은 컴파일 타임에 검사되지 않거든요. dyn의 형식이 dynamic이기 때문이에요.
다음 예시는 여러 선언에서 dynamic을 사용해요. Main 메서드는 컴파일 타임 형식 검사와 런타임 형식 검사를 대조해서 보여주기도 해요.
[!code-csharpcsrefKeywordsTypes#25]
C# 언어 사양
자세한 내용은 C# 언어 사양의 다음 섹션들을 확인해 보세요.
- §8.2.3 The object type
- §8.2.4 The dynamic type
- §8.2.5 The string type
- §8.2.8 Delegate types
- §6.4.5.6 String literals