문자열 리터럴과 유니코드 코드 포인트 리터럴

문자열 리터럴과 유니코드 코드 포인트 리터럴

Zig의 문자열은 단순한 &str 같은 개념이 아니라, 널 종료 바이트 배열을 가리키는 포인터라는 타입 정보까지 담고 있어요. 이번 섹션에서는 문자열 리터럴과 유니코드 코드 포인트 리터럴이 어떤 타입을 갖는지, 그리고 바이트를 어떻게 다루는지 하나씩 살펴볼게요. 유니코드 문자를 다룰 때 자주 헷갈리는 부분이라 천천히 따라와 주세요.

출처: Zig Documentation

본문

문자열 리터럴은 널 종료 바이트 배열을 가리키는 상수 단일 항목 포인터(constant single-item Pointer)예요. 문자열 리터럴의 타입은 길이널 종료(null-terminated)라는 사실을 둘 다 담고 있어요. 그래서 문자열 리터럴은 SliceNull-Terminated Pointer 양쪽으로 강제 형변환(coerced)될 수 있어요. 그리고 문자열 리터럴을 역참조(dereference)하면 Array로 변환됩니다.

Zig 소스 코드는 UTF-8 인코딩이기 때문에, 소스 코드의 문자열 리터럴 안에 들어 있는 non-ASCII 바이트들은 그 UTF-8 의미를 그대로 Zig 프로그램의 문자열 내용으로 가져와요. 컴파일러가 그 바이트를 수정하지 않아요. 그래서 \xNN 표기법을 쓰면 non-UTF-8 바이트도 문자열 리터럴 안에 넣을 수 있어요.

non-ASCII 바이트를 포함한 문자열을 인덱싱하면, 그 바이트가 유효한 UTF-8이든 아니든 개별 바이트 하나가 반환됩니다.

유니코드 코드 포인트 리터럴의 타입은 Integer Literals와 같은 comptime_int예요. 그리고 모든 Escape Sequence는 문자열 리터럴과 유니코드 코드 포인트 리터럴 양쪽에서 유효합니다.

아래 예시로 문자열 바이트가 실제로 어떻게 보이는지 확인해 볼게요. 여기서는 @TypeOf로 타입을, .len으로 길이를, 인덱싱으로 특정 바이트를 찍어 봐요.

const print = @import("std").debug.print;
const mem = @import("std").mem; // will be used to compare bytes

pub fn main() void {
    const bytes = "hello";
    print("{}\n", .{@TypeOf(bytes)}); // *const [5:0]u8
    print("{d}\n", .{bytes.len}); // 5
    print("{c}\n", .{bytes[1]}); // 'e'
    print("{d}\n", .{bytes[5]}); // 0
    print("{}\n", .{'e' == '\x65'}); // true
    print("{d}\n", .{'\u{1f4a9}'}); // 128169
    print("{d}\n", .{'💯'}); // 128175
    print("{u}\n", .{'⚡'});
    print("{}\n", .{mem.eql(u8, "hello", "h\x65llo")}); // true
    print("{}\n", .{mem.eql(u8, "💯", "\xf0\x9f\x92\xaf")}); // also true
    const invalid_utf8 = "\xff\xfe"; // non-UTF-8 strings are possible with \xNN notation.
    print("0x{x}\n", .{invalid_utf8[1]}); // indexing them returns individual bytes...
    print("0x{x}\n", .{"💯"[1]}); // ...as does indexing part-way through non-ASCII characters
}

실행해 보면 타입은 *const [5:0]u8(길이 5, 널 종료)이라는 사실이 드러나요. '\u{1f4a9}''💯'는 같은 코드 포인트라 같은 숫자 128169·128175로 나오고, mem.eql로 비교했을 때도 "hello""h\x65llo"와 같아요. 💯 역시 \xf0\x9f\x92\xaf 4바이트와 정확히 같죠. 그리고 invalid_utf8처럼 non-UTF-8 바이트를 넣어도 인덱싱하면 0xfe 같은 개별 바이트 그대로 돌아옵니다.

$ zig build-exe string_literals.zig
$ ./string_literals
*const [5:0]u8
5
e
0
true
128169
128175
⚡
true
true
0xfe
0x9f

Escape Sequences

문자열 리터럴과 코드 포인트 리터럴에서 쓸 수 있는 escape sequence는 다음과 같아요.

Escape Sequence Name
\n Newline
\r Carriage Return
\t Tab
\\ Backslash
\' Single Quote
\" Double Quote
\xNN hexadecimal 8-bit byte value (2 digits)
\u{NNNNNN} hexadecimal Unicode scalar value UTF-8 encoded (1 or more digits)

참고로 유효한 Unicode scalar value의 최댓값은 0x10ffff예요.

Multiline String Literals

여러 줄에 걸친 문자열 리터럴은 escape가 없고 여러 줄에 걸칠 수 있어요. multiline 문자열 리터럴을 시작하려면 \\ 토큰을 사용해요. 주석처럼, 문자열 리터럴은 그 줄의 끝까지 이어져요. 줄의 끝(개행)은 문자열 리터럴에 포함되지 않아요. 다만 다음 줄이 \\로 시작하면 새줄(newline)이 추가되고 문자열 리터럴이 이어집니다.

const hello_world_in_c =
    \\#include <stdio.h>
    \\
    \\int main(int argc, char **argv) {
    \\    printf("hello world\n");
    \\    return 0;
    \\}
;

더 알아보기