字符串
在讲解字符串之前,我们需要先说明一个基础的概念:
那就是所有数据在计算机中均是以 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 位或者多位 |
多行字符串
如果要使用多行字符串,可以使用 \\
,多行字符串没有转义,最后一行行尾的换行符号不会包含在字符串中。示例如下:
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 msg = "banana";
这样定义的结果是 msg
的类型是 *const [6:0]u8
,它的意思是一个指向 const [6:0]u8
的指针,也就是说可变的是指针的值,而指针指向的常量哨兵数组是不可变的。
定义字符串的多种方式
以下三种均是定义字符串的方式:
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
切片。