一、前言
前面的章节介绍了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
类型了
是不是觉得很奇怪!这里的是&'static str
类型,并不是字符串String
类型。
这里我们先不管这两个类型之间的区别,仅从现象上看就能说明,字符串字面量的类型就是&' static str
因为编译器推断s
的类型是通过右边的字面量来推断的,既然是s
是&' static str
类型,那么右边的字面量肯定也是&'static str
类型才对。
那么就此可以推断出,String::from
这个函数的作用就是将一个字符串字面量&'static str
类型转换为String
类型。
因为经由这个函数之后,编译器就推断出s
为String
类型了。
注意这个'static
的前缀,它实际上并不是类型、而是生命周期标注,static意味着它是静态的、全局可用的,所以其更简洁的类型表示实际上是&str
。
那么这两种类型有什么区别呢?
目前来看,最直观的区别就是: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
类型,仍然是不可变的。
对于字符串的+
运算符,会将后面的字符串追加到第一个字符串并返回,比如s+"test"
,就是尝试将"test"
追加到s
之后、并返回拼接的结果,如果s
不可变,那么就会拼接失败。
原因在于mut
指代的是s
本身可变,比如你完全可以让s
等于一个新的字符串:
let mut s="hello";
s="world"; //s变量本身可变,所以可以重新赋值为新的字符串。
s+"world"; //+运算符的底层逻辑是在s所指向的字符串后拼接、追加新的字符串,由于字符串s的类型为&str,不可变,所以失败
下面我们就要开始分别深度探讨两个几个问题:
String
类型与&str
类型到底代表什么意思?- 为什么两者会有这个特性?
只要搞懂了这两个问题,你对于rust的设计理念认知便又进了一大步,这对于之后的学习是很有帮助的。
因为除了字符串之外,其它很多地方都会用到这个特性,这并非是字符串所独有的。
三、&str与String
首先看到第一个问题: String
类型与&str
类型到底代表什么意思?
我们要知道&
这个符号是可以剥离出来的,实际上应该是:String
与str
。
如何你对C++熟悉的话,那么可以认为&
基本就等价于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
。
此时s1
就为子字符串:he
。
因为下标是从0开始的,到下标2的位置,但不包含2
,如果用区间表示就是左闭右开:[0,2)
注意,在创建str
类型时必须要在前面添加&
符号,代表引用,因为它不拥有该字符串。
其意义在于它可以使用,但不能修改,并且其本身不与该字符串有其它任何权利关系。
事实上,它有一个更加官方的说话,叫做slice
(切片)。
也因此,str
类型都不能单独存在,而是结合&
符号一起出现。
事实上并不仅仅是str
,所有切片类型都只能与&
符号共存,原因就在于它们没有内存的所有权,仅仅在于引用。
以上就是对第一个问题的简要探究,下面我们再来看看第二个问题: 为什么两者具有这种特性?
这里所说的特性,指的是String
对字符串拥有所有权,而str
类型没有所有权,明明就一个字符串类型而已,为什么要拥有两种类型呢?
在第一章提到过,rust这门语言的目的是简化编程的同时,尽可能地提高程序运行效率。
而程序运行过程中,最影响效率的地方就是内存拷贝问题。
如果你学过C/C++、乃至其它语言,应该都多多少少听说过堆栈的概念。
前面我们提到过,我们的应用程序实际上都是运行在电脑内存里面的。