字符串
在深入探讨字符串之前,我们首先明确一个基本概念:计算机中的所有数据都以二进制(0 和 1)形式存储。
大家都知道字符串是“Hello, world!”这种格式。我们经常需要在源代码中定义字符串,例如某些提示信息。通常,这种直接嵌入源代码的字符串被称为硬编码字符串。
那么,这些字符串被放在哪里呢?
由于字符串通常是只读的(运行时生成的字符串是另一种情况),它们被存储在二进制程序的只读数据段中。对于相同的字符串字面量,编译器通常只会保留一个副本,这被称为 字符串驻留(String interning)。
字符串定义
在 Zig 中,字符串字面量是一个指向以 null
结尾的 u8
数组的常量单项指针。因此,它可以隐式转换为切片和哨兵指针,对其解引用则会得到数组本身。
例如:
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
提供了相关的函数。
关于字符串有一个示例:
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]
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
新手常会遇到以下代码导致的编译错误:
const std = @import("std");
pub fn main() void {
funnyPrint("banana");
}
fn funnyPrint(msg: []u8) void {
std.debug.print("*farts*, {s}", .{msg});
}
这段代码看起来很正常,但它无法通过编译。
./example.zig:4:15: error: expected type '[]u8',
found '*const [6:0]u8' funnyPrint("banana");
新手可能会对此感到困惑:一个旨在打印所有字符串的函数,难道不应该使用切片吗?为什么会报错?
回顾字符串的定义:硬编码于源代码中的字符串是只读的常量。因此,需要使用 []const u8
类型来接收。
这里涉及到一个自动转换的规则:指向数组的指针可以隐式转换为切片。然而,常量性是单向的,这意味着可以将可变指针/切片传递给需要常量指针/切片的函数,但反之则不行。
再解答另一个问题:为什么字符串是 const
呢?
这与字符串驻留有关。在编译时,重复的字符串字面量会被去重。因此,如果允许修改其中一个实例,将会影响所有引用该字符串的地方,这显然是不合理的。这种设计决策是明智且安全的。
试图让字符串可变
有些新手可能会尝试通过将字符串字面量赋值给 var
变量来使其“可变”,例如:
var msg = "banana";
这样定义的 msg
类型是 *const [6:0]u8
。这意味着 msg
是一个指向 const [6:0]u8
类型的指针。可变的是指针本身(即 msg
可以指向其他字符串),但它所指向的常量哨兵数组的内容是不可变的。
定义字符串的多种方式
以下三种均是定义字符串的方式:
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
切片。