Rust 基础知识6 - 所有权(枯燥但重要)

简介

  • 接上回

Rust 的所有权概念

  • Rust 的核心特性就是所有权。
  • Rust 和大多数语言的垃圾回收机制(内存垃圾回收)不同,它是通过一个所有权系统来管理内存。
  • 所有权特性不会减慢程序的运行速度。
  • 这部分内容枯燥且重要,切记。

Stack(栈内存) 和 Heap(堆内存)

  • Stack和Heap都是你可用的内存,但是他们的结构不同。
  • 栈内存运行速度相较于堆内存快的多,因为栈内存是挨着顺序排列的,但是堆内存并不是绝对连续的。
  • 存储数据时需要注意,把值压入stack上不叫分配,因为指针是已知的固定大小,可以把指针存放在stack上。

所有权存在的原因

  • 跟踪代码的那部分正在使用heap的哪些数据
  • 最小化heap上的重复数据
  • 清理heap上未使用的数据以避免空间不用。

所有权规则

  • 每个值都有一个变量,这个变量就是该值的所有者。
  • 每个值同时只能有一个所有者,这和一些传统语言不同,比如C语言,同一个变量可以有多个指针标记。
  • 当所有者超出作用域(socpe)时,该值将被删除。

String 类型

  • 基础数据类型存放在栈上,但是String类型是存储在堆上的。
  • String类型别基础标量数据类型更复杂,这种类型在heap上分类,能够存储在编译时为止的数量
// 可以使用 from 函数从字符串字面值创建出String类型
let s = String::from("hello");
  • 这类字符串是可以被修改的,但是字符串字面值是不能被修改的,因为字符串字面值在编译时就知道它的内容了,其文本内容直接被硬编码到最终的可执行文件中,速度快、高效也是因为其的不可变性质。

内存和分配

  • Rust 采用了不同的方式:对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动交换给操作系统。
  • 内存释放是会自动调用 drop函数。

变量和数据交互的方式(Move)移

  • 这是Rust 的特点之一,但是对于“栈”变量和“堆”变量还是有区别的,比如如下代码可以看到第一个println是可以执行成功的,但是第二个就会造成编译错误。


    image.png
  • 那么如上的原因是什么呢,因为对于栈数据来说上面代码实际上是完成了一次深copy,他会给x1 对应的值入栈,但是对于string他移动的就是指针了。
image.png
  • 就像上面的结构图一样,Rust是不允许这种情况出现的,一旦发生浅拷贝(实际上是Rust的Move)那么变量s2将指向堆位置,s1也就被释放了。

  • 所以Rust 通过 Move 来解决祖先语言的二次释放的问题。

  • 浅拷贝和深拷贝,Rust中的Move 不仅仅是浅拷贝,因为他不仅仅复制了指针,还把之前的数据指针给释放了,这样就保证了他在编译阶段是安全的。

fn main() {
    let msg = String::from("Hello");
    let said = msg; // 这里赋值后 msg 变量指针就失效了
    println!("Stack num x:{} , y:{}", msg, said);
}
  • 如上代码编译是无法通过的会报错:^^^ value borrowed here after move
    image.png
  • 如果要完成字符串的深copy 需要使用clone 方法,举例:
fn main() {
    let s1 = String::from("Hello");
    let s2 = s1.clone(); // 这样就不会导致 s1 失效了
    println!("Num : {},{}", s1, s2); // 会导致失败
}

  • 数据的克隆使用 clone 方法,开销相对较大,主要应用于堆数据,对于栈数据则不需要考虑克隆的问题,因为深拷贝和浅拷贝在栈数据上的行为是一致的,可以看一下如下代码:
// 栈数据的copy 
fn main() {
    let x = 3;
    let y = x; // 深浅copy 行为一致
    println!("Stack num x:{} , y:{}", x, y);
}
image.png
  • 相关概念总结,Copy trait 可以用于像整数这样完全存放在stack上面的类型,如果一个类型实现了 copy 这个 trait,那么旧的变量复制后仍然可以使用比如 i32,如果一个类型或者该类型的一部分实现了 Drop trait 那么Rust 不允许让它再去实现Copy trait了。

  • 一些拥有 Copy trait 的类型(说人话就是:一些具备Copy接口的类型)

1、任何简单标量的组合类型都是可以Copy的
2、任何需要分配内存或某种资源的都不可以Copy的。
3、一些拥有Copy trait类型,整数全部、bool、浮点、char、
4、Tuple,如果 (i32,i32) 是,(i32,String)不是,也就是这个元组一部分没有实现Copy trait。

所有权与函数

  • 如果你理解C语言的指针和传引用概念实际上这部分也不难理解,当然这部分也很重要避免以后一脸懵逼。
  • 先举一个简单的例子:
// 如下代码是无法通过编译的
fn main() {
    let s1 = String::from("Hello");
    say(s1);
    println!("Variable val: {}", s1);
}

fn say(msg: String) {
    println!("Msg : {}", msg);
}
image.png
  • 出现这个问题的原因是s1的所有权被Move了,Move到say里面,结果这个函数
    终止后超出作用域,就被销毁了

  • 为了处理这种方式可以有两种形式,形式1,是通过返回值将所有权在交回去,例如:

fn main() {
    let s1 = String::from("Hello");
    let s2 = say(s1);
    println!("Variable val: {}", s2);
}
fn say(msg: String) -> String{
    // println!("Msg : {}", msg);
    msg
}
  • 第二种方式是把引用传过去,也就是传一个指针过去,而不是真的把变量扔过去,例如:
fn main() {
    let s1 = String::from("Hello");
    say(&s1); // 这里面也需要使用 &s1 将引用指针明确的传递过去
    println!("Variable val: {}", s1);
}
// 注意这里面的参数定义 &String 表示传递引用指针
fn say(msg: &String) {
    println!("Msg in fun : {}", msg);
    
}
image.png

引用与借用

  • 通过&符合可以把引用传递过去,其实就是类似C语言中指向指针的指针,这时候传递并不是所有权,所以不会销毁原来的变量。


    image.png
  • 但是这个也有一个比较特殊的地方需要注意,借用的东西默认是不可变的,如果一定要改变借用变量的值同样需要 mnt 关键字进行辅助。&mnt String

  • 这会导致直接报错。


    image.png
  • 尝试修改一下:

fn main() {
    // 变量要声明成 mut 类型
    let mut s1 = String::from("Hello");
    say(&mut s1); // 带入值也需要传入可变引用地址
    println!("Variable val: {}", s1);
}

fn say(msg: &mut String) { // 注意这里声明成 mut 可变
    msg.push_str("!!!");
    println!("Msg in fun : {}", msg);
    
}
  • 这样是可以的


    image.png
  • 可变引用重要的限制,在特定的作用域内,对某一块数据,只能有一个可变的引用,这样做的好处是在编译时防止数据竞争,以下三种行为下会发生数据竞争:
1、两个或者多个指针同时访问同一个数据。
2、至少有一个指针用于写入数据。
3、没有使用任何机制来同步对数据的访问。
  • 多个不变的引用是可以的。
  • 另外不可以同时拥有一个可变引用和一个不可变引用。举例来说:

fn main() {
    // 变量要声明成 mut 类型
    let mut origin_str = String::from("Hello");
    let s1 = &origin_str;
    let s2 = &origin_str;
    let s3=&mut origin_str; // 这就会报错,某一个区块只能有一个可变引用
}
  • 单个区块多个可变引用也会报错,违反了某个区块只能有一个可变引用的原则


    image.png
  • 但是可变引用也并非不能创建多个,可以通过作用域来创建多个可变引用,举例:

// 可以用 {} 创造一个块
fn main() {
    // 变量要声明成 mut 类型
    let mut origin_str = String::from("Hello");
    // let s1 = &origin_str;
    // let s2 = &origin_str;
    {
        let s3=&mut origin_str;
    }
    let s4=&mut origin_str;
    println!("! {} ", s4);
}
image.png

悬空引用 Dangling References

  • Rust 在编译器就可以防止这种“野指针”的存在,如果你引用了某些数据,编译器将保证在引用离开作用域之前数据不会离开作用域。

  • 举例来说,下面的截图中函数 dangle 返回字符串引用后,字符串就s就会被回收,此时如果可以编译会导致str变成野指针,但是Rust显然可以防止这种情况的出现


    image.png

    image.png
  • 对于引用再做个小节:

1、一个可变的引用。
2、任意数量不可变的引用。
3、引用必须一直有效。

切片

  • Rust 另外一种不持有所有权的数据类型,就是切片(slice)

字符串切片

  • 字符串切片是指向字符串中一部分内容的引用
fn main() {
    let slice_str = "What do you want?";
    let s1 = &slice_str[..5];
    let s2 = &slice_str[5..8];
    println!("{},{}", s1, s2);
}
image.png
  • 需要注意的问题:
1、字符串切片的范围索引必须发生在有效的UTF8字符边界内。
2、如果尝试从一个多字节的字符串中创建字符串切片,程序会报错并退出。
  • 用字符串切片的好处就是,当原始字符串被释放掉后切片也就不可用了,比如下图的情况:


    image.png
  • 行6调用clear() 方法会释放掉 slice_str 从而造成程序错误,不过Rust强大的编译机制会事先防止这种情况的发生,如果硬编译会提示下面的错误:


    image.png

字符串字面值就是切片

  • 有经验的Rust 开发者会采用 &str 作为参数类型,因为这样就可以同时接受 String 和 &str 类型的参数了,因为 String 可以通过切片创造 &str ,这样API函数就会变得更通用。
  • 建议把函数参数改成字符串切片,就是 &str

数组的切片

  • 说实话这个没什么可说的,和字符串切片的使用方法类似。
fn main() {
    let num_arr = [1,2,3,4,5];
    let select_num = &num_arr[1..2];
    println!("Second number is : {}", select_num[0]);
}
image.png

结束

  • 感谢阅读
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容