본문 바로가기

Language/C#

C#의 String 객체 + 연산보단 StringBuilder::Append()를 사용하자.


C# 에서 제공되는 String class 의 객체를 사용하면 C처럼 힘들게 char 로 선언을 할 필요가 없다. 알아서 힙(heap)영역에 자동으로 때려 박아 주기 때문이다.

여기서 굉장히 매력적인 것은 + 연산자 오버라이딩을 제공 하기 때문에 C에서 처럼 strcat 같은 함수를 사용하다가 잘못된 포인터 참조 에러나 부적절한 포인터 변환 등 과 같은 에러를 만날 필요가 없다는 것이다. 

C#에서 기본 스트링은 Unicode를 사용하며, 변형되지 않는 시퀀스를 의미한다. 여기서 변형되지 않는다 'immutability'는 일단 string이 힙에 할당되면, 그것의 값은 결코 변하지 않음을 의미한다. 만약 그 값이 변한다면 .NET은 새로운 String 객체를 생성하고, 그 변환 값을 성생된 변수에 할당한다. 이것은 스트링이 여러 가지 면에서 참조형이 아니라 값형(value type)처럼 여겨짐을 의미한다. C#은 System.String 클래스를 나타내는 별칭(string)을 제공한다. 만일 코드에 String을 쓰고 있다면, using System; 이라는 문장이 코드 상단에 있어야만 한다. 그러나 미리 정의도니 string을 쓰면 아무것도 할 필요가 없다.
                                                              -C# Programmer's Reference 中 발췌

무슨소린지 좀 어렵군...ㅡㅡ;;

중요한건 이게 아니다.
String 는 + 연산자 오버라이딩이 되어있기 때문에
String str = "Hello";
String str2 = str + " There";

코딩이 가능하다. (C++도 가능함)

하지만 대부분은 다음과 같은 코드 형식을 사용한다.
String s1 = "Hello";
String s2 = s1 + " There";
String s3 = s2 + " John" ;

이 코드는 정상적으로 작동되는 코드이다. 하지만 IL( IL 이란 : Intermediate Language 라고 하며, 저수준의 언어로 컴파일된후 나타나는 코드이며 IL은 런타임 때 마다(Just-In-Time(JIT))원시 기계어 코드로 컴파일된다. 이 컴파일은 성능 비용의 많은 부분을 줄여서 운영체제와 하드웨어의 최적화를 가능하게 한다.) 코드에서 보면

.locals init (string V_0, string V_1, string V_2)
L_0000: ldstr   "Hello"       //스트링 "Hello"를 로드한다.
IL_0005: stloc.0              // 그 값을 variable V_0으로 넣는다.
IL_0006 ldloc.0              // V_0을 스택에 넣는다.
IL_0007: ldstr   " There"  //스트링 " There"를 로드한다.
// String::Concat() 메소드를 로드한다.
IL_000c: call    string [mscorlib]System.String::concat(string, string)
IL_0011: stloc.1            //리턴값을 V_1로 넣는다.
IL_0012: ldloc.1            // V_1값을 스택에 넣는다.
IL_0013: ldstr   "John "   //스트링 "John " 을 로드한다.
// String::Concat() 메소드를 로드한다.
IL_000c: call    string [mscorlib]System.String::concat(string, string)

3개의 스트링(String)뿐 아니라 String::Concat() 호출에 의해 두 개의 스트링(string)이 더 만들어 진다. 각각의 스트링에 대한 변화는 힙에 할당되어진 새로운 String 객체 때문이다.

한마디로 단순히 String s2 = "There" + s1; 와 같은 문장시 "There" 또한 String객체에 할당되어 힙 영역에 올라간다는 말이다.

작은 프로그램이야 상관 없겠지만 프로그램이 크고 많은 스트링을 묶어야 하는 작업이 있다면 이 오버해드는 절대 무시 할 수 없다.

이 문제를 StringBuilder 객체(System.Text 네임스페이스에 존재)를 사용해서 string의 불변성을 해결할 수 있다. StringBuilder는 문자들의 순서를 변하게 할 수 있고 만약 많은 수의 스트링을 연결할 때에는 string보다 더 효율적이다. 위 코드를 StringBuilder로 다시 나타내면 다음과 같다.

StringBuilder sb = new StringBuilder("Hello");
sb.Append(" There");
sb.Append(" John");

이것을 IL 코드로 컴파일 하면,

.locals init (class [mscorlib]System.Text.stringBuilder V_0)
L_0000: ldstr   "Hello"      
IL_0005: newobj   instance void
                        [mscorlib]System.Text.StringBuilder::.ctor(string)
IL_000a: stloc.0
IL_000b: ldloc.0 
IL_000c: ldstr  " There"

IL_0011: callvirt   instance class [mscorlib]System.StringBuilder
           [mscorlib]System.Text.StringBuilder::Append(string)

IL_0016: pop
IL_0017: ldloc.0
IL_0018: ldstr   " John"
IL_001d: callvirt   instance class [mscorlib]System.StringBuilder
           [mscorlib]System.Text.StringBuilder::Append(string)

이 경우에 단지 3개의 string과 한 개의 StringBuilder 클래스의 인스턴스를 가진다. 또한 StringBuilder::Append() 호출은 새로운 StringBuiler 인스턴스를 생성하지 않는다는게 흥미롭다. 많은 스트링이 연결되고 반복적으로 될때에는 그 성능의 향상은 상당하다.
                                                          -C# Programmer's Reference 참고-