Rust还有第二种字符串类型:String
它是在 Heap 上进行分配的,能够存储在编译时未知数量的文本,相对于那些基础标量数据类型更加的复杂,基础标量数据类型是存放在 Stack上的,它们在离开自己的 scope 时会自动弹出栈,我们如果使用字符串字面值声明字符串的话是不可变的,String就是为了此需求而诞生的
我们可以使用 from 函数从字符串字面值创建出 Strng 类型:
fn main() {
let mut s = String::from("Hello");
println!("{}",s);
s.push_str("_World");
println!("{}",s);
}
// Hello
// Hello_World
而 String类型可以修改的主要原因就是处理内存的方式和字符串字面值处理内存的方式不一样
字符串字面值在编译的时候就知道它的内容了,其文本内容是直接被硬编码到最终的可执行文件中的,所以字符串字面值的优势就是速度快,这得益于它的不可变性
String类型就是为了支持可变性而诞生的,它需要在 Heap 上分配内存来保存编译时未知的文本内容
如果在拥有 GC 的语言中,GC 会跟踪并清理不再使用的内存,如果该语言没有 GC ,那么就需要我们自己去识别内存何时不再使用,并调用代码将它返回,如果我们没有返回,内存就会浪费,如果返回提前了,变量则会非法,如果做了俩次返回操作,则会出现不可预估的BUG
当然,既然提到了这点,那么 Rust 必然是有解决办法的,对于某个值来说,当拥有它的变量走出 scope 时,内存则会立即释放,也就是立即自动的将内存返回给操作系统,这一步的实现归功于 Rust 的 drop函数,只要变量走出 scope, Rust立即执行 drop 函数,进行内存释放
Rust 中有 Move这样一个概念,应用于变量和数据的交互类型:多个变量可以于同一个数据使用一种独特的方式来进行交互
fn main() {
let x = 66;
let y = x;
// 这里的 x 是之前 x 的副本
println!("{}--{}",x,y);
}
// 66--66
整数是已知且固定大小的简单的值,这俩个整数字面量被压到了 Stack 中
当然这是标量数据类型,如果是存储在 Heap 内的数据呢?我们以 String 类型为例子:
fn main() {
let s1 = String::from("Hello");
let s2 = s1;
println!("{}",s2);
}
// Hello
这没有问题呀,有什么区别吗?
区别其实就在于 s1 不见了!不信我们可以输出 s1:
fn main() {
let s1 = String::from("Hello");
let s2 = s1;
println!("{}",s2);
println!("{}",s1);
}
// borrow of moved value: `s1`
Rust 说 s1 被移动了?这是怎么回事?
这个问题其实还和数据存储离不开,我们来看一下 String 的结构:
没错,指针、长度、容量,String就是由这三个部分组成的、这些东西将存放在 Stack 上、存放真正字符串内容的部分不在 Stack上,而是在 Heap 上
当我们把 s1 赋给 s2 的时候, String 的数据被复制了一份,具体是怎么进行的呢?
为什么 s1会失效?
其实不难理解,就是因为我们上述提到的解决内存释放问题 Rust 给我们的答案:
当变量离开 scope 的时候,Rust 会自动调用 drop 函数,这个我们之前提到过,并将变量使用的 Heap 内存释放,所以按照这个逻辑,s1、s2都离开 scope 时会怎么做?必定会引起二次释放,然后导致程序错误
为了保证内存的安全,Rust 没有去复制被分配的内存、而是让 s1 失效,当 s1 离开 scope 时无需释放任何内存
所以让我们的思路再次回到标题: Move
我之前是前端开发工程师、所以对于 JavaScript 有些见解、对于深浅拷贝也是烂熟于心,在Rust 中也是有这样一个模糊概念的,为什么是模糊概念?我们可以将复制 Stack 理解为浅拷贝,但是深拷贝呢?Rust 已经让 s1 失效了,所以 Rust 叫它为 移动,Rust是不会自动去创建数据深拷贝的
在运行时,为了性能的极致追求,任何的自动赋值的操作都是廉价的
如果我们真的有需求对 Heap 上的数据进行深拷贝的话,可以使用 Rust 为我们提供的 clone 方法
fn main() {
let s1 = String::from("Hello");
let s2 = s1.clone();
println!("{}--{}",s1,s2);
}
// Hello--Hello
上面我们提到了 Heap 上的数据克隆、接下来我们谈谈 Stack 上的数据复制怎么理解
在 Rust 上有这样一个接口:Copy trait
,它用于类似整数这样的完全存放在 Stack 上的数据类型的复制
如果某一个类型实现了 Copy 这个 trait,那么旧的变量在赋值之后任然可以使用
如果一个类型的一部分实现了 Copy trait,那么 Rust 是不允许它实现 Cpoy trait的,这主要是针对元组这样的数据类型的,如果 Tuple 中所有字段拥有 Copy trait的话,那么该 Tuple 也将拥有 Copy trait
拥有 Copy trait 的类型:
当然函数在返回值的过程中同样也会发生所有权的转移
也就是说一个变量总会遵循以下模式:
那么使函数使用某一个值但是不获取其所有权呢?
fn main() {
let s1 = String::from("Hello");
let (s2,len) = func(s1);
println!("{}->length:{}",s2,len);
}
fn func(s:String) -> (String,usize) {
let length = s.len();
(s,length)
}
// Hello->length:5