Rust入门笔记 2.函数、所有权与复合类型

yyi
yyi
2023-08-11 / 0 评论 / 199 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2023年08月11日,已超过445天没有更新,若内容或图片失效,请留言反馈。

Rust入门笔记 —— 函数,所有权与复合类型

1 结构体与枚举

1.1 结构体

先讲结构体是因为后面方法需要结构体做支撑,其实这种用例子解释语法的方式,还是需要学过其他语言。因为前期肯定会用到后面才讲的东西

结构体主要有三种类型

  • tuple : struct Pair(i32, f32)
  • 经典的 C struct,当然,结构体也是可以复合的

    struct Point {
      x: f32,
      y: f32
    }
  • 单元(Unit)结构体:没有字段

结构体和 C 一样,用.+field_name引用变量

let point: Point = Point{ x: 10.3, y: 0.4 }; // 这里的 x 和 y 并不能省略
println!("coord : {}, {}", point.x, point.y);
let point_ext: Point = Point{ x: 1.2, ..point}; // 使用这种方法继承字段
let pair = Pair(1, 0.1) // 元组类型的就可以省略啦(不省略也没名字)

1.2 枚举

用 enum 关键字声明一个枚举,相比与 C,rust 的枚举有更多功能,任何对于结构体合法的内容也可以用于 enum

enum WebEvent {
  PageLoad,
  PageUnload,
  // Unit
  KeyPress(char),
  Paste(String),
  // Tuple
  Click { x: i64, y: i64},
  // 标准的结构
}
// 使用这种 match 结构就可以引用枚举字段及其内容
fn inspect(event: WebEvent) {
    match event {
        WebEvent::PageLoad => println!("page loaded"),
        WebEvent::PageUnload => println!("page unloaded"),
        // Destructure `c` from inside the `enum` variant.
        WebEvent::KeyPress(c) => println!("pressed '{}'.", c),
        WebEvent::Paste(s) => println!("pasted \"{}\".", s),
        // Destructure `Click` into `x` and `y`.
        WebEvent::Click { x, y } => {
            println!("clicked at x={}, y={}.", x, y);
        },
    }
}

2 函数

2.1 函数与返回值

Rust的函数以 fn fn_name(arg_list) -> ret_type {body} 定义,函数的最后一行是返回值

Rust函数名称风格是snake_case(区别于Go的CamelCase)

Rust不要求被调用的函数必须在调用者之前定义,只要有定义即可

Rust的函数返回值可以是函数体尾部的表达式,也可以显式的使用return语句

返回()类型的函数,返回值类型声明可以省略

fn main() {
    // 不要修改下面两行代码!
    let (x, y) = (1, 2);
    let s = sum(x, y);

    assert_eq!(s, 3);
}

fn sum(x : i32, y:i32) -> i32 { // original : (x, y:i32) {
    return x + y; // original : x + y;
}

上述答案中第10行可以是 x + y,可以是return x + y;,但是不能是 x + y; 因为分号会让表达式变为语句,语句的值是()

// 用两种方法求解
fn main() {
    never_return();
}

fn never_return() -> ! {
    // 实现这个函数,不要修改函数签名!
}

这题没有想到答案,两种方法分别可以是

fn never_return() -> ! {
    panic!("I return nothing!")
}
fn never_return() -> ! {
    loop{}
}

其实感觉有点扯淡

还有类似于成员函数的概念:把函数和类型加以关联,Rust 中用关联函数和方法实现,我门后面再讲

2.2 函数体表达式

我斗胆猜测这就是 Rust 中匿名函数的表现,Rust 使用大括号来编写一个较为复杂的表达式

let y = {
  let x = 3;
  x + 1
};
// y == 4

可以看到,大括号中的内容和函数中的内容一致,但是函数体表达式不能使用 return

3 所有权、引用和借用

C 使用手动管理内存,开发者申请释放,这常常造成资源浪费和内存泄漏。

Java 的 JVM 会负责在运行时回收内存,然而 GC 会导致运行效率的下降。

Rust 采用所有权机制,让它在编译阶段能更有效的分析内存资源。

  • Rust 中的每个值都有一个变量,称为其所有者
  • 一个值同时只能有一个所有者
  • 当所有者不在程序运行范围内时,该值会被删除

3.1 变量范围

{    
  // 无效,未声明
  let s = "str";
  // 可用
}
// 无效

上面这段代码描述了 s 这个变量的作用范围,它代表变量的可行域,默认从声明变量开始有效,直到变量域结束。

变量和数据的交互方式主要有移动 Move 和克隆 Clone

3.2 所有权

移动与克隆

移动,即直接复制,对于基本数据类型(bool,整形、浮点等)Rust 会在栈上复制一份。

let x = 5;
let y = x;

在这样的操作后,栈上会有两个 5

但是如果数据在堆上,只会复制指针,类似于浅拷贝。

let s1 = String::from("str");
let s2 = s1;
println!("{}", s1); // Wrong! borrow of moved value: `s1`

当 s2 被绑定到堆上的 "str" 时,s1 就已经失效

为了降低程序运行成本,默认的情况下非基本类型采用移动,但是如果需要复制一份数据,需要使用克隆

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

此时堆中会有两个 str,分别绑定给 s1 和 s2,释放的时候也会分别释放。

对于练习:

fn main() {
    // 使用尽可能多的方法来通过编译
    let x = String::from("hello, world");
    let y = x;
    println!("{},{}",x,y);
}
// 显然,编译会因为 x 失效而失败
// Sol1 :
let y = x.clone();
// Sol2 :
let x = "hello, world"; // 在栈上,会移动

所有权转移的时候,可变性也会跟着改变

fn main() {
  let s = String::from("hello");
  let mut s1 = s;
  s1.push_str(" world");
  println!("{}", s1);
}

涉及函数的情况

调用函数传入的参数,其所有权会被传递给 callee,因此会失效。但是基本类型不会失效,道理和上面一样。

换句话说,传参和移动的效果是一样的。

// 不要修改 main 中的代码
fn main() {
    let s1 = String::from("hello, world");
    let s2 = take_ownership(s1);
        // s1 在此处失效
    println!("{}", s2);
}

// 只能修改下面的代码!
fn take_ownership(s: String) {
    println!("{}", s);
}
// Sol1
fn take_ownership(s: String) -> String {
    println!("{}", s);
      s
}

对于返回值,其所有权会被移动出函数,返回到调用函数的地方,不会被释放

fn main() {
    let s = give_ownership();
    println!("{}", s);
}

// 只能修改下面的代码!
fn give_ownership() -> String {
    let s = String::from("hello, world");
    // 将 String 转换成 Vec 类型
    let _s = s.into_bytes();
    s
}
// sol1 :
let _s = s.clone().into_bytes();
// sol2 :
let _s = s.as_bytes();

上面这道题中,s 的所有权在into_bytes方法中被移动,因此不能在返回处使用。但是修改之后,s 的所有权被移交回 main,所以 main 中可以使用。

引用和租借

引用类似于取指针,和 C++ 里面的引用感觉差不多

fn main() {
    let s = String::from("hello, world");
    print_str(&s);
      // print_str(s);
    println!("{}", s);
}

fn print_str(s: &String) {
// fn print_str(s: String) {
    println!("{}",s);
}

在变量的值被引用的时候,变量不会被认定为无效。

引用不会获得值的所有权,只会 租借 值的所有权,未显式声明可变租借的所有权不可以修改数据

引用本身也是一个类型,它存的值其实就是被引用的值的地址。

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    let s3 = s1;
    println!("{}", s2);
}
// 错误,s2 只租借了 s1 的所有权,当 s1 的所有权失效,s2 租借的也失效了
fn main() {
    let s1 = String::from("run");
    let s2 = &s1;
    println!("{}", s2);
    s2.push_str("oob"); // 错误,禁止修改租借的值
    println!("{}", s2);
}
fn main() {
    let mut s1 = String::from("run");
    // s1 是可变的
    let s2 = &mut s1;
    // s2 是可变的引用
    s2.push_str("oob");
    println!("{}", s2);
}

可变引用不允许多重引用,但是不可变引用可以。

0

评论 (0)

取消