看一看Rust,记录笔记:所有权

第六章:所有权系统

通用概念

栈内存和堆内存

值语义与引用语义

值语义: 是指 按位复制 与原值无关,保证值的独立性。

基本数据都是值语义。

按位复制 就是栈复制,也是浅复制

深复制 : 对栈上和堆上数据一起复制

引用语义:

  1. 指数据存储在堆内存中,通过存储在栈内存的指针来管理堆内的数据,并且禁止按位复制。
  2. 动态数组和字符串都是属于引用语义

复制语义 与 移动语义

复制语义对应值语义,具有值语义特性的变量可以在栈上进行按位复制,以便于内存管理

移动语义对应引用语义,在堆上存储的数据只能进行深复制,而深复制又可能带来极大的性能开销,因此需要在栈上移动指向堆上数据的指针地址,不仅保证了内存安全,还用于按位复制的性能。

所有权机制

Rust 所有权机制的核心是以下的3点:

  1. 每个值都有一个被称为其所有者的变量,也就是每块内存空间都有其所有者,所有者具有这块内存空间的释放和读写权限
  2. 每个值在任意时刻由其仅有一个所有者
  3. 当所有者(变量)离开作用域,这变量将被丢弃

变量绑定

一般使用let 声明变量,但是本质是一种绑定语义。 let 关键字 将 一个变量 与一个值绑定在一起。这个变量就拥有了这个值的所有权。或者说与该内存空间绑定,从而拥有这块内存空间的所有权。

Rust确保对于每块内存空间都只有一个绑定变量与之对应,不允许有两个变量同时指向同一块内存空间

变量绑定具有空间和时间的双重属性。

空间属性:指变量与内存空间进行了绑定,

时间属性: 指绑定的时效性,也就是他的生命周期。

fn foo(){
	let s = String::from("hello"); // s 有效
}  // 函数执行完成,s离开作用域,drop函数 清理s 的内存空间

当多个变量,指定同样的堆存储的数据,注意!!!

所有权转移

所有权转移 对应于 移动语义。

—— 其绑定变量的所有权转移给另一个变量的过程

example : 变量赋值,函数传递值,函数返回值

变量赋值

所有权机制只针对堆上分配的数据,基本类型的存储都在栈上

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

    let s1 = String::from("hello");
    let s2 = s1;  // 这里所有权转移
    println!("s1:{},s2:{}", s1, s2) // 转移之后,并不能再使用s1 了: 因为指针无法指向堆上面的数据,
}

上述代码,有两种解决办法:

  1. 采用深度复制(栈堆数据都复制),将堆上的数据再复制一份给s2 (深度复制如果数据量大,会造成性能影响)
  2. 采用浅复制(只复制栈上数据),将 两个变量,都指向同一份堆数据(涉及修改变量,另一个也会改变。涉及s1,s2 离开作用域,drop 会清理两次,导致潜在的安全漏洞出现)

所以Rust 的解决办法, 就前变量 进行 设置为无效状态,在函数或者方法执行完毕后,不会再进行drop 清理。

向函数传递值

将值传递给函数在语义上给变量赋值相似,

向函数传递字符串参数时转移所有权
fn main() {
    let s = String::from("hello");// s有效
    take_ownership(s); // s 所有权转移给函数参数

    let x = 4;// x有效
    make_copy(x); // x 绑定值按位复制传递给函数参数
} // 作用域结束,x无效,s无效,s所有权转移,无须特殊处理

fn take_ownership(str: String) {
    println!("{}", str)
} // 释放str 的内存

fn make_copy(int: i32) {
    println!("{}", int)
}// int 无效,无须特殊操作
向HashMap 的方法传递&str 类型参数时不转移所有权
fn main() {
    let key = "Favorite color";
    let value = "red";


    let mut map = HashMap::new();

    map.insert(key, value);
    println!("{}", map[key])
}
向HashMap的方法传递String类型参数是转移所有权
fn main() {
    let key = String::from("Favorite color");
    let value = String::from("red");

    let mut map = HashMap::new();

    map.insert(key, value);  
    println!("{}", map[key]);  // 会 panic ,,插件也会有红下划线
}
// panic

向HashMap 的方法传递String类型参数的引用时不转移所有权
fn main() {
    let key = String::from("Favorite color");
    let value = String::from("red");

    let mut map = HashMap::new();

    map.insert(&key, value);
    println!("{}", map[key]);
}
// 根据上述代码,传递引用,不会报错
从函数返回值

函数的形参所获得的所有权会在函数执行完成时失效,失效之后再也不能被访问。

解决问题: 通过函数返回值将所有权转移给调用者。

fn main() {
    let s1 = give_ownership();

    // s1 所有权转移到take_and_give_back 函数中
    // take_and_give_back 函数返回值所有权转移给s2
    let s2 = take_and_give_back(s1);
}

fn give_ownership() -> String {
    let str = String::from("hello");
    str
}

fn take_and_give_back(name: String) -> String {
    let hello = String::from("hello");
    hello + " " + &name
}

浅复制与深复制

浅复制: 只复制栈上的数据

深复制: 复制栈上和堆上的数据

基本数据类型 默认支持 浅复制,String 类型不支持浅复制。

Copy trait 可以区分值定义和引用语义,实现这个的类型,

凡是值语义类型数据都支持浅复制。

整数类型、浮点数类型、布尔类型 等基本数据类型都默认实现了Copy trait,

对于元组类型上文中提到: 如果每个元素类型都实现了Copy trait,那么元组类型也支持浅复制

结构体和枚举,,即使所有元素类型都实现了Copy trait ,也不支持浅复制

当上述无法使用浅复制,必须使用Copy Clone 来变成深复制 #[derive(Copy,Clone)]

引用和借用

书中的理解: 引用是一种语法(本质上是Rust提供的一种指针语义),而借用是对引用行为的描述。

引用分为:

  1. 不可变引用 -----> 对应着不可变借用
  2. 可变引用 -------> 对应着可变借用

使用& 操作符执行不可变引用,

使用&mut 执行可变引用

通过&操作符完成对所有权的借用,不会造成所有权的转移

引用和可变引用

向函数传递实参时转移所有权

fn main() {
    let vec1 = vec![1, 2, 3];
    let vec2 = vec![4, 5, 6];

    let answer = sum_vec(vec1, vec2);
    println!("v1 : {:?}", vec1);
}

fn sum_vec(v1: Vec<i32>, v2: Vec<i32>) -> i32 {
    let sum1: i32 = v1.iter().sum();
    let sum2: i32 = v2.iter().sum();

    sum1 + sum2
}
// 这里会编译报错,,所有权通过函数参数,进行所有权转移,并不能再使用这个变量了。

通过函数的返回参数返回所有权

fn main() {
    let vec1 = vec![1, 2, 3];
    let vec2 = vec![4, 5, 6];

    let (v1,v2,answer) = sum_vec1(vec1, vec2);
    println!("v1 : {:?}", vec1);
}

fn sum_vec1(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
    let sum1: i32 = v1.iter().sum();
    let sum2: i32 = v2.iter().sum();

    (v1, v2, sum1 + sum2)
}

上述存在重复劳动

以引用作为函数参数不获取值的所有权

fn main() {
    let vec1 = vec![1, 2, 3];
    let vec2 = vec![4, 5, 6];

    let answer= sum_vec2(&vec1, &vec2);
    println!("v1 : {:?}", vec1);
}

fn sum_vec2(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
    let sum1: i32 = v1.iter().sum();
    let sum2: i32 = v2.iter().sum();

    sum1 + sum2
}

引用默认是只读的,如果想要修改引用,使用&mut 为可变引用

使用 *y 解引用的值,用来获取追踪引用的值

借用规则

为了内存安全,必须遵循规则:

  1. 对于同一个资源的借用,在同一个作用域只能有一个可变引用(&mut T) ,或 有 n 个 不可变引用(&T) ,但不能同时存在可变引用和不可变引用。
  2. 在可变引用释放前不能访问资源所有者
  3. 任何引用的作用域都必须小于资源所有者的作用域,并在离开作用域后自动释放

上述类似于 读写锁

fn main() {
    let mut x = 6;
    let y = &mut x;
    *y += 1;


    let z = &x;

    println!("y :{},z:{}",*y,*z)
}
// 上述代码就会报错,

报错信息可知: 在同一个作用域,有一个可变引用y,又有一个不可变引用z,这样显然违反了借用规则第一条

借用示例1:切片

切片本身是没有所有权的,他是通过引用语法实现对集合中一段连续的元素序列的借用

1. 切片定义

本质上就是指向一段内存空间的指针,用于访问一段连续内存块中的数据。

字符串切片与动态数组切片

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

    println!("{}", &s[0..5]);
    println!("{}", &s[..5]);
    println!("{}", &s[7..s.len()]);
    println!("{}", &s[7..]);
    println!("{}", &s[..s.len()]);
    println!("{}", &s[..]);


    let vec = vec![1, 2, 3, 4, 5, 6];
    println!("{:?}",&vec[0..2]);
    println!("{:?}",&vec[..2]);
    println!("{:?}",&vec[2..vec.len()]);
    println!("{:?}",&vec[2..]);
    println!("{:?}",&vec[0..vec.len()]);
    println!("{:?}",&vec[..]);

}
// 这里注意第一个字符串,如果长度不够,并不会报错
2. 切片作为函数参数

切片可以作为函数的参数,把数组、动态数组、字符串中一段连续的元素序列通过引用的方式传递给函数。

切片作为函数参数


fn main() {
    let s = String::from("Hello ,Rust!");
    let str = "Hello";
    let vec = vec![1, 2, 3, 4, 5];
    print_str(&s[0..5]);
    print_str(&str);
    print_vec(&vec[2..1]);
}

fn print_str(s: &str) {
    println!("sliect:{:?}", s)
}

fn print_vec(vec: &[i32]) {
    println!("sliect:{:?}", vec)
}
3. 可变切片

默认情况下,切片是不能改变所引用的数组、动态数组、字符串中的元素的,也就是说不能通过个更改切片的元素来影响源数据。

但是,如果声明源数据是可变的,同时声明切片也是可变的,就可以通过更改切片的元素来更改源数据。

fn main(){
	let mut vec = vec![1,2,3,4,5];
    let vec_slice = &mut vec[3..];
    vec_slice[0] = 7;
    
}

借用示例2:迭代器

迭代器所有权借用创建方法迭代器元素类型
IntoIter转移所有权into_iterT
Iter不可变借用iter&T
IterMut可变借用iter_mut&mut T
转移所有权IntoIter

迭代器IntoIter 由into_iter 方法创建,会把容器中元素的所有权转移给迭代器,之后原容器不能再使用

into_iter方法转移所有权

 let vec = vec!["java", "rust", "python"];
    for str in vec.into_iter() {
        match str {
            "rust" => println!("niubility"),
            _ => print!("{} ", str),
        }
    }
    // 这里会报错,创建迭代器后,原容器不能再使用
   //   println!("{:?}", vec);
}
不可变借用iter

迭代器iter 由iter 方法创建,能把容器中元素的引用传递给迭代器,而不发生所有权转移。即源容器还可以使用

iter方法获得所有权的不可变引用

fn main() {
    let vec = vec!["java", "rust", "python"];
    for str in vec.iter() {
        match str {
            &"rust" => println!("niubility"),
            _ => print!("{} ", str),
        }
    }

    println!("{:?}", vec);
}
可变引用IterMut

迭代器IterMut 由iter_mut 方法创建,会把容器中元素的可变引用传递给迭代器,不发生所有权转移。

iter_mut 方法与iter 方法的不同点在于, iter 方法创建的是只读迭代器,不能在迭代器中改变源容器元素。但是item_mut 创建的是可变迭代器,可以改变源容器的元素。

iter_mut 方法获得所有权的可变借用

fn main() {
    let mut vec = vec!["java", "rust", "python"];
    for str in vec.iter_mut() {
        match str {
            &mut "Rust" => {
                *str = "niubility";
                println!("{}", str)
            }
            _ => println!("{}", str)
        }
    }
    println!("{:?}", vec);
}

生命周期

Rust 的每一个引用以及把汗引用的数据结构,都有一个其保持有效的作用域。

生命周期可以视为这个作用域的名字。

如果存在多个周期某种方式关联的情况,就需要生命周期注解来进行描述。确保使用的引用是有效的。

生命周期语法

生命周期注解的语法是以‘开头再加上小写字母

生命周期注解位于引用的& 操作符之后,并用一个空格将生命周期注解与引用类型分隔开。比如 &’a i32

注意: 生命周期注解并不改变任何引用的生命周期的大小,只用于描述多个生命周期的关系

隐式生命周期
fn foo(x: &str) -> &str {
	x
}

实际上,foo函数包含隐式的生命周期注解,相当于下面代码。

fn foo<'a>(x: &'a str) -> &'a str {
	x
}

由编译器自动推导,这里要求返回值的生命周期必须大于或等于参数x的生命周期。

显示生命周期

无须必要的条件下,不需要显示指定,会降低程序的可读性。

fn  long_str(x:&str,y: &str) -> &str {
	if x.len()  > y.len() {
    	x
    }else {
    	y
    }
}

上述代码会报错。编译器无法判断 返回值的生命周期到底是与 x 或者y 的生命周期挂钩。

也就不能通过观察作用域来确定返回的引用是否总是有效。

fn long_str<'a>(x:&'a str,y: &'a str) -> &'a str {
	if x.len() > y.len(){
    	x
    }else{
    	y
    }
}
静态生命周期

Rust 预定义一种特殊的生命周期注解’static ,它具有和整个程序运行时相同的生命周期。

let s: &'static str = "I have a static lifetime";
fn foo<'a>(x: &'a str) -> &'a str {
	"Hello Rust!"
}

程序中大部分出现的与生命周期有关的问题,都是因为创建了悬垂引用或是生命周期不匹配,要解决这些问题不能只靠将生命周期设为'static

悬垂引用

含义: 引用了无效的数据,就是内存中的数据释放后,被再次使用。

fn main(){
	let r;
    {
    	let i = 7;
        r = &i;
    }
    
    println!("r:{}",r);
}

上述代码变量r 在内部作用域声明,但是在外部作用域使用,故报错。

这里错误,但凡学过的语言都不会出现这样的错误。。

为什么Rust 知道变量r 引用的值在尝试之前就已经离开作用域了?

答: 编译器中的借用检查器,他通过比较生命周期来确保所有的借用都是有效的。

生命周期与函数

通常,在函数体内不需要显示指定生命周期,这是因为上下文中Rust 可以能够分析函数的代码而不用协助。

但是当函数外的代码调用,rust 无法分析参数与返回值的生命周期。。

那么就需要在函数签名中显示指定参数与返回值的生命周期。

注意的是: 在函数签名中指定生命周期,并不会改变任何传入值或返回值的生命周期,而是指出了任何不满足这个约束条件的值都将被借用检查器拒绝。

生命周期可以结合泛型类型、trait约束一起使用

fn main() {
    let str1 = String::from("abcd");
    let str2 = "xyz";

    let result = long_str(str1.as_str(), str2);
    println!("logger string:{}", result)
}

// 对于 ‘a 的生面周期等于x 和 y 的生命周期中较小的那个。
fn long_str<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

生命周期与结构体

在结构体定义中使用生命周期注解,类似于声明泛型类型——将生命周期声明在结构体名称后面的的尖括号中。

如果需要为不同的字段指定不同的生命周期,这些生命周期都必须放在尖括号中并以逗号分隔。

impl 实现方法,也必须在impl后面声明生命周期

struct Foo<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

impl<'a, 'b> Foo<'a, 'b> {
    fn get_x(&self) -> &i32 { self.x }

    fn get_y(&self) -> &i32 { self.y }

    fn max_x(&'a self, f: &'a Foo) -> &'a i32 {
        if self.x > f.x {
            self.x
        } else {
            f.x
        }
    }
}

fn main() {
    let f1 = Foo { x: &3, y: &5 };
    let f2 = Foo { x: &7, y: &9 };

    println!("x:{}", f1.get_x());
    println!("max_x:{}",f1.max_x(&f2))
}

生命周期省略规则

默认规则——称为生命周期省略规则的引用分析模式,这些规则适用于函数或方法定义。

  1. 每一个被省略生命周期注解的参数,都具有各不相同的生面周期

    fn foo(x:&i32) 等同于fn foo<'a> (x:&'a i32)
    fn foo(x:&i32,y: &i32)等同于fn foo<'a,'b>(x:&'a i32,y:&'b i32)
    
  2. 如果只有一个生命输入周期(无论是否省略),这个 生命周期会赋给所有被省略的输出生命周期。

  3. 方法中self的生命周期会赋给所有被省略的输出生命周期

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值