Skip to content
zig 版本:0.14.0

字符串

在深入探讨字符串之前,我们首先明确一个基本概念:计算机中的所有数据都以二进制(0 和 1)形式存储。

大家都知道字符串是“Hello, world!”这种格式。我们经常需要在源代码中定义字符串,例如某些提示信息。通常,这种直接嵌入源代码的字符串被称为硬编码字符串。

那么,这些字符串被放在哪里呢?

由于字符串通常是只读的(运行时生成的字符串是另一种情况),它们被存储在二进制程序的只读数据段中。对于相同的字符串字面量,编译器通常只会保留一个副本,这被称为 字符串驻留(String interning)。

字符串定义

在 Zig 中,字符串字面量是一个指向以 null 结尾的 u8 数组的常量单项指针。因此,它可以隐式转换为切片和哨兵指针,对其解引用则会得到数组本身。

例如:

zig
const print = @import("std").debug.print;
pub fn main() void {
    const foo = "banana";
    print("{}\n", .{@TypeOf(foo)});
}

例如,foo 的类型将显示为 *const [6:0]u8,这表明它实际上是一个指针。

值得注意的是,Zig 默认将字符串字面量视为 UTF-8 编码。由于 Zig 源文件本身就是 UTF-8 编码,任何非 ASCII 字节都会被视为 UTF-8 字符。编译器不会修改这些字节,因此如果需要将非 UTF-8 字节放入字符串中,可以使用 \xNN 转义序列。

Unicode 码点字面量类型是 comptime_int。所有转义字符都可以在字符串字面量和 Unicode 码点字面量中使用。

对包含非 ASCII 字节的字符串进行索引会返回单个字节,无论是否为有效的 UTF-8。

为了方便处理 UTF-8 和 Unicode,Zig 的标准库 std.unicode 提供了相关的函数。

关于字符串有一个示例:

zig
const print = @import("std").debug.print;
const mem = @import("std").mem; // 用于比较字节

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")}); // true
    const invalid_utf8 = "\xff\xfe"; // 非UTF-8 字符串可以使用\xNN.
    print("0x{x}\n", .{invalid_utf8[1]}); // 索引它们会返回独立的字节
    print("0x{x}\n", .{"💯"[1]});
}

转义字符

转义字符含义
\n换行
\r回车
\t制表符 Tab
\\反斜杠 \
\'单引号
\"双引号
\xNN十六进制八位字节值,2 位
\u{NNNNNN}十六进制 Unicode 码点 UTF-8 编码,1 位或者多位

多行字符串

要定义多行字符串,可以使用 \\。多行字符串字面量不会进行转义,并且最后一行行尾的换行符不会包含在字符串中。示例如下:

INFO

字符串字面量中不能直接包含<Tab>字符(Zig 语言规范不允许在源代码中使用<Tab>)。但可以使用\t转义序列或@embedFile内建函数来实现类似的功能。 参考:[enum-backed address spaces](https://github.com/ziglang/zig-spec/issues/38]

zig
const print = @import("std").debug.print;

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

常见错误

下面列举了几个使用字符串时容易遇到的错误。

编译错误 *const [*:0]u8

新手常会遇到以下代码导致的编译错误:

zig
const std = @import("std");

pub fn main() void {
    funnyPrint("banana");
}

fn funnyPrint(msg: []u8) void {
    std.debug.print("*farts*, {s}", .{msg});
}

这段代码看起来很正常,但它无法通过编译。

sh
./example.zig:4:15: error: expected type '[]u8',
    found '*const [6:0]u8' funnyPrint("banana");

新手可能会对此感到困惑:一个旨在打印所有字符串的函数,难道不应该使用切片吗?为什么会报错?

回顾字符串的定义:硬编码于源代码中的字符串是只读的常量。因此,需要使用 []const u8 类型来接收。

这里涉及到一个自动转换的规则:指向数组的指针可以隐式转换为切片。然而,常量性是单向的,这意味着可以将可变指针/切片传递给需要常量指针/切片的函数,但反之则不行。

再解答另一个问题:为什么字符串是 const 呢?

这与字符串驻留有关。在编译时,重复的字符串字面量会被去重。因此,如果允许修改其中一个实例,将会影响所有引用该字符串的地方,这显然是不合理的。这种设计决策是明智且安全的。

试图让字符串可变

有些新手可能会尝试通过将字符串字面量赋值给 var 变量来使其“可变”,例如:

zig
var msg = "banana";

这样定义的 msg 类型是 *const [6:0]u8。这意味着 msg 是一个指向 const [6:0]u8 类型的指针。可变的是指针本身(即 msg 可以指向其他字符串),但它所指向的常量哨兵数组的内容是不可变的。

定义字符串的多种方式

以下三种均是定义字符串的方式:

zig
const message_1 = "hello";
const message_2 = [_]u8{ 'h', 'e', 'l', 'l', 'o' };
const message_3: []const u8 = &.{ 'h', 'e', 'l', 'l', 'o' };

第一种是最常用的方式,编译器会自动推断其类型。

第二种是手动定义一个常量 u8 数组,并由编译器推断其长度。

第三种是手动定义一个常量 u8 数组并获取其指针,编译器会将其自动转换为常量 u8 切片。