与 Rust 勾心斗角 · 字符串

garfileo
English

由三个字符构成的字符串

OFF

是「Object File Format」的简写。在 Rust 语言里,字符串对应的类型是什么呢?

&str

若用 C 语言,可使用 char * 类型,例如

char *s = "OFF";

Rust 语言有类似的类型 &str,例如

let s: &str = "OFF";

或省略 s 的类型声明

let s = "OFF";

rustc 认为双引号包围的符号串便是 str 类型的引用形式 &str 的字面量,因而能够推断出上例中 s 的类型。

C 语言能够通过指针或下标形式遍历字符串,例如

char *s = "OFF";
for (char *p = s; *p != '\0'; p++) {
        printf("%c\n", *p);
}

size_t n = strlen(s);
for (size_t i = 0; i < n; i++) {
        printf("%c\n", s[i]);
}

Rust 语言能够通过下标形式遍历字符串,但过程有些曲折,例如

let s = "OFF";
let t = s.as_bytes();
for i in 0 .. s.len() {
    let a: u8 = t[i];
    let b: char = a as char;
    println!("{}", b);
}

首先需要将字符串转化为字节数组,然后在遍历数组的过程中将字节数据转换为字符类型。倘若使用字符串切片引用的方法,代码会优雅一些,例如

let s = "OFF";
for i in 0 .. s.len() {
    println!("{}", &s[i .. i + 1]);
}

不过,Rust 对字符串给出了简洁的语法糖,例如

let s = "OFF";
for c in s.chars() {
    println!("{}", c);
}

字符串实例方法 chars 返回的是迭代器。使用字符串的 chars 方法的好处是便于遍历 UTF-8 编码的字符串,例如

let s = "OFF 格式";
for c in s.chars() {
    println!("{}", c);
}

倘若在遍历每个字符时希望能够获得字符的下标,则可使用 char_indices 方法,例如

let hello = "你好,Rust!";
for x in hello.char_indices() {
    let (a, b) = x;
    println!("{}, {}", a, b);
}

let hello = "你好,Rust!";
for x in hello.char_indices() {
    println!("{}, {}", x.1, x.2);
}

输出结果为

0, 你
3, 好
6, ,
9, R
10, u
11, s
12, t
13, !

char_indices 返回的迭代器产生的结果是元组(Turple)。上述代码展示了元组的基本用法。

字符串比较

对于一个字符串,如何确定它的值是否为 "OFF" 呢?只需写一个能够比较两个字符串是否相等的函数即可解决该问题。

fn str_eq(a: &str, b: &str) -> bool {
    let a_n = a.len();
    let b_n = b.len();
    if a_n != b_n {
        return false;
    } else {
        let a_bytes = a.as_bytes();
        let b_bytes = b.as_bytes();
        for i in 0 .. a_n {
            let a_i = a_bytes[i];
            let b_i = b_bytes[i];
            if a_i != b_i {
                return false;
            }
        }
    }
    return true;
}

我的 str_eq 写得应该是有些丑陋,不过没关系,本意就是想证明我有多么不会 Rust。

下面测试 str_eq 能否正确工作:

let s = "OFF";
println!("{}", str_eq(s, "OFF"));
println!("{}", str_eq(s, "off"));
println!("{}", str_eq(s, "O F F"));

输出

true
false
false

即使 str_eq 写得丑陋也没关系,在实际的代码里,我并不会使用它,因为 str 类型实现了一个叫作 Eq 的 Trait,可直接用 eq 函数进行字符串比较。例如

let s = "OFF";
println!("{}", s.eq("OFF"));

文本 -> 数字

有一个字符串,表达一个小数,例如

let s = "0.618";

如何将其中的数字解析为 f64 类型的值呢?

写一个将小数的字面值转化为小数的函数并不是很难,所以就作为无聊时打发时间的练习题吧!在实际的项目里,通常可以使用 str 的实例方法 parse 方法解决该问题。例如

let a: f64 = "0.618".parse().unwrap();

str 的实例方法 parse 返回值的类型为 Result,通常情况下需要基于模式匹配对其进行解构处理方能获得所需的值,例如:

let a: f64 = match "0.618".parse() {
    Ok(v) => v,
    Err(e) => panic!("Error: {}", e)
};

上述针对 Result 类型的值的解构过程较为普遍,因此 Result 类型将上述过程定义为实例方法 unwrap

字符串集

有一个字符串,表达三个小数,以空格作为间隔,例如

let s = "0.618 2.718 3.141";

如何将其分割为三个字串,分别表达一个小数?

我知道 str 类型有一个 split_whitespace 方法能够解决这个问题,但是现在为了近距离接触 Rust,我应该为此写一个简单的状态机,而不是用现成的方法。在写这个状态机之前,先确定如何表达字符串的分割结果,亦即如何表示字符串集。不妨以 Vec<(usize, usize)> 类型表示字符串集,即以二元组为元素的向量,每个二元组用于记录待分割字串中一段子字串的起止下标。

下面是一个试验,表明通过 Vec<(usize, usize)> 能够表达字符串的分割结果。

let s = "0.618 2.718 3.141";
let mut slices: Vec<(usize, usize)> = Vec::new();
slices.push((0, 5));
slices.push((6, 11));
slices.push((12, 17));
println!("({0}, {1}, {2})",
         &s[slices[0].0..slices[0].1],
         &s[slices[1].0..slices[1].1],
         &s[slices[2].0..slices[2].1]);

输出结果为

(0.618, 2.718, 3.141)

上述代码除包含了 Rust 元组的基本用法。,形如 &s[a..b] 的语法称为字符串切片,通过它能够访问字符串中下标 a 到下标 b 之间的这段内容,若以数学区间的形式表示这段下标区间,可写为 [a, b),即前闭后开区间。

状态

要实现用于分割字符串的状态机,还要考虑如何表达状态。Rust 有枚举类型,可用于表达状态。例如

enum Status {
    Init,
    Space,
    NonSpace
}

为什么有 Init 状态而没有 Stop 状态呢?因为字符串遍历过程自身能够终止,无需显式给出状态机的终止状态。

下面的代码可在遍历字符串的过程中根据字符设定状态:

fn display_status(m: &Status) {
    match m {
        Status::Init => println!("Init"),
        Status::Space => println!("Space"),
        Status::NonSpace => println!("NonSpace")
    }
}

fn main() {
    let s = "0.618 2.718 3.141";
    let mut m = Status::Init;
    display_status(&m);
    for x in s.char_indices() {
        let (_, b) = x;
        if b == ' ' {
            m = Status::Space;
        } else {
            m = Status::NonSpace;
        }
        display_status(&m);
    }
}

结果为

Init
NonSpace
NonSpace
NonSpace
NonSpace
NonSpace
Space
NonSpace
NonSpace
NonSpace
NonSpace
NonSpace
Space
NonSpace
NonSpace
NonSpace
NonSpace
NonSpace

上述代码复习了变量所有权借用、条件表达式以及模式匹配等内容。不过,将条件表达式改为模式匹配,代码通常会简洁一些,例如

for x in s.char_indices() {
    match x.1 {
        ' ' => m = Status::Space,
        _   => m = Status::NonSpace
    }
    display_status(&m);
}

状态机

状态机由一些在状态发生变化时触发的功能构成。下面这个状态机能够基于空格对字符串进行分割:

let s = "0.618 2.718 3.141";
let mut slices: Vec<(usize, usize)> = Vec::new();
let mut m = Status::Init;
for x in s.char_indices() {
    match m {
        Status::Init => {
            match x.1 {
                ' ' => m = Status::Space,
                _   => {
                    m = Status::NonSpace;
                    slices.push((x.0, x.0 + 1));
                }
            }
        },
        Status::Space => {
            match x.1 {
                ' ' => {},
                _   => {
                    m = Status::NonSpace;
                    slices.push((x.0, x.0 + 1));
                }
            }
        },
        Status::NonSpace => {
            match x.1 {
                ' ' => m = Status::Space,
                _   => {
                    slices.last_mut().unwrap().1 += 1;
                }
            }
        },
    }
}
for slice in slices {
    println!("({}, {})", slice.0, slice.1);
}

向量的 last_mut 方法可以返回 Result 类型,其中包含着向量最后一个元素的指针,且通过该指针可以修改该元素的值。倘若仅仅是访问向量的最后一个元素,可使用 last 方法。

小结

enum Status {
    Init,
    Space,
    NonSpace
}

fn split_str(s: &str) -> Vec<(usize, usize)> {
    let mut slices: Vec<(usize, usize)> = Vec::new();
    let mut m = Status::Init;
    for x in s.char_indices() {
        match m {
            Status::Init => {
                match x.1 {
                    ' ' => m = Status::Space,
                    _   => {
                        m = Status::NonSpace;
                        slices.push((x.0, x.0 + 1));
                    }
                }
            },
            Status::Space => {
                match x.1 {
                    ' ' => {},
                    _   => {
                        m = Status::NonSpace;
                        slices.push((x.0, x.0 + 1));
                    }
                }
            },
            Status::NonSpace => {
                match x.1 {
                    ' ' => m = Status::Space,
                    _   => {
                        slices.last_mut().unwrap().1 += 1;
                    }
                }
            },
        }
    }
    return slices;
}

fn main() {
    let s = "0.618 2.718 3.141";
    let slices = split_str(s);
    for slice in slices {
        println!("({}, {})", slice.0, slice.1);
    }
}
阅读 899

5.9k 声望
1.9k 粉丝
0 条评论
5.9k 声望
1.9k 粉丝
宣传栏