白话Rust2~万物皆有主:什么是rust的所有权

在道教的卦辞解读中,有这么一副批语:

世间万物皆有主

一厘一毫君莫取

英雄豪杰自天生

也需步步循规矩

世界万物皆有主,万物都有它的来处,一朵花,一粒沙,皆有姓名,你不能随意的处置它,轻视它,你得步步循规矩,才能天人合一,才能羚羊挂角,不着痕迹。否则就是胡搅乱缠,惊起一滩鸥鹭了。

rust就是这样一门世界万物皆有主的语言,在rust中,核心功能之一就是所有权(ownership)机制:

  • 内存值都有唯一的owner(所有者)

先说结论

  • 所有权机制就是内存值都有唯一的owner
  • rust是静态内存管理
  • 所有权机制帮助编译器实现静态内存管理

所有权理解

要理解所有权,我们只需要对比其他的语言就明白了。

1. 首先对比手动管理内存的c语言

下面代码中,我们分配了一段100字节的内存,这段内存分别有两个指针:owner1和owner2。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main() {
    void* owner1 = calloc(100, 1);
    void* owner2 = owner1;

    char* data = (char*)owner2;
    strcpy(data, "Hello, World!");

    printf("output:%s\n", owner1);

    free(owner2);
    
    return 0;
}

很显然,owner1和owner2地位完全是平等的,都可以操作内存值:

  • 通过owner2往内存中写入数据
  • 通过owner1读出数据
  • 通过owner2释放内存

因此c语言没有所有权一说。

2. 对比gc管理内存的golang

同样,下面这段代码中,我们使用golang分配一段slice,owner1和owner2的地位是相同的,他们都可以访问该slice。golang也没有所有权一说。

owner1 := make([]byte, 100)
owner2 := owner1

copy(owner2, []byte("Hello, World!"))

fmt.Println(string(owner1))

3. 但是在rust中,情况不一样了

fn main() {
    let owner1 = String::from("hello");
    let owner2 = owner1;

    println!("{}, world!", owner1);//error
}

这段代码运行出错,原因就在于所有权机制。

这段代码的逻辑为:

  • 先为String类型分配了一片内存,owner1拥有该内存的所有权
  • 在赋值语句中,owner1把该所有权转让给了owner2。owner1丧失了所有权。
  • println所在行代码出错,它尝试通过owner1获取内存中的值,但是owner1已经丧失所有权。正确的做法,应该用owner2访问内存值

明白了吗,所有权机制就是owner唯一性,内存中的每一个值,在任意时刻,都只能有一个owner。

借用

所有权这个设计似乎有点反常识,毕竟几乎所有的主流语言,都没有所有权一说的。

比如常见的函数调用场景为:

  1. 定义一个变量
  2. 传递变量给函数
  3. 函数返回后继续使用该变量

以golang为例,我们写一个把hello world变成hello kitty的 函数

func main() {
   owner := make(map[string]string)
   
   owner["hello"]="world"
   fmt.Printf("hello, %v\n", owner["hello"])
   
   toKitty(owner)
   fmt.Printf("hello, %v\n", owner["hello"])
}

func toKitty(m map[string]string){
   m["hello"]="kitty"
}

在函数toKitty中,m变量和外面的owner变量,都共同拥有map对应内存的所有权。

函数执行完后,通过Printf继续使用owner变量,没有任何问题。

但是在rust中,唯一所有权的存在,上面的代码无法执行,owner将会在调用toKitty函数的时候,参数传递时失去所有权。

为了解决这个问题,rust中使用了借用(引用)的概念。Rust中,借用跟引用在rust中是相同的含义,意思就是:不改变所有权,临时借用下变量。

注意下面代码中,第10行使用了&,表明传递给函数的是一个引用,而不是所有权转移。因此,owner依然掌握着map的所有权。

use std::collections::HashMap;

fn main() {
    let mut owner:HashMap<String, String> = HashMap::new();
    owner.insert("hello".to_string(),"world".to_string());
    if let Some(v) = owner.get("hello"){
        println!("{} {}", "hello", v)
    }

    toKitty(&mut owner);
    if let Some(v) = owner.get("hello"){
        println!("{} {}", "hello", v)
    }
}

fn toKitty(m: &mut HashMap<String, String>){
    m.insert("hello".to_string(),"kitty".to_string());
}

总结一下:

  • Rust中内存值都有唯一的owner,但可通过借用(引用)的方式访问该值

静态内存管理

rust设计这么一套机制有什么意义呢,似乎除了把问题搞复杂,暂时未看到好处。

我还看到有文章声称,这样做的目的,是为了培养程序员优良的编程习惯,深入思考变量之间的关系,这就有点搞笑了。

rust引入所有权,本质是由rust的内存回收机制决定的。可以说,rust是不得不引入所有权。

在软件开发中,大多数场景下,内存管理是一个绕不开的坎,常见的内存管理方式有两种:

  • 垃圾回收机制(GC),在程序运行时GC也在运行,不断回收无用的内存,典型代表:Java、Go
  • 手动管理内存,需要程序员主动申请和释放内存,典型代表:C

通过GC的方式,程序员不需要管理内存释放,交给GC管理,代价就是GC很蠢,常常把系统搞得很慢。几乎没有强调高性能的软件,会使用Java和Go编写。即使使用了,也是一个脑袋几个大,天天琢磨各种优化。

而手动管理内存,则对程序员要求极高,一不小心就内存泄露,导致程序员秃头率居高不下。

而Rust则另辟蹊径,采用了第三种方式:

  • 静态内存管理

Rust会在编译期间,由编译器分析变量的作用域,在作用域结束时,自动由编译器添加上内存释放代码。

比如一段rust代码

{
    owner <- 分配一片内存  
    owner存活期
}//作用域结束

经过编译器分析后,就会变成:

{
    owner1 <- 分配一片内存  
    owner存活期
   (释放owner1内存,Drop)//由编译器添加
}//作用域结束

可以看到,编译器在变量作用域结束时,会自动加上释放内存的代码,而代码作者完全不用考虑。

这样妙啊,只要编译器对作用域判断得足够准确,就不需要runtime GC,避免了GC引入的性能开销和不确定性。也不需要手工管理,彻底防止了内存泄露。这使得 Rust 能够在对内存安全和性能要求较高的场景中发挥优势。

明白了这个rust对内存管理的思路之后,是不是所有权就理所应当了?

毕竟在编译器眼中,变量的作用域很容易判断,大多数情况下,通过大括号就可以判断。但作用域结束后,往往无法决定是否应该释放内存,因为该变量对应的内存,可能被其他变量继续使用,也就是,该内存有多个所有者。

那么,只要严格规定每个内存值都有唯一的所有者,那问题是不是迎刃而解了?在所有者作用域结束时,释放内存,perfect,简直是完美!

有人会问了,那内存释放了,其他引用怎么办,会不会引用到空内存?

显然rust也有对策的,在编译期间,同样会检查引用与owner的关系,确保所有引用的作用域,都小于owner的作用域,还是那句话,编译器很容易判断变量的作用域。

结论

现在结论是明显的了:

  1. rust在编译期间执行静态内存管理
  2. 所有权机制帮助编译器实现静态内存管理

好处多多,缺点也很明显,那就是程序员们又得花不少时间适应这种所有权风格了。但是对于真正的极客来说,这又算得了什么呢?

君不见,程序猿,多少性能追逐客,夜夜键盘响,白了少年头。



《 “白话Rust2~万物皆有主:什么是rust的所有权” 》 有 3 条评论

  1. how to get cheap cytotec for sale Cytologic differential diagnosis infundibular keratinizing acanthoma, epidermal cyst or follicular cyst

  2. Diosgenin 1 was also selected as the starting material in the synthesis of 26 thio and 26 seleno dioscins 102 and 103 in order to prefabricate their steroid cores, compounds 104 and 105, respectively Scheme 16 priligy alternative Andreanos K, et al

  3. Ok so, I am in Australia and after reading Some reviews here I have placed an order and after 30 days I got a letter from customs stating my gear was seized buy generic priligy

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

About Me

一位程序员,会弹吉他,喜欢读诗。
有一颗感恩的心,一位美丽的妻子,两个可爱的女儿
mail: geraldlee0825@gmail.com
github: https://github.com/lisuxiaoqi
medium: https://medium.com/@geraldlee0825