문자열 리터럴과 유니코드 코드 포인트 리터럴
문자열 리터럴과 유니코드 코드 포인트 리터럴
Zig의 문자열은 단순한 &str 같은 개념이 아니라, 널 종료 바이트 배열을 가리키는 포인터라는 타입 정보까지 담고 있어요. 이번 섹션에서는 문자열 리터럴과 유니코드 코드 포인트 리터럴이 어떤 타입을 갖는지, 그리고 바이트를 어떻게 다루는지 하나씩 살펴볼게요. 유니코드 문자를 다룰 때 자주 헷갈리는 부분이라 천천히 따라와 주세요.
본문
문자열 리터럴은 널 종료 바이트 배열을 가리키는 상수 단일 항목 포인터(constant single-item Pointer)예요. 문자열 리터럴의 타입은 길이와 널 종료(null-terminated)라는 사실을 둘 다 담고 있어요. 그래서 문자열 리터럴은 Slice와 Null-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;
\\}
;