前面的章节介绍了Rust中的很多基本基本数据结构,并且常常用到String
这个类型,因为它与Rust中的一个核心概念有关:所有权
所以前面一直没有对它进行解答,而本文的目的,就是深究这个String
的底层原理,并以此作为媒介,引出所有权的概念
从比较宏观的角度去看你经常接触到的软件,可以发现,对于控制台程序,你看到的只有字符
而对于有图形界面的软件(GUI),除了图案、就是各种文字了(字符串)
从这你便能看出字符串的重要性:无论什么程序,基本都离不开字符串的使用
在rust中使用字符串,可以用以下代码:
fn main() {
let s=String::from("hello world");
println!("{}", s);
}
即:用String
这个crate(箱子、包)
中的from
函数,来从一个字面量:hello world
得到一个字符串String
类型
那看到这里你可能觉得有些奇怪了,明明我直接等于一个字符串字面量也可以呀!
比如像下面这样:
fn main() {
let s="hello world";
println!("{}", s);
}
这确实是没问题的,但如果你看到编译器自动推导的类型就会发现,此刻的s
已经不是String
类型了
是不是觉得很奇怪!这里的是&str
类型,并不是字符串String
类型
这里我们先不管这两个类型之间的区别,仅从现象上看,就能说明,字符串字面量的类型就是&str
因为
let s="hello world";
这段代码中,编译器推断s
的类型是通过右边的字面量来推断的,既然是s
是&str
类型,那么右边的字面量肯定也是&str
类型才对
那么就此可以推断出,String::from
这个函数的作用就是将一个字符串字面量&str
类型转换为String
类型
因为经由这个函数之后,编译器就推断出
s
为String
类型了
那么这两种类型有什么区别呢?
最直观的区别就是,String
类型是可变的,而&str
类型是不可变的
比如下面的代码:
fn main() {
let mut s=String::from("hello world");
s=s+"world"; //正确
println!("{}", s);
let mut s="hello world"; //用到变量隐藏特性
s=s+"test"; //错误
println!("{}", s);
}
即使你添加了mut
关键字,对于&str
类型,仍然是不可变的
下面我们就要开始分别深度探讨两个几个问题:
String
类型与&str
类型到底代表什么意思?只要搞懂了这三个问题,你对于rust的设计理念认知便又进了一大步,这对于之后的学习是很有帮助的。
因为除了字符串之外,其它很多地方都会用到这个特性,这并非是字符串所独有的。
首先看到第一个问题:
String
类型与&str
类型到底代表什么意思?首先我们要知道&
这个符号的作用,它是可以剥离出来的,即:实际上应该是两种类型,String
与str
&
基本就等价于C++中的引用,这个后面再提
其中,String
代表一个可独立操作的字符串,也就是说,它拥有一个字符串所有操作权力,比如下面这段代码:
let mut s=String::from("hello world");
此时变量s
就完全拥有了操作这个字符串"hello world"
的权利,增删改查均可以,一旦你这个变量s
没有了,那么"hello world"
这个字符串同样也会消失,两者是彻底绑定在一起的。
而str
这个类型,其意义仅在于你可以用它,比如打印,遍历,查看等等,除此之外,它对字符串本身根本做不了任何改变,因为它不拥有这个字符串
let mut a="hello world";
比如上面这段代码,即使a
变量消失了,这个字符串"hello world"
实际上仍然在内存中,并没有消失,因为a
并不拥有这个字符串
所以如果你想要修改一个str
类型的变量,你就必须先将其转换为String
类型
let mut a="hello world";
let mut s=a.to_string();
这个to_string
的原理是,将a
所指向的字符串复制一份并交由s
,然后s
就拥有了字符串副本的所有权利,可以任意增删改查
如果想要将String
转换为str
也是可以的,并且更加的简单:
let mut s=String::from("hello world");
let s1=&s[0..2];
就像上面那样,你只需要通过[0..2]
的方式表达一个范围,比如这里的意思就是s
中0
到2
的范围的子字符串,然后前面添加一个&
,就可以将其转换为str
类型,并交给s1
打印结果为:
he
因为下标是从0开始的,到下标2的位置,但不包含2
,如果用区间表示就是左闭右开:[0,2)
注意,在创建str
类型时,必须要在前面添加&
符号,代表引用的意思
事实上,它有一个更加官方的说话,叫做
slice
(切片),由于它不拥有字符串,所以必须要使用引用的方式,即在前面添加&
符号,其意义在于它可以使用,但不能修改,并且其本身不与字符串有其它任何权利关系
也因此,str
类型都不能单独存在,而是结合&
符号一起出现
事实上,并不仅仅是
str
,所有切片类型,都只能与&
符号共存,就因为它们没有内存的所有权,仅仅在于引用
这里所说的特性,指的是String
对字符串拥有所有权,而str
类型则没有所有权,明明就一个字符串类型而已,为什么要拥有两种类型呢?
以前提到过,rust这门语言的目的是简化编程的同时,尽可能地提高程序运行效率。
而程序运行过程中,最影响效率的地方就是内存拷贝问题
如果你学过C/C++、乃至其它语言,应该都多多少少听说过堆栈的概念。
前面我们提到过,我们的应用程序实际上都是运行在电脑内存里面的。
虽然同样是内存,但为了方便程序使用,我们会人为的给某些内存定义为栈、某些内存定义为堆
这个涉及系统底层架构原理,比如栈就有一个专用的寄存器来表示,但对于写程序的我们来说,并不需要了解太多
对于前面提到的函数,在调用时,参数想要传入函数内部,就是用的栈,在调用前将参数压栈,在函数内部就可以出栈取出各个参数,函数结束后,这块栈内存就自动被释放了。
对于函数内部定义的变量,同样是在这个栈中,比如我们前面在main
函数中用let
关键字定义的一系列变量,就在main
函数的栈中(只不过对于我们不可见),只要函数结束,这些变量就随着栈的消失而消失。
因此这些变量你只能在这个函数的内部进行使用
这也就意味着,如果使用栈,就只能保存当前函数内存使用的变量值,一旦传递给其它函数,就需要进行一次复制(将参数压入准备调用的函数的栈的过程)
这对于数字、字符还好,因为它们占用的空间也不大,也就几个字节而已
可一旦来到字符串那就不一样了,你应该常常能看到几k乃至几兆的文本文件吧,如果将其换算成为字节,比如1k,就是1024字节
而处理字符串的函数又很常见,如果每次处理一次,就要将这么多数据压栈进行传送,就太影响程序的效率了。
所以这个时候,就出现了堆,它的意义就在于,它并不在栈中保存数据,而是类似于随意找了一块没人用的内存来保存数据,并返回这块内存地址交给你。
只要你有了这块内存的地址,那你就可以使用这块内存上的数据。
比如上面说到的字符串,不同函数间,你不用再将整个字符串传递给对方,而是只需要将保存字符串的地址传给对方就行了,这样就可以极大的提高程序运行效率
上面说的这些,如果了解C/C++语言,应该很清楚其中的细节
在C/C++中,就需要用
new
或者malloc
函数来在堆中申请内存,但同样,你就需要在不用这块内存的时候,用delete或free将内存释放掉,否则就会内存泄漏
而在rust
中,由于为了安全考虑,并没有将这一过程给我们暴露出来
因为手动申请的内存,就意味着你需要手动去释放,如果不释放,就造成了常听到的内存泄露,如果多次释放同一块内存,又会造成程序直接崩溃,这是用C/C++写大型项目的灾难源泉
了解了这个,我们就可以回到上面的String
与&str
这两个类型了
在rust
中,为了更好的管理内存使用,String
类型统一作为在堆上申请的内存
比如这段代码:
let mut s=String::from("hello world");
字面量"hello world"
会嵌入最终生成的可执行文件中(.exe
),而一旦程序运行到这段代码,就会在堆上开辟一块内存,将这个字符串复制过去,而变量s
,则保存了这块内存的首地址、字符串的长度、以及这块内存的容量
字符串的长度不一定等于这块内存的大小,一般内存大小会比字符串长度更大,这是为了方便追加字符串在后面,如果追加时发现内存容量不够,就会重新开辟一块更大的内存,并将这块内存的数据全部拷贝过去,并释放掉这块内存
如果你用十六进制编辑器打开这个代码生成的可执行文件,就能直接看到字面量”hello world“
也就是说,在你代码中直接写出来的字符串,都是直接嵌入最终的可执行文件里面的
而&
符号的意思其实是引用,如果你学过C++,应该就很清楚它的意思了
它的意义在于,在不拥有这块内存掌控权的情况下,可以使用这块内存
因为你没有所有权,所以你的存在与否,和这块内存没有任何关系(这是
rust
中的新概念,与C++不同,C++中不存在什么所有权,只要你有内存的地址就能随意使用,没有则意味着这块内存丢了,也就是内存泄漏(没有delete的话))
这就是官方的概念:所有权(ownership)
通俗来讲就是,每块堆内存都会有一个变量具有它的所有权,并且只能有一个变量具有它的所有权,一旦这个具有这块内存所有权的变量没了,那rust就会自动将这块堆内存给删除掉
通过这种行为,就基本解决了C/C++中的内存管理问题,我们不用再手动删除内存了,同时也不怕内存泄漏了!
而这种所有权是可以移交的,比如以下这段代码:
fn main() {
let mut s=String::from("hello world");
println!("{}", s); //正确
let s1=s; //所有权移交给s1
println!("{}", s); //错误,s已经没有内存的所有权了
}
想要不移交,那就需要用到引用:
fn main() {
let mut s=String::from("hello world");
println!("{}", s); //正确
let s1=&s; //只是移交给s1一个引用
println!("{}", s); //正确,s仍然拥有这块堆内存的所有权
println!("{}", s1); //正确,s1拥有这块内存所有者s的引用
}
注意,此时s1的类型为&String
,而不是&str
用上面这个例子解释引用的本质为:s1变量里面存着的是s变量的位置,而s变量里面才存着堆内存的首地址、长度、内存大小
所以如果我们想要使用引用变量,按理说是应该先进行解引用,之后才能使用的,比如像下面这样:
println!("{}", *s1); //正确,s1拥有这块内存所有者s的引用
用*号可以对引用变量
进行解除引用,也就是取得s变量的位置,这句代码也就等价于:
println!("{}", s); //正确,s仍然拥有这块堆内存的所有权
之所以我们不用写*
来解除引用,是因为rust
编译器为了适配对应的参数,自动为我们解引用了的
毕竟如果一个变量引用了太多层,一旦需要解引用,就需要使用一大堆
*
,人为写起来就太麻烦了
所以一般来说,引用类型基本可以像所有者那样正常使用,只是它没有内存的所有权,也就是说,无论它存在与否,这块内存都会存在,这块内存是否存在与它无关
比如下面这个例子:
fn main() {
let s=String::from("hello world");
{
let s1=&s; //只是移交给s1一个引用
println!("{}", s1); //正确,s1拥有这块内存所有者s的引用
}
println!("{}", s); //正确,s仍然拥有这块堆内存的所有权
}
如果你像上面这样写,是完全没有问题的
变量的作用域就是它所在的
{}
之内,所以这里为s1
变量添加{}
的含义是这个变量只能存在与内部这个{}
中,一旦离开了,这个变量就会被删除
明白了以上的原理,我们就可以再来看看下面这个例子:
fn main() {
let s1; //只是移交给s1一个引用
{
let s=String::from("hello world");
println!("{}", s); //正确,s1拥有这块内存所有者s的引用
s1=&s; //错误
}
println!("{}", s1); //正确,s仍然拥有这块堆内存的所有权
}
这次我们将s
变量放在了内部的{}
中,如果这个时候,想要让外面的s1
变量取得它的引用就会报错
想一想为什么?
因为:s是这块内存的所有者,一旦离开了内部的
{}
它就会消失,这块内存的所有者没了,那它也会随之被释放,引用也就没有了意义,因为这块内存已经被释放了,rust
的编译器非常智能,可以在编译期间就给我们指出来哪里不对
上面的两个例子因为是放在了一个函数的内部,所以很容易看出来,所以下面我们拆分一下:
fn main(){
let mut s=String::from("hello world");
test(s);
println!("{}", s);
}
fn test(s:String){
println!("{}",s);
}
此时上面的代码就会报错,因为函数test
接收的是一个String
类型,一旦你将s
传递进去了,就意味着字符串的所有权已经移交了,此时s
不再拥有任何内存,所有你想要再使用它就会报错
这就是最常见的一个所有权错误,为了解决这个问题,我们就可以使用引用:
fn main(){
let mut s=String::from("hello world");
test(&s);
println!("{}", s);
}
fn test(s:&String){
println!("{}",s);
}
注意在函数声明,以及函数调用的时候,都是需要添加引用符号&
的
这一点与C++不同,C++中的引用只需要在函数声明的时候添加,而在函数调用的时候并不需要
最后再来看看关于所有权中最常见的一个错误:
fn main(){
let mut s=String::from("hello world");
let s1=&mut s;
let s2=&mut s;
println!("{} {}", s1, s2);
}
引用也是有两种类型的,一种是正常的引用,只可以使用,而另一种则是可变引用,就像上面代码中所写的那样:&mut s
,需要在引用后面添加一个mut
关键字,代表这个引用是可变的
此时上面的代码就会报错,因为我们同时创建了两个可变引用
报错理由也很容易理解,如果让同一个内存拥有两个可变引用,就意味着这块内存可以同时在两个地方被更改,这就潜在的蕴藏不可预知的错误,比如你想要将
hello world
中的world
删除掉,但却发现这个这个字符串已经被清空了(原因就是另一个干的,而你还不知道)
当然,改成下面这样也是不行的:
fn main(){
let mut s=String::from("hello world");
let s1=&mut s;
let s2=&s;
println!("{} {}", s1, s2);
}
即:创建了一个可变引用后,就不能再创建不可变引用了,因为可变引用会影响到不可变引用的值
所以总的来说就是,只要存在可变引用,那么就不能再有其它引用,而如果没有可变引用,那么不可变引用可以有任意多个
本文主要探究了一下rust中的String
与str
这两种类型,如果是初学者,通读下来可能还是有些云里雾里的。
所以这里列出一些要点,只要掌握这些要点,一般写程序都不会出现什么错误。
String
与str
的主要区别在于String
拥有内存的所有权,而str
不拥有str
是切片类型,不拥有内存,所有只能与&
符号组合使用,代表的是引用一个变量String
作为一个拥有字符串所有权的crate
,它所操作的所有字符串都存在堆内存中mut
关键字,比如let s1=&mut s;
,唯一需要注意的点是,只要可变引用存在一个,那就不能再存在其它引用了