【rCore OS 开源操作系统】Rust 语法详解: Strings
前言
这次涉及到的题目相对来说比较有深度,涉及到 Rust 新手们容易困惑的点。
这一次在直接开始做题之前,先来学习下字符串相关的知识。
Rust 的字符串
Rust中“字符串”这个概念涉及多种类型,这里介绍题目中涉及到的两种的字符串类型:&str(字符串切片,slice)和String(可变字符串)。
String 可变字符串
存储在堆中,并且是可变的——是和其他大部分主流编程语言类似的概念。
示例:
let mut s: String = String::from("hello");
s.push_str(", world"); // “hello, world”,是的这里就是可变的
&str 字符串切片
那么,什么是切片呢?
既然 String 被我们称为可变字符串,这里也就暗示字符串切片是不可变的(immutable)。
同时,它是存储在栈中的。
来看看如何产生字符串切片:
// 字面量是一种切片
let s1 = "Hello, World";
// 完整的写法其实是 let s1: &str = "Hello, World"; // 也可以从可变字符串中切割
let string:String = String::from("wow,amazing");
let s2 = string[0..3]; // 左开右闭,正好就是 “wow”
可以从切片中得到可变字符串:
let string1 = String::from("hello");
let string2 = "hello".to_string();
这里也不很啰嗦,很直观地就认识了两种字符串在形式上的区别。
那再来看看本质。
所有权、引用与借用
要搞懂字符串的本质,还得回到所有权、引用和借用三个概念上。
所有权
这里是一段官话:
所有权是 Rust 中的核心概念之一,它决定了数据的生命周期和内存管理方式。每个值在 Rust 中都有一个拥有它的变量,称为“所有者”。
所有权有三个特性:
- 唯一所有者:一个值在同一时间内只能有一个所有者。
- 所有权转移:当把一个所有者的值传递给另一个变量时,所有权会被转移。
- 自动释放内存:当一个值的所有者离开作用域时,该值所占用的内存会被自动释放
第一次看估计不太理解,但是没关系,直接来对照代码再看一次:
这里就当作是一个代码拟人小故事来看
// 现在变量 s 是 String::from("hello") 的所有者
let s = String::from("hello");
// 直接赋值,现在 String::from("hello") 到了 t 的手中,t 是新的所有者,而 s 不再是了,s 会被释放掉!
// 这里就体现了三个特性,“唯一所有者”,“所有权转移”和“自动释放内存”。
let t = s;
// 这里会编译失败,因为 s 已经不再有效println!("{}", s); // 报错
所有权的作用是什么?
这个问题有有更广而全面的回答,此处只简单说明最核心的一点
内存安全,所有权机制的存在避免了许多常见的内存错误,如悬空引用、双重释放等问题——因为一个引用永远都是有值的,并且同一时刻只有一个指针能够操作值,而且在值指针失效时会自动销毁。
引用和借用
Rust 中的引用是(直接)基于指针实现的,可以看作是别名。
而 JavaScript 等语言并不是直接基于指针实现的,而是依靠对象的引用实现的。
当然了,对象的引用的本质其实也还是可以基于指针——所以这里提到了“直接”一词。
基于指针是什么意思呢?
对于 String 类型,它的定义差不多是这样:
struct MyString {ptr: *const u8, // 一个指针len: usize, // 字符串长度, 是当前字符串中字符的数量cap: usize, // 字符串的最大容量,指最多可以容纳多少长度
}
那别名是什么意思呢?
这里我们也拟人地来解释下:
// a 和 b 是两个人,只是名字相同。
let a = 1;
let b = 1;// s 和 r 是同一个人,只是有两个名字
let mut s = String::from("hello");
let r = &s; // r 是 s 的引用
当然,这里的写法中, r 是不可以去修改值的。
如果需要修改,那么要这样写:
let r = &mut s; // r 是 s 的引用
那什么是借用呢?
这里的官话就是:
借用是值,允许使用一个变量的值,而不改变所有权。
其实,借用就是 Rust 的引用的一个特性。
来看下述代码(魔改自《Rust 高级程序设计》):
fn main() {let s1 = String::from("hello");// 传入一个 s1 的引用,不会改变所有权。// 而 s1 的所有权没有转移,就意味着 s1 不会被销毁,在后面可以接着用。let len = calculate_length(&s1); // 使用 s1 不报错呢println!("The length of '{}' is {}.", s1, len);
}fn calculate_length(s: &String) -> usize {s.len()
}
练习题
Strings1
题目
// strings1.rs
//
// Make me compile without changing the function signature!
//
// Execute `rustlings hint strings1` or use the `hint` watch subcommand for a
// hint.// I AM NOT DONEfn main() {let answer = current_favorite_color();println!("My current favorite color is {}", answer);
}fn current_favorite_color() -> String {"blue"
}
题解
有了上面的解释后,这里做起来就简单一些了。
这里的考点也就是区分可变字符串 String 和字符串切片 &str:
// strings1.rs
//
// Make me compile without changing the function signature!
//
// Execute `rustlings hint strings1` or use the `hint` watch subcommand for a
// hint.fn main() {let answer = current_favorite_color();println!("My current favorite color is {}", answer);
}fn current_favorite_color() -> String {// 两种写法都可以// "blue".to_string()String::from("blue")
}
Strings2
题目
// strings2.rs
//
// Make me compile without changing the function signature!
//
// Execute `rustlings hint strings2` or use the `hint` watch subcommand for a
// hint.fn main() {let word = String::from("green"); // Try not changing this line :)if is_a_color_word(&word) {println!("That is a color word I know!");} else {println!("That is not a color word I know.");}
}fn is_a_color_word(attempt: &String) -> bool {attempt == "green" || attempt == "blue" || attempt == "red"
}
题解
在上文的字符串的知识点梳理中,其实已经给出了类似的代码了。
// strings2.rs
//
// Make me compile without changing the function signature!
//
// Execute `rustlings hint strings2` or use the `hint` watch subcommand for a
// hint.fn main() {let word = String::from("green"); // Try not changing this line :)if is_a_color_word(&word) {println!("That is a color word I know!");} else {println!("That is not a color word I know.");}
}fn is_a_color_word(attempt: &String) -> bool {attempt == "green" || attempt == "blue" || attempt == "red"
}
Strings3
题目
// strings3.rs
//
// Execute `rustlings hint strings3` or use the `hint` watch subcommand for a
// hint.// I AM NOT DONEfn trim_me(input: &str) -> String {// TODO: Remove whitespace from both ends of a string!???
}fn compose_me(input: &str) -> String {// TODO: Add " world!" to the string! There's multiple ways to do this!???
}fn replace_me(input: &str) -> String {// TODO: Replace "cars" in the string with "balloons"!???
}#[cfg(test)]
mod tests {use super::*;#[test]fn trim_a_string() {assert_eq!(trim_me("Hello! "), "Hello!");assert_eq!(trim_me(" What's up!"), "What's up!");assert_eq!(trim_me(" Hola! "), "Hola!");}#[test]fn compose_a_string() {assert_eq!(compose_me("Hello"), "Hello world!");assert_eq!(compose_me("Goodbye"), "Goodbye world!");}#[test]fn replace_a_string() {assert_eq!(replace_me("I think cars are cool"), "I think balloons are cool");assert_eq!(replace_me("I love to look at cars"), "I love to look at balloons");}
}
题解
这个题目还是有点难度,考察 Rust 常用的字符串操作 API。
这里我根据题目的提示查阅了官方文档:
然后还要注意,有的方法挂在是字符串切片,有的则挂在可变字符串上。
所以在操作它们的时候,还得先转化。
// strings3.rs
//
// Execute `rustlings hint strings3` or use the `hint` watch subcommand for a
// hint.fn trim_me(input: &str) -> String {let string = input.to_string();string.trim().to_string()
}fn compose_me(input: &str) -> String {let mut string = input.to_string();// 这里有个坑点是,push_str 改变的是在原来的值上进行修改,而返回值是一个空的元组string.push_str(" world!");string// 还有一种比较奇技淫巧:// format!("{} world!", input)
}fn replace_me(input: &str) -> String {let str = input.replace("cars", "balloons");str.to_string()
}#[cfg(test)]
mod tests {use super::*;#[test]fn trim_a_string() {assert_eq!(trim_me("Hello! "), "Hello!");assert_eq!(trim_me(" What's up!"), "What's up!");assert_eq!(trim_me(" Hola! "), "Hola!");}#[test]fn compose_a_string() {assert_eq!(compose_me("Hello"), "Hello world!");assert_eq!(compose_me("Goodbye"), "Goodbye world!");}#[test]fn replace_a_string() {assert_eq!(replace_me("I think cars are cool"),"I think balloons are cool");assert_eq!(replace_me("I love to look at cars"),"I love to look at balloons");}
}