程序在内存中运行,所有权指的也是内存的所有权,这个概念被提出的目的,就是让Rust在编译阶段能够更有效地分析内存资源,并优化管理,相当于是静态的垃圾回收。

A 1 , A 2 , ⋯   , A n A_1,A_2,\cdots,A_n A1,A2,,An是一群变量, V 1 , V 2 , ⋯   , V n V_1, V_2,\cdots, V_n V1,V2,,Vn是内存中的值,那么Rust中的所有权可以写成三条规则:

  • V n V_n Vn对应某个变量 A n A_n An,则该变量 A n A_n An V n V_n Vn的所有者。
  • 在某个时刻, V n V_n Vn只能对应一个 A n A_n An
  • A n A_n An离开作用域后, V n V_n Vn会被删除。

这里面的值,可以理解为是物理上的一段存储空间,所有者即变量,是代码中用于标记这段内存的名字。

可行域

在Rust中,任何变量都有其可行域,在作用域中创建的变量,无法渗透到外面,这就是所有权的一个体现。比如在调用下面这个函数时就会报错

fn owner(){
    {
        let a = "micro";
    }
    println!("{}",a);
}

在上述代码中,a就是一个变量,或者说所有者;字符串micro则是a的值;外面的花括号就是a的可行域。根据所有权的第【3】条规则,a离开作用域后,"micro"会被删除。

在编程中难免遇到一些长度不固定的变量,所以程序必须有在执行期间分配内存的能力,不能一切都指望编译期。而且这个分配,既包括新内存的发放,也包括老内存的销毁。Rust通过所有权机制,在编译期会自动添加一些释放资源的函数,以维护内存安全。

移动和克隆

下面这种写法大家已经司空见惯了,将5绑定给x,然后再将x绑定给y,其编译运行结果没有任何疑问,符合直觉。

fn test_move1(){
    let x = 5;
    let y = x;
    println!("x={},y={}", x,y)
}

但换一种数据类型,结果却报错了。

fn test_move2(){
    let x = String::from("micro");
    //let y = x;            //报错
    let y = x.clone();      // 正确
    println!("x={},y={}", x,y)
}

根据等号赋值的错信息可知,需要调用clone方法对x进行克隆,即修改后采用的x.clone()。在直接使用等号赋值的过程中,相当于把x的值移动给了y,x自己就没有了,触犯了所有权的第【2】条规则。

在rust中,一些小而直接的数据类型,默认启用克隆模式,比如test_move1中演示的整数,这些类型还包括

  • 所有整数类型,例如 i32、u32、i64 等。
  • 布尔类型 bool,值为 true 或 false 。
  • 所有浮点类型,f32 和 f64。
  • 字符类型 char。
  • 仅以上类型数据的元组

函数传参

在rust中,任何表达式都有返回值,故而可看成是函数,所以变量在传递过程中的特性,在函数传参时仍旧适用,示例如下

fn test_print(s:String){
    println!("s={}", s);
}

fn test_print_int(i:i32){
    println!("i={}", i);
}

fn test_move3(){
    let x = String::from("micro");
    //test_print(x);        //所有权问题报错
    test_print(x.clone());
    println!("x={}", x);
    let y = 5;
    test_print_int(y);      //无所有权问题
    println!("y={}", y);
}

其中, y y y在成为函数的参数后,并未触发所有权问题;但字符串 x x x在直接传入test_print后,会因所有权问题报错。

引用与租借

如果想把一个复杂的变量x赋予y,要么选择移动,但这样x自己就没了;要么选择克隆,但开销比较大。一个自然的想法就是,能不能把x的地址传给b,这样两人就可以共享一块内存区域。

这种操作rust当然是支持的,名曰引用,只需用到取地址符&,这个特性在前面介绍循环时提到过,示例如下

fn test_ref(){
    let x = String::from("cool");
    let y = &x;
    x.push_str("l")
    println!("x={},y={}", x,y)
}

函数参数传递的道理一样。

但是,正所谓皮之不存毛将焉附,如果y在引用x的值之后,如果x的值被移动给了另外一个变量,那么y的引用也自然就作废了。

另一方面,y只是引用到了x的地址,但并没有得到这篇内存的所有权,所以y在理论上是不可写入的,这种感觉就像是借书一样,可以随便看,但不能乱写乱画。所以下面的代码报错就是理所当然的了。

实例

fn test_borrow(){
    let x = String::from("cool");
    let y = &x;
    y.push_str("l");
    println!("{}", y);
}

但是,非要更改也不是完全不可以,只需采用mut引用,写成下面这样就可以了,y成了一个可变引用类型的数据。

fn test_borrow2(){
    let mut x = String::from("cool");
    let y = &mut x;
    y.push_str("l");
    println!("{}", y);
}

但可变引用也存在问题,即只允许可变引用一次。最后,引用不可以作为函数的返回值,因为函数的返回值将不确定这个引用会给谁,可能导致灾难性后果。

这就是Rust,严谨而死板。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐