2023 年 06 月 15 日
假设存在一个字符串,其内容为
以下 Rust 程序
@
fn main() {
println!("Hello world!");
}
@
可在终端打印「Hello world!」。
编写一个 Rust 程序,以 @
所在行作为分割符,对该字符串进行分割。
Rust 有多种类型可表达字符串,以下示例给出了最直白的一种:
let text = "潜龙勿用";
上述代码中的变量 text
,其类型是什么呢?可使用 Rust
标准库中的 std::any::type_name
函数打印其类型,例如:
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>());
}
fn main() {
let x = "潜龙勿用";
&x);
print_type_of(}
假设将上述代码保存至 foo.rs 文件,然后使用 rustc 编译该文件:
$ rustc foo.rs
运行编译所得程序 foo:
$ ./foo
&str
程序 foo 的输出结果 &str
便是变量 x
的类型。
上述代码中定义的函数 print_type_of
巧妙地利用了泛型以及类型推导功能,将其所接受的参数的类型「转发」给了
std::any::type_name
函数。需要注意的是,为了避免参数复制而带来的内存和时间消耗,print_type_of
的参数采用了变量的引用形式。关于泛型和引用,后文会对其进行更为全面且深入的介绍,在此不必为之惶恐。
现在已知变量 x
的类型是 &str
,即
str
类型的实例(或数据)的引用。可将 str
类型的实例想象为一组字符构成的序列,而 &str
类型的变量仅记录该序列的首地址以及该序列的长度信息,因此所占用的内存长度通常远小于字符序列本身。
在 Rust 语言中,类型 &str
通常称为字符串切片,原因是基于该类型可引用一个字符串的片段。例如
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>());
}
fn main() {
let x = "Hello world!";
let y = &x[0..5];
&y);
print_type_of(println!("\"{}\" of \"{}\".", y, x);
}
输出结果为
&str
"Hello" of "Hello world!".
亦即 y
是对 x
所引用的字符序列的一个片段的引用。&x[0..5]
表达的是左闭右开区间,即 x
所引用的 str
实例的第 1 个字符(下标为 0)至第 5 个字符(下标为 4)。
Rust 使用泛型容器
Vec<T>
(向量)类型表达动态数组。下面的示例可将三个字符串切片存于
Vec<T>
的实例:
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>());
}
fn main() {
let x = "Hello@world@!";
let v = vec![&x[..5], &x[6..11], &x[12..]];
&v);
print_type_of(println!("[{}, {}, {}]", v[0], v[1], v[2]);
}
输出为
alloc::vec::Vec<&str>
[Hello, world, !]
vec!
是 Rust
语言用于简化向量实例初始化的宏。倘若不使用该宏,则上述向量的构造过程可基于
Vec
类型的 push
方法实现:
let mut v = vec![]; // 初始化空向量
.push(&x[..5]);
v.push(&x[6..11]);
v.push(&x[12..]); v
由于 v
是动态构建的,因此其必须冠以 mut
表示其值可变。此外,上述代码中字符串切片表达式中的[..5]
是
[0..5]
的简写,而 [12..]
是
[12..x.len()]
的简写,x.len()
方法可以获得
str
实例(即字符序列)的长度。
对于本章要解决的问题,上述两节其实已经解决了待实现的程序的输入和输出问题,即
&str
作为输入,Vec<&str>
类型作为输出。
Rust 标准库为 &str
类型提供了 split
方法,该方法正是以 &str
作为输入,且其输出能转化为
Vec<&str>
类型。例如
fn main() {
let x = "abc@def@ghi";
let v: Vec<&str> = x.split("@").collect();
println!("[{}, {}, {}]", v[0], v[1], v[2]);
}
str
类型的 split
方法的返回值是
Split
类型,该类型的 collect
方法可将字符串分割结果转化为 Vec<&str>
类型。
Split
类型实际上是一种迭代器类型——该类型实现了
Iterator
特性(Trait),若不使用它的 collect
方法,使用 for...in
语句也能够获得字符串分割结果,例如
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>());
}
fn main() {
let x = "abc@def@ghi";
let v = x.split("@");
&v);
print_type_of(for i in v {
println!("{}", i);
}
}
输出为
core::str::iter::Split<&str>
abc
def
ghi
关于迭代器类型的技术细节,目前知之甚浅,先行略过,随着 rzeo 项目的逐步推进,在合适的时机,再对其深入探究。
对于本章开始提出的问题,以下程序可作为答案:
fn main() {
// 注意:字符串中出现双引号需要用反斜线予以转义
let x ="以下 Rust 程序
@
fn main() {
println!(\"Hello world!\");
}
@
可在终端打印「Hello world!」。";
let v = x.split("\n@\n");
println!("----");
for i in v {
println!("{}", i);
println!("----");
}
}
其输出为
----
以下 Rust 程序
----
fn main() {
println!("Hello world!");
}
----
可在终端打印「Hello world!」。
----
下面采用 C 语言实现与上一节相同功能的程序,并且尝试模拟 Rust
的迭代器以获得 &str
和 Split
等类型更为深刻的理解。此外,我也想试图向自己证明 C 语言并非许多 Rust
爱好者们所宣扬的那样陈旧甚至糟糕。
C 语言标准库提供了函数
strtok
,可用于字符串分割。例如
#include <stdio.h>
#include <string.h>
int main(void) {
char x[] = "123 @ 456 @ 789";
char *d = " @ ";
char *v = strtok(x, d);
("%s\n", v);
printfwhile (v = strtok(NULL, d)) {
("%s\n", v);
printf}
return 0;
}
或
#include <stdio.h>
#include <string.h>
int main(void) {
char x[] = "123 @ 456 @ 789";
for (char *s = x; ; s = NULL) {
char *v = strtok(s, " @ ");
if (!v) break;
("%s\n", v);
printf}
return 0;
}
输出为
123
456
789
strtok
是不可重入的函数,故而不能用于多线程任务。对于上述示例,strtok
有以下副作用:
x
会被 strtok
修改,即 strtok
使用 '\0'
代替分割符的首字符;strtok
通过一个全局变量记录字符串的剩余内容。在 Rust 语言爱好者看来,strtok
不安全,绝不可容忍。不过,用 C
语言自行实现一个安全的字符串分割函数并不困难。C 标准库提供了
strstr
函数,可用于在字符串中查找目标字符串第一次出现的位置,基于该函数便可实现安全的字符串分割功能,下文给出了一种实现。
首先,需要引入以下 C标准库头文件:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
定义一个结构体类型,用于表示字符串切片,并为该类型定义打印函数:
typedef struct {
char *head;
char *tail;
} StrSlice;
void str_slice_print(StrSlice *slice) {
for (char *p = slice->head; p != slice->tail; p++) {
(*p);
putchar}
}
再定义一个结构体类型,用于记录分割符以及字符串每次分割后剩余内容,并为该类型定义构造函数:
typedef struct {
char *remainder; /* 字符串剩余内容 */
char *delimiter; /* 分割符 */
size_t delimiter_length; /* 分割符长度 */
} StrSplitEnv;
*str_split_env(char *str, char *delimiter) {
StrSplitEnv *env = malloc(sizeof(StrSplitEnv));
StrSplitEnv ->remainder = str;
env->delimiter = delimiter;
env->delimiter_length = strlen(delimiter);
envreturn env;
}
定义字符串分割函数:
*str_split(StrSplitEnv *env) {
StrSlice if (!env->remainder) return NULL;
*slice = malloc(sizeof(StrSlice));
StrSlice ->head = env->remainder;
slice->tail = strstr(env->remainder, env->delimiter);
sliceif (slice->tail) {
->remainder = slice->tail + env->delimiter_length;
env} else {
->remainder = NULL;
env->tail = slice->head + strlen(slice->head);
slice}
return slice;
}
字符串分割函数的测试代码如下:
int main(void) {
char *x = "以下 Rust 程序\n@\n"
"fn main() {\n"
" println!(\"Hello world!\");\n"
"}\n@\n"
"可在终端打印「Hello world!」。";
*env = str_split_env(x, "\n@\n");
StrSplitEnv *v;
StrSlice ("----\n");
printfwhile (v = str_split(env)) {
(v); putchar('\n');
str_slice_print(v);
free("----\n");
printf}
(env);
freereturn 0;
}
Rust 语言以保证内存安全而著称。C 语言可借助带有内存错误检测工具的 C 编译器或辅以 Valgrind 工具保证内存安全。假设将上述 C 程序保存为 foo.c 文件,可使用以下命令编译并运行:
$ gcc -std=c2x -pedantic -g -fsanitize=address -o foo foo.c
$ ./foo
或
$ gcc -std=c2x -pedantic -g -o foo foo.c
$ valgrind --leak-check=full ./foo
若程序运行结束后,未出现错误信息,则通常可视为该程序是内存安全的,之后可重新编译程序,消除调试信息,并开启优化:
$ gcc -std=c2x -pedantic -O2 -o foo foo.c
在对 Rust 语言较为熟悉的情况下,编写的 Rust 代码通常较实现同样功能的 C 代码更为简洁,一方面是因为 Rust 标准库的功能比 C 标准库更为丰富,另一方面 Rust 的语法掩盖了许多细节。对于后者,除了本章语焉不敢甚详的泛型, 特性以及迭代器等元素外,值得一提的是,一个 Rust 新手也许难以想象,为何以下 Rust 代码无法通过编译:
fn main() {
let x = "abc@def@ghi";
let v = x.split("@");
for i in v {
println!("{}", i);
}
for i in v {
println!("{}", i);
}
}
倘若将 Rust 语言视为比 C 甚至比 C++ 更好的语言,那心态上就不要乐观,更好往往意味着更难驾驭。汽车比摩托车更好,在驾照方面,汽车要比摩托车难得多,至少在国内是这样。