문자열은 바꿀 수 없다
Esc

    문자열은 바꿀 수 없다

    –C#기초

    앞 글에서 글자 하나에 번호가 붙는 걸 봤습니다. 'A' 는 65였습니다.

    이제 글자 여러 개를 다룹니다. 그게 string 입니다.

    string s = "Hello";
    
    Console.WriteLine(s.Length);   // 5
    Console.WriteLine(s[0]);       // H
    Console.WriteLine(s[4]);       // o
    Console.WriteLine(s[5]);       // IndexOutOfRangeException

    글자가 다섯 개인데 s[5] 가 없습니다. 그리고 첫 글자가 s[1] 이 아니라 s[0] 입니다. 이게 컴퓨터의 별난 습관처럼 보이는데, 앞의 이야기로 돌아가면 당연한 결과입니다.

    문자열은 문자를 나란히 놓은 것

    "Hello" 는 char 다섯 개입니다. 그리고 그 다섯 개가 메모리에 빈틈없이 붙어 있습니다.

    앞 글에서 본 대로 char 는 2바이트니, 이 문자열은 10바이트를 통째로 씁니다.

    붙어 있으면 좋은 점이 하나 있습니다. 몇 번째 글자든 계산으로 바로 찾습니다. 시작 지점을 알고 칸 하나의 크기를 알면 나머지는 곱셈과 덧셈입니다.

    찾아갈 자리 = 시작 지점 + 인덱스 × 칸 하나의 크기

    글자를 눌러 보세요.

    여기서 답이 나옵니다. 인덱스 0을 넣으면 시작 지점 그대로입니다.

    0x1000 + 0 × 2 = 0x1000

    첫 글자는 시작 지점에서 0칸 떨어져 있습니다. 그래서 0번입니다. 1번이 아닙니다.

    인덱스는 “몇 번째냐” 가 아니라 “시작에서 몇 칸 떨어졌느냐” 입니다.

    달리기 출발선과 같습니다. 출발선에 서 있는 첫 사람은 0미터 지점에 있습니다. 1미터 지점이 아닙니다.

    마지막은 Length 가 아니라 Length - 1

    글자가 5개면 인덱스는 0 부터 시작하니 마지막은 4 입니다.

    s[5] 는 다섯 칸 떨어진 자리인데, 거기는 문자열이 끝난 뒤입니다. 남의 자리라서 예외를 던집니다.

    for (int i = 0; i < s.Length; i++)    // i <= s.Length 가 아닙니다
    {
        Console.WriteLine(s[i]);
    }

    <= 로 쓰면 마지막 한 바퀴에서 예외가 납니다. 반복문에서 < 를 쓰는 이유가 이것입니다.

    마지막 글자를 꺼낼 때는 s[s.Length - 1] 대신 s[^1] 을 쓸 수 있습니다. ^ 는 “끝에서부터” 라는 뜻이고 ^1 이 마지막 글자입니다. 끝에서 세는 것이라 ^0 은 없습니다.

    전부 훑기만 할 거면 foreach 가 낫습니다. 인덱스를 안 쓰니 한 칸 넘어갈 일도 없습니다.

    foreach (char c in s)
    {
        Console.WriteLine(c);
    }

    읽을 수는 있는데 쓸 수는 없다

    인덱스로 글자를 읽는 것은 됩니다. 그런데 쓰는 것은 안 됩니다.

    string s = "Hello";
    
    char c = s[0];    // 읽는 건 됩니다
    s[0] = 'h';       // 컴파일 에러

    C# 의 string 은 불변(immutable) 입니다. 한 번 만들어지면 그 내용이 끝까지 바뀌지 않습니다.

    왜 이렇게 만들었을까요. 문자열은 여기저기서 돌려쓰기 때문입니다. 누군가 내용을 고칠 수 있으면 그 문자열을 들고 있던 다른 코드도 같이 바뀝니다. 바뀌지 않는다고 보장하면 그런 사고가 없고, 여러 곳에서 같이 읽어도 안전합니다.

    그래서 문자열을 바꾸는 메서드들은 원본을 고치는 게 아니라 바뀐 새 문자열을 돌려줍니다.

    string s = "Hello";
    
    s.ToUpper();              // 아무 일도 안 일어납니다
    Console.WriteLine(s);     // Hello
    
    s = s.ToUpper();          // 돌려받은 것을 다시 담아야 합니다
    Console.WriteLine(s);     // HELLO

    ToUpper · ToLower · Replace · Trim · Substring 전부 같습니다. 돌려받지 않으면 아무 일도 일어나지 않습니다. 처음에 자주 걸리는 자리입니다.

    + 는 새 문자열을 만든다

    불변이라는 성질은 + 에서 비용이 됩니다.

    string s = "Hello";
    s = s + " World";

    기존 문자열이 늘어나는 게 아닙니다. "Hello World" 라는 새 문자열이 따로 만들어지고, s 가 그쪽을 가리키게 됩니다. 원래 "Hello" 는 아무도 안 쓰게 되어 GC 를 기다립니다.

    쓰던 종이에 이어 쓰는 게 아니라 매번 새 종이에 전부 옮겨 적는 것과 같습니다.

    한두 번이면 아무 문제 없습니다. 반복문 안에 들어가면 달라집니다.

    열 번은 차이가 없고, 만 번은 다른 세상입니다. + 는 매번 지금까지 쌓인 것을 전부 새 종이에 옮겨 적기 때문입니다.

    그래서 반복문 안에서 문자열을 쌓을 때는 StringBuilder 를 씁니다.

    StringBuilder 는 바꿀 수 있는 버퍼입니다. 다 쌓은 뒤 ToString() 으로 한 번만 문자열을 만듭니다. using System.Text; 가 필요합니다.

    그래도 몇 번은 그냥 + 를 써도 된다

    한 줄 안에서 여러 번 이어붙이는 것은 괜찮습니다.

    string name = "손" + "오공" + " 님";

    컴파일러가 이런 연속된 + 를 한 번의 합치기로 묶어줍니다. 중간 문자열이 생기지 않습니다.

    문제가 되는 건 반복문처럼 한 번에 한 조각씩 여러 번 나눠 붙일 때입니다. 그때는 컴파일러가 묶을 수가 없습니다.

    읽기 좋게 쓸 거면 문자열 보간이 낫습니다.

    string msg = $"{name} 님의 점수는 {score}점입니다";

    앞에 $ 를 붙이면 중괄호 안에 변수를 그대로 넣을 수 있습니다. + 로 이어붙이는 것보다 읽기 쉽고 속도도 같습니다.

    Length 가 눈에 보이는 글자 수가 아닐 수 있다

    앞 글에서 char 가 UTF-16 한 조각이라 2바이트라고 했습니다. 여기에 한 가지가 딸려 옵니다.

    Console.WriteLine("한글".Length);   // 2  (글자 두 개, 맞습니다)
    Console.WriteLine("👍".Length);     // 2  (글자는 하나인데요?)

    char 는 글자 하나가 아니라 UTF-16 한 조각입니다. 대부분의 글자는 조각 하나에 들어가서 한글도 한자도 문제없습니다. 그런데 이모지 같은 일부 문자는 조각 두 개를 써야 합니다.

    그래서 이모지가 섞인 문자열은 Length 가 눈에 보이는 글자 수와 다르고, 인덱스로 자르면 글자가 반토막 납니다.

    눈에 보이는 글자 단위로 세거나 잘라야 한다면 System.Globalization.StringInfo 를 쓰거나, 최신 C# 이라면 EnumerateRunes() 를 씁니다. 사용자 닉네임 길이 제한처럼 사람이 세는 글자 수가 필요한 자리에서 이 차이가 드러납니다.

    정리

    • 문자열은 문자를 메모리에 나란히 놓은 것이다.
    • 인덱스는 순번이 아니라 시작 지점에서의 거리다. 그래서 0부터 시작한다.
    • 마지막 인덱스는 Length 가 아니라 Length - 1 이다. 반복문에서 < 를 쓰는 이유다.
    • string 은 불변이다. 읽을 수는 있어도 쓸 수는 없다.
    • ToUpper 같은 메서드는 원본을 고치지 않고 새 문자열을 돌려준다. 돌려받아야 한다.
    • + 는 매번 새 문자열을 만든다. 반복문 안에서는 StringBuilder 를 쓴다.
    • 한 줄 안의 연속된 + 는 컴파일러가 묶어주므로 괜찮다.
    • char 는 UTF-16 한 조각이라, 이모지가 섞이면 Length 가 보이는 글자 수와 다르다.