4. Rust中的String、str与所有权

一、前言

前面的章节介绍了Rust中的很多基本基本数据结构,并且常常用到String这个类型,因为它与Rust中的一个核心概念有关:所有权

所以前面一直没有对它进行解答,而本文的目的,就是深究这个String的底层原理,并以此作为媒介,引出所有权的概念。

二、字符串浅析

从比较宏观的角度去看你经常接触到的软件可以发现,对于控制台程序,你看到的只有字符。

而对于有图形界面的软件(GUI),除了图案、就是各种文字了(字符串)。

从这你便能看出字符串的重要性:无论什么程序,基本都离不开字符串的使用

在rust中使用字符串,可以用以下代码:

fn main() {
    let s=String::from("hello world");
    println!("{}", s);
}

方式是用String这个crate(箱子、包)中的from函数,来从一个字面量hello world得到一个字符串String类型:

image.png

那看到这里你可能觉得有些奇怪了,明明我直接等于一个字符串字面量也可以呀!

比如像下面这样:

fn main() {
    let s="hello world";
    println!("{}", s);
}

这确实是没问题的,但如果你看到编译器自动推导的类型就会发现,此刻的s已经不是String类型了

image.png

是不是觉得很奇怪!这里的是&'static str类型,并不是字符串String类型。

这里我们先不管这两个类型之间的区别,仅从现象上看就能说明,字符串字面量的类型就是&' static str

因为编译器推断s的类型是通过右边的字面量来推断的,既然是s&' static str类型,那么右边的字面量肯定也是&'static str类型才对。

那么就此可以推断出,String::from这个函数的作用就是将一个字符串字面量&'static str类型转换为String类型。

因为经由这个函数之后,编译器就推断出sString类型了。

注意这个'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类型到底代表什么意思?

我们要知道&这个符号是可以剥离出来的,实际上应该是:Stringstr

如何你对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]的方式表达一个范围,比如这里的意思就是s02范围的子字符串,然后前面添加一个&,就可以将其转换为str类型,并交给s1

此时s1就为子字符串:he

因为下标是从0开始的,到下标2的位置,但不包含2,如果用区间表示就是左闭右开:[0,2)

注意,在创建str类型时必须要在前面添加&符号,代表引用,因为它不拥有该字符串。

其意义在于它可以使用,但不能修改,并且其本身不与该字符串有其它任何权利关系。

事实上,它有一个更加官方的说话,叫做slice(切片)。

也因此,str类型都不能单独存在,而是结合&符号一起出现。

事实上并不仅仅是str,所有切片类型都只能与&符号共存,原因就在于它们没有内存的所有权,仅仅在于引用。

以上就是对第一个问题的简要探究,下面我们再来看看第二个问题: 为什么两者具有这种特性?

这里所说的特性,指的是String对字符串拥有所有权,而str类型没有所有权,明明就一个字符串类型而已,为什么要拥有两种类型呢?

在第一章提到过,rust这门语言的目的是简化编程的同时,尽可能地提高程序运行效率。

而程序运行过程中,最影响效率的地方就是内存拷贝问题。

如果你学过C/C++、乃至其它语言,应该都多多少少听说过堆栈的概念。

前面我们提到过,我们的应用程序实际上都是运行在电脑内存里面的。

作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux