[공유] <Rust> 문자열 처리
https://www.rust-lang.org/ko-KR/faq.html#how-to-convert-string-or-vec-to-slice
String의 각 문자를 O(1), 즉 상수 시간에 접근하려면 어떻게 해야 하나요?
불가능합니다. 적어도 “문자”가 무슨 의미인지 제대로 이해하고 있지 않거나, 원하는 문자의 인덱스를 찾으려 문자열을 전처리하지 않는다면 말이지요.
Rust 문자열은 UTF-8로 인코딩되어 있습니다. 보기에 하나의 문자는 UTF-8에서는 ASCII 문자열는 달리 꼭 한 바이트인 건 아닙니다. 각 바이트는 “코드 단위”라고 불립니다(UTF-16에서는 코드 단위가 2바이트이고 UTF-32에서는 4바이트이지요). “코드포인트”는 하나 이상의 코드 단위로 구성되어 있고, 문자를 가장 가까이 근사한다고 할 수 있는 “자소(grapheme) 클러스터”는 여러 개의 코드포인트로 구성되어 있습니다.
따라서 UTF-8 문자열에서 바이트를 인덱싱할 수 있다 하더라도 상수 시간에 i번째 코드포인트나 자소 클러스터를 얻어낼 수는 없습니다. 하지만 원하는 코드포인트나 자소 클러스터가 어느 바이트에서 시작하는지 안다면 그건 상수 시간에 접근할 수 있습니다. str::find()나 정규식 검색 결과는 바이트 인덱스를 반환하므로 이 방법으로 접근하는 게 가능합니다.
왜 문자열이 기본적으로 UTF-8인가요?
str 타입이 UTF-8인 것은 현실에서, 특히 엔디안이 정해져 있지 않은 네트워크 전송에서 이 인코딩이 널리 쓰이기 때문이고, I/O를 할 때 어느 방향에서도 코드포인트를 다시 변환할 필요가 없는 것이 최선이라고 생각하기 때문입니다.
물론 이는 문자열 안의 특정 유니코드 코드포인트의 위치를 찾는데 O(n) 연산이 필요하다는 뜻이긴 합니다. 이미 시작하는 바이트 인덱스를 알고 있을 경우에는 예상대로 O(1) 시간이 걸리겠지만요. 어떻게 보면 바람직하지 않을 수도 있지만, 어떻게 보면 이 문제 자체가 트레이드오프로 가득 차 있기에 다음 중요한 점들을 지적할 필요가 있겠습니다:
str에서 ASCII 영역의 코드포인트를 훑는 건 바이트 단위로 안전하게 할 수 있습니다. .as_bytes()를 쓸 경우 u8을 얻는 건 O(1) 연산이며 이 값은 ASCII 범위의 char로 변환하거나 비교할 수 있습니다. 그러니까 이를테면 '\n'로 줄 바꿈을 찾는다면 바이트 단위로 검색해도 됩니다. UTF-8은 원래부터 이렇게 설계되었거든요.
대부분의 “문자 기반” 텍스트 연산들은 “ASCII 범위의 코드포인트 한정” 같이 매우 제약된 언어 가정이 있어야만 동작합니다. ASCII 범위를 벗어나면 언어학적인 단위들(글리프, 낱말, 문단)의 경계를 찾기 위해 (상수 시간이 아닌) 복잡한 알고리즘을 써야 하기 마련입니다. 저희는 “솔직한”, 언어학적으로 올바르며 유니코드에서 인증한 알고리즘을 권장합니다.
char 타입은 UTF-32입니다. 한 번에 한 코드포인트를 들여다 보는 알고리즘이 정말로 필요하다고 생각한다면 type wstr = [char]을 정의하여 str로부터 한번에 읽어들인 뒤 wstr에서 연산을 하면 됩니다. 다르게 말하면, 언어가 “기본적으로 UTF-32로 디코딩하지 않는다”고 해서 UTF-32로 디코딩하거나 다시 인코딩하는 것 자체가 불가능한 건 아니라는 말입니다.
왜 UTF-8이 UTF-16이나 UTF-32보다 보통 더 선호되는지 자세한 설명을 원한다면 UTF-8 Everywhere manifesto를 읽어 보시길 바랍니다.