rust教程 第四章 —— 深究String与所有权

文章目录

  • 前言
  • 一、字符串浅析
  • 二、探究两个问题
  • 三、所有权
  • 总结


前言

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

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

一、字符串浅析

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

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

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

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

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

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

rust教程 第四章 —— 深究String与所有权_第1张图片
那看到这里你可能觉得有些奇怪了,明明我直接等于一个字符串字面量也可以呀!

比如像下面这样:

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

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

rust教程 第四章 —— 深究String与所有权_第2张图片
是不是觉得很奇怪!这里的是&str类型,并不是字符串String类型

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

因为let s="hello world";这段代码中,编译器推断s的类型是通过右边的字面量来推断的,既然是s&str类型,那么右边的字面量肯定也是&str类型才对

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

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

那么这两种类型有什么区别呢?

最直观的区别就是,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类型到底代表什么意思?

首先我们要知道&这个符号的作用,它是可以剥离出来的,即:实际上应该是两种类型,Stringstr

&基本就等价于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

打印结果为:

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“
rust教程 第四章 —— 深究String与所有权_第3张图片
也就是说,在你代码中直接写出来的字符串,都是直接嵌入最终的可执行文件里面的

&符号的意思其实是引用,如果你学过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

rust教程 第四章 —— 深究String与所有权_第4张图片

用上面这个例子解释引用的本质为: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中的Stringstr这两种类型,如果是初学者,通读下来可能还是有些云里雾里的。

所以这里列出一些要点,只要掌握这些要点,一般写程序都不会出现什么错误。

  1. Stringstr的主要区别在于String拥有内存的所有权,而str不拥有
  2. str是切片类型,不拥有内存,所有只能与&符号组合使用,代表的是引用一个变量
  3. 只要涉及到所有权,都是指代的堆内存String作为一个拥有字符串所有权的crate,它所操作的所有字符串都存在堆内存中
  4. 所有权代表着,一块内存同一时间只能拥有一个所有者,但可以移交给别人
  5. 引用也分为两种,一般的是不可变引用,只能查看,还有一种是可变引用,使用方式是添加一个mut关键字,比如let s1=&mut s;,唯一需要注意的点是,只要可变引用存在一个,那就不能再存在其它引用了

你可能感兴趣的:(Rust从入门到精通,rust,开发语言)