안녕하세요 마수리입니다!
오늘은 StringBuilder
시리즈의 마지막 시간으로 StringBuilder
의 .ToString()
은 어떻게 최종적으로 1개의 string
객체를 뽑아내는지 살펴볼 예정입니다.
사실 이전 시리즈를 계속 보셨다면 쉽게 유추가 되실 것으로 판단됩니다. 왜냐하면 제가 계속 이야기를 드렸기 때문이죠 😁
시리즈의 마지막 글이기 때문에 간단하게 StringBuilder
에 대해서 정리해보고 들어갈까요?
StringBuilder
는 버퍼를 갖는다.StringBuilder
에 계속 문자열을 추가하다보면 언젠가 버퍼가 가득 차는 상황이 온다.StringBuilder
는 새로운 버퍼를 생성한다.Linked List
형태로 이어서 데이터를 저장한다..ToString()
함수가 호출되면 Linked List
로 이어진 모든 버퍼를 방문하면서 최종 1개의 string
객체를 만든다.오늘은 이 과정에서 6번 과정에 대해서 이야기 해볼까합니다.
과연.... 정말로... 이 글에서 이야기 하는대로 StringBuilder
는 동작하고 있을까요? 우리는 직접! .ToString()
함수를 들여다 보기로 했습니다..
// 실제 코드는 더 복잡합니다.
public override String ToString() {
// 총 버퍼의 크기만큼 메모리를 미리 할당
string ret = string.FastAllocateString(Length);
// 현재 버퍼부터 방문 시작
StringBuilder chunk = this;
unsafe {
fixed (char* destinationPtr = ret)
{
do
{
if (chunk.m_ChunkLength > 0)
{
// Check that we will not overrun our boundaries.
if ((uint)(chunkLength + chunkOffset) <= ret.Length && (uint)chunkLength <= (uint)sourceArray.Length)
{
fixed (char* sourcePtr = sourceArray)
{
// chunkOffset, sourcePtr, chunkLength을 이용해서 뒤에서부터 버퍼의 문자열을 새로운 문자열에 복사한다.
string.wstrcpy(destinationPtr + chunkOffset, sourcePtr, chunkLength);
}
}
else
{
throw new ArgumentOutOfRangeException("chunkLength", Environment.GetResourceString("ArgumentOutOfRange_Index"));
}
}
// 현재 버퍼부터 역순으로 모든 버퍼를 방문한다.
chunk = chunk.m_ChunkPrevious;
} while (chunk != null);
}
}
// 최종적으로 모두 복사된 1개의 String을 반환
return ret;
}
제가 가장 처음 이 코드를 보면서 이해가 되지 않았던 포인트는 반복문을 돌면서 chunk = chunk.m_ChunkPrevious;
이 구문을 통해 역순으로 버퍼를 방문한다는 것이였습니다. 역순으로 방문하면.. 그걸 다 합쳤을 때 최종적으로 만들어지는 문자는 이상하게 나오지 않을까..?? 예를들어 오늘 날씨는 아주 맑네요.
라는 문자를 쓰다 한번 버퍼가 갱신되어 오늘 날씨는 아주 맑
과 네요.
2개의 버퍼가 생겼을 때 역순으로 방문해서 복사를 시작한다면 2번째 버퍼의 내용이 먼저 오므로 최종적으로는 네요. 오늘 날씨는 아주 맑
가 되지 않을까요..?? 저만 그렇게 생각하는건가요?? 🤣
그런데.. 그게 아니였습니다.
그게 아닌 이유는 복사 함수에 있었는데요. 제가 잘 모르는 함수라 자세히 보기전까지 어떻게 동작하는지 잘 몰랐었는데요 자세히 보니 문자를 앞에서부터 복사하는 것이 아니였습니다.
// dmem : destination memory를 의미하며 이 위치에서부터 복사된 문자열이 쓰여집니다. (여기가 포인트!! 🌟)
// smem : smem은 source memory를 의미하며 이 포인터가 가리키는 메모리 위치에서 데이터를 읽어와 dmem이 가리키는 위치로 복사합니다.
// charCount : 몇 개의 문자를 복사할지를 지정합니다.
internal static unsafe void wstrcpy(char *dmem, char *smem, int charCount)
자! 이제 실제 호출 되는 함수를 보도록 하겠습니다.
// destinationPtr의 시작 주소부터 복사하는 것이 아니라 chunkOffset만큼 이동한 위치에서 복사하네?
string.wstrcpy(destinationPtr + chunkOffset, sourcePtr, chunkLength);
destinationPtr
에서 chunkOffset
만큼 이동한 자리부터 버퍼를 버퍼의 길이만큼 복사합니다. 결론적으로 최종적으로 리턴할 문자열의 앞에서부터 복사하는 것이 아니라 현재 방문하고 있는 버퍼가 모든 버퍼를 다 합했을 때 앞에서부터 얼마나 떨어져 있는지 알고 거기서부터 현재 버퍼의 내용을 쓰는 것입니다.
한번 번호를 매겨서 예를 들어 설명해보겠습니다.
[안][녕][하][세][요]
라는 문자열이 2개의 버퍼로 저장되어 [안][녕][하]
, [세][요]
로 저장 됐고 가정해 보겠습니다..ToString()
에선 총 버퍼의 크기인 5의 크기로 최종적으로 모든 버퍼의 문자열이 저장될string
메모리를 미리 잡는다..ToString()
을 호출하면 버퍼를 역순으로 방문하기 때문에 [세][요]
, [안][녕][하]
순으로 버퍼를 방문한다.string.wstrcpy()
를 호출하면 다음과 같이 복사된다.chunkOffset
만큼 이동한 자리부터 현재 버퍼의 내용을 복사한다.[][][][세][요]
[안][녕][하][세][요]
.[안][녕][하][세][요]
를 반환한다.혹시 이해가 잘 되시나요?
자..!! 드디어 최종 마무리의 시간이 다가왔습니다. 여러분들이 StringBuilder
라는 단어를 들었을 때 지금 저와 같은 자신감이 있는 상태가 되셨길 희망합니다. 혹시 제 글에서 이해가 잘 안가시는 부분이 있으시면 언제든지 댓글 주셔도 됩니다. 😍
그리고 .NET4.0
이전 버전과 이후의 StringBuilder
의 내용이 아주 많이 다른걸로 알고 있습니다. .NET4.0
이 전 버전에선 버퍼를 생성할 때마다 이전 버퍼에 있던 모든 문자를 새로운 버퍼에 복사하는 과정이 있었다고 합니다. 저희가 살펴보았던 .NET4.0
이후 버전에선 새로운 버퍼가 생겼을 때 어떻게 하나요? 따로 복사하지 않고 이전 버퍼의 내용을 그냥 Linked List
로 가지고 있고 마지막에 .ToString()
을 호출 했을 때 이 데이터를 모두 방문해서 리턴하는 것으로 바뀌었습니다. 불필요한 복사 연산을 많이 줄인 노력이 보이시나요?
StringBuilder
에 대해서 더 궁금하신 점이 있으신 분들은 .NET4.0
이전 버전과 이후 버전을 비교해보시는 것도 재미있을 것 같네요. 그럼 이만 이 시리즈를 마무리 하도록 하겠습니다.
감사합니다. 🤞