# stack vs. heap

stack 速度比 heap 快

所有权解决的问题:

  1. 跟踪代码哪些部分正在使用 heap 的哪些数据
  2. 最小化 heap 上的重复数据量
  3. 清理 heap 上未使用的数据,以避免空间不足

管理 heap 数据是所有权的主要原因

# 所有权规则

  1. 每个值都有一个变量,这个变量是该值的所有者
  2. 每个值同时只能有一个所有者
  3. 当所有者超出作用域(scope)时,该值将被删除。

# 变量作用域

  • scope 就是程序中一个项目的有效范围

# 所有权举例:String 类型

  1. String 比那些基础标量数据类型更复杂
  2. 字符串字面值:程序里手写的那些字符串值。不可变。
  3. String 在 heap 创建

# 创建 String 类型的值

使用 from 函数从字符串字面值创建出 String 类型

let mut s = String::from("Hello");

这类字符串是可以被修改的

内存:

  1. 内存不释放就浪费内存
  2. 提前释放,变量就非法
  3. 释放多次就导致 bug,因此必须一次分配对应一次释放

# 内存和分配

  1. rust 对于某个值来说,当拥有它的变量走出作用范围时,内存就会立即自动交还给操作系统
  2. drop 函数

# 变量和数据交互的方式:移动(move)

  1. 多个变量可以与同一个数据使用一种独特的方式来交互

let x = 5;

let y = x;

这 2 个变量的值压入到 stack 中

# 变量和数据交互的方式:移动,String 版本

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    // 这里会报错;
    // 因为 s1 的值所有权已经移动到 s2 了;
    //s1 失效,不能再使用了
    println!("{}", s1); // error.
}

# 浅拷贝、深拷贝和移动

浅拷贝(shallow copy)和深拷贝(deep copy)是其他语言的概念。

浅拷贝导致内存得不到释放或者多次释放。

rust 不使用浅拷贝这种概念,rust 使用移动(move)概念;
例如上面的 s1 和 s2;s2 = s1 会把 s1 的值移动到 s2;数据的所有权移动到了 s2,且 s1 失效。

rust 也不会创建深拷贝,移动操作都是很廉价、快速的。

# 变量和数据交互的方式:克隆(Clone)

如果真的需要深拷贝,可以使用 clone 方法,这里不使用深拷贝概念。

浅拷贝对应 rust 的移动概念,rust 多了一步失效前一个变量的操作。

深拷贝对应 rust 的克隆概念。

以上移动和克隆都是对 heap 上的数据讨论的。stack 上的数据直接复制就 ok。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();
    println!("{} {}", s1, s2);
}

# stack 上的数据:复制

fn main() {
    let x = 5;
    let y = x;
    // 不报错,这里不发生移动
    //stack 执行的是复制操作
    //stack 数据都是确定的,所以可以自动计算
    println!("{} {}", x, y);
}
  1. Copy trait: 可以像整数一样,完全放在 stack 上面的类型。
  2. 如果一个类型实现了 Copy 这个接口 (trait,特性),那么旧的变量在赋值后仍然可以使用。因为发生的是复制 (Copy),而不是移动。
  3. 因此,如果一个类型或者该类型的一部分实现了 Drop trait,那么 rust 不允许再实现 Copy trait。因为复制和移动这 2 个概念是冲突的,因此不能同时实现。

# 一些拥有 Copy trait 的类型

  1. 任何简单的标量的组合类型都可以 Copy 的
  2. 任何需要分配内存或者某种资源的都不是 Copy 的
  3. 一些拥有 Copy trait 的类型:
    • 所有整数类型,例如 u32
    • bool
    • char
    • 所有的浮点类型,例如:f64
    • Tuple(元组),如果其所有的字段都是 Copy 的:(i32, i32) 是,而 (i32, String) 不是

# 所有权与函数

将值传递给函数和把值赋值给变量是类似的:

  • 将值传递给函数会发生移动或复制
fn main() {
    let s = String::from("Hello World");
    //s 的值在堆中,值的所有权移动到函数内部了
    // 函数结束后,因为持有所有权,因此就会释放
    // 函数后面就不能再使用 s1 变量了。
    take_ownership(s);
    
    let x = 5;
    // 因为 x 是标量,值在 stack 中;
    //i32 实现了 Copy trait
    // 传入函数内部发生复制,因此函数后面 x 还能使用
    makes_copy(x);
    
    println!("{}", x);
}
fn take_ownership(some_string: String) {
    //some_string 获得所有权
    println!("{}", some_string);
} // 函数结束,释放 some_string 堆上的内存
fn makes_copy(some_number: i32) {
    //some_number 通过复制获得新的拷贝值
    println!("{}", some_number);
} // 函数结束,some_number 在 stack 内存上自动释放
  // 因为是复制的,不会影响外部的变量

# 返回值与作用域

函数的返回值的过程中同样也会发生所有权的转移

fn main() {
    // 从函数返回值中获得所有权
    let s1 = gives_ownership();
    
    let s2 = String::from("hello");
    
    // 传入 s2 变量,所有权移动到函数内部
    // 函数直接返回,再次将所有权移动到函数外面的 s3
    let s3 = takes_and_gives_back(s2);
    
    // 后面不能使用 s2 变量,因为所有权移动到 s3 上了
    //s2 -> a_string(函数入参) -> s3
}
fn gives_ownership() -> String {
    let some_string = String::from("hello");
    some_string
} // 返回值移动所有权到函数调用者
fn takes_and_gives_back(a_string: String) -> String {
    a_string
} // 传入参数获得所有权,直接返回,返回值移动所有权到函数调用者

# 如何让函数使用某个值,但不获取其所有权?引用

fn main() {
    let s1 = String::from("hello");
    
    // 为了获取字符串的长度,需要传入变量
    // 但这样传入发生所有权的移动,而且发生 2 次移动
    // 有点啰嗦和影响性能
    let (s2, len) = calculate_length(s1);
    
    println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

函数传入参数,又返回此参数;

所有权移动了 2 次,但我们可能只想使用,但不需要获取其所有权,因此引入了引用(Reference)和借用(Borrow)的概念。

需要区分所有权、引用 / 借用的概念,这 2 个概念容易混淆,但不是同一个东西。

引用 / 借用不会改变所有权,函数结束后,也不会释放变量(没所有权)。

# 引用与借用

fn main() {
    let s = String::from("hello");
    
    // &s 这里是引用
    let length = calculate_length(&s);
    
    println!("The length of '{}' is {}.", s, length);
}
// 这里 s 是借用;只有借用关系,不会发生所有权的移动
fn calculate_length(s: &String) -> usize {
    s.len()
}

引用和借用是同一个东西,只不过发生的位置不同,传入参数就是引用的概念;函数参数定义就是借用的概念。

# 借用

和变量一样,引用默认也是不可变的。

如果借用一个值,需要修改,则需要定义为可变引用,变量也要定义为可变。

fn main() {
    // 变量需要定义为可变
    let mut s = String::from("hello");
    // 引用需要 &mut 可变引用
    let length = calculate_length(&mut s);
    
    println!("The length of '{}' is {}.", s, length);
}
// 函数参数需要定义为可变借用
fn calculate_length(s: &mut String) -> usize {
    s.push_str(" World");
    s.len()
}

# 可变引用

  1. 可变引用有一个重要的限制:在特定作用域内,对某一块数据,只能有一个可变的引用。
  2. 这样做的好处是可在编译时防止数据竞争。

以下三种行为下(同时存在)会发生数据竞争:

  • 两个或多个指针同时访问同一个数据
  • 至少有一个指针用于写入数据
  • 没有使用任何机制来同步对数据的访问
fn main() {
    let mut s = String::from("hello");
    let s1 = &mut s;
    let s2 = &mut s;
    //s1 和 s2 会发生数据竞争,因此编译不通过
    println!("{} {}", s1, s2);
}

可以通过创建新的作用域,来允许非同时的创建多个可变引用

fn main() {
    let mut s = String::from("hello");
    {
        let s1 = &mut s;
    }
    let s2 = &mut s;
}

# 另外一个限制

  1. 不可以同时拥有一个可变引用和一个不可变引用
  2. 多个不变引用是可以的

可变引用会修改数据,如果没有同步操作,不可变引用的值可能被改变导致数据不一致;因此 rust 限制这种情况。

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    // 不能同时有可变和不可变的引用
    let s1 = &mut s;
    println!("{} {} {}", r1, r2, s1);
}

# 悬空引用 Dangling References

  1. 悬空指针(Dangling Pointer): 一个指针引用了内存中的某一个地址,而这块内存可能已经释放并分配给其它人使用了。

  2. Rust 中,编译器可以保证引用永远都不是悬空引用。

  3. 在 rust 中,只有这种情况可能发生悬空引用:

    • 函数内部创建一个对象,然后仅仅返回其引用;然而函数结束后;由于函数内持有所有权,结束会释放内存,但返回引用给调用者;这样就发生悬空引用。
    • rust 不会允许这种情况发生,需要指定生命周期,扩大对象的作用域范围。
fn main() {
    let r = dangle();
}
fn dangle() -> &String {
    let s = String::from(hello"");
    &s
}

# 引用的规则

  • 在任何给定的时刻,只能满足下列条件之一:
    1. 只有一个可变的引用
    2. 或者只能任意数量的不可变的引用
  • 引用必须一直有效

# 切片

rust 的另外一种不持有所有权的数据类型:切片 (slice)

以下程序可能有什么问题?

fn main() {
    let mut s = String::from("Hello World");
    let word_index = first_word(&s);
    // 如果 s 的内存被清空了,但还有 index 在;
    // 后面使用 index 从 s 中获取内容,
    // 就导致 bug,例如下面这句;
    s.clear();
    println!("{}", word_index);
}
fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }
    s.len()
}

# 字符串切片

字符串切片是指向字符串中一部分的内容引用

注意需要 & 符号,因为是一个引用,类型是 &str。

形式:& 变量 [开始索引.. 结束索引],不包含结束索引

注意:字符串切片范围索引要是有效的 utf-8 字符边界内,否则程序会报错退出。

fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];   // 语法糖:&s [..5];
    let world = &s[6..11];  // 语法糖:&s [6..]; 或 &s [6..s.len ()];
    let helloworld = &s[..]; // 语法糖
}

# 使用字符串切片重写例子

fn main() {
    let mut s = String::from("Hello World");
    let word_index = first_word(&s);
    // 这里由于所有权会报错,因此 rust 机制保证安全,
    // 防止内存释放后,还使用相关内容
    s.clear();
    println!("{}", word_index);
}
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i];
        }
    }
    &s[..]
}

# 字符串字面值是切片

  • 字符串字面值直接被存储在二进制程序中。
  • 变量 s 的类型是 &str,它是一个指向二进制程序特点位置的切片。
  • 字面值不可变。
fn main() {
    let s = "Hello World";  // 相当于 &str 类型
    println!("{}", s);
}

# 将字符串切片作为参数传递

建议函数入参不使用 &String,更通用的做法是使用切片作为入参。

fn main() {
    let mut s = String::from("Hello World");
    let word_index = first_word(&s[..]);
    
    println!("{}", word_index);
    
    // 通用
    let word_index = first_word("Hello World");
    println!("{}", word_index);
}
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i];
        }
    }
    s
}

# 其它类型的切片

例如数组 i32 的切片,类型是 &[i32]

fn main() {
    // 类型 &[i32]
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3];
    println!("{} {}", slice[0], slice[1]);
}
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Cecil 微信支付

微信支付

Cecil 支付宝

支付宝

Cecil PayPal

PayPal