Rust New Concept

August 30, 2019

可变性 mut

默认变量都是不可变的, 用 mut 声明为可变

  let mut x = 5;
  println!("The value of x is: {}", x);
  x = 6;
  println!("The value of x is: {}", x);

依然存在常量(constants), 常量相比不可变变量的区别在于, 可在包括全局作用域的任何作用域中声明, 声明时必须指定数据类型, 不能让 rust 自行推导:

const MAX_POINTS: u32 = 100_000;

想要通过引用修改变量得值时候, 要声明为可变引用:

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

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

本身变量就要是 mut, 引用也要是 &mut, 开两层锁才能改

隐藏 shadowing

类似于同名变量重复定义, 后定义的变量名最夺取前一个变量名

let spaces = "   ";
let spaces = spaces.len();

最大的好处是少想几个变量名, 坏处是降低了可读性. 很长一段代码前面的 spaces 和后面的 spaces 可能是完全两个东西.

数据类型 data type

rust 是静态类型 statically typed 语言, 通过类型注解来声明类型:

let guess: u32 = "42".parse().expect("Not a number!");

parse 时,无法推导出类型时, 就要明确的写出注解. 可以认为只是平时能推导时省略了注解, 类似这种情况以及声明 const 时候, 又要明确的写出来.

rust 来省略这点, 导致了不一致性, 觉得没有太大必要.

idea 在编写 rust 时会把类型注解显示出来, 一开始觉得杂乱, 后来觉得很好.

Screen Shot 2019-08-30 at 2.53.53 PM.jpg

另一个必须声明类型的地方就是函数的参数

表达式 expression-based

rust 是基于表达式的语言.

语句(Statements)是执行一些操作但不返回值的指令, 表达式(Expressions)计算并产生一个值.

  • 数值是一个表达式
  • 函数调用是一个表达式
  • 调用是一个表达式
  • 代码块 {},也是一个表达式
  • if 以及 loop 控制流也是表达式(while 和 for 其实也是)

表达式默认自带返回属性, 没有定义返回值时, 也会返回一个 () 或者 {}

赋值是一个语句, 不是表达式. 类似 let x = (let y = 6) x = y = 6 在 rust 是非法的, 这非常好, 受够了 javascript 的肆无忌惮了.

函数指定了返回值类型, 又没有返回值时候:

fn plus_one(x: i32) -> i32 {
    x + 1;
}

触发的错误是 mismatched types, 因为函数作为表达式默认返回的 () 和声明的 i32 不一致, 明白了表达式对这个错误就不会困惑了.

返回值类型必须确定并且一致, 如果 if 表达式根据不同的条件返回不同数据类型的值, 是不允许的. 因为这样无法在编译阶段确定数据类型, 再次明确: rust 是静态类型 statically typed 语言

loop 的返回值在 break 的时候带回去:

  let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

while 和 for 也是表达式, 但是只能返回 (), 讨论在这里 https://github.com/rust-lang/rfcs/issues/1767#issuecomment-292678002

注释 comments

没有专门的多行注释

// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.

推荐注释单独做一行, 放在结尾可以但是不清晰.

控制流

条件判断类型

条件判断类型必须是 bool

    let number = 3;

    if number {
        println!("number was three");
    }

这个是会报错的, 很好, 受够了 javascript 的肆无忌惮了.

match

match 是一个强大的控制流表达式, 通常与枚举 enum 配合使用

match 概念上比较像其他语言的 switch case

match 可以根据一个已实例化的 enum, 根据其实际的 variants 来做一些不同的事:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

=> 用来分割模式和要执行的代码, 别和 javascript 的箭头函数搞混了.

想要从 enum 的 variants 中把数据抽取出来, 也要用到模式匹配

#[derive(Debug)]
enum IpAddrKind {
    V4,
    V6(String),
}

impl IpAddrKind {
    fn print(&self) {
        match self {
            IpAddrKind::V4 => println!("{:#?}", self),
            IpAddrKind::V6(d) => println!("{}", d)
        }
    }
}

fn main() {
    let mut four = IpAddrKind::V4;
    four.print();

    four = IpAddrKind::V6("bigzhu kao".to_string());
    four.print();
}

有初始化参数的 variants 匹配条件那里也需要填入一个变量名, 具体的数据值会被填充到这个变量 IpAddrKind::V6(d), 并且这里的变量会被暴露到后续的执行代码段中, 抽取完成.

match 必须覆盖每一种匹配模式:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

编译器明确知道 Option enum 还有个 None, 这个找不到对应的匹配, 于是就报错. 匹配模式如果是 u8 , 岂不是要写 255 个匹配模式, 好在有 _ 类似 else 匹配所有其他的模式:

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

值匹配一个模式时候 match 和 _ 还有一个语法糖 if let else 来简化代码段:

let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}

等于

let mut count = 0;
match coin {
    Coin::Quarter(state) => println!("State quarter from {:?}!", state),
    _ => count += 1,
}

if let 也是另外一个抽取 enum variants 值的办法?

作用域

代码块 {} 界定了作用域, 这个很好理解

变量声明到最后一次使用也是作用域边界:

    let mut s = String::from("hello");
    let r1 = &mut s;
    println!("{}", r1);
    let r2 = &mut s;
    println!("{}", r2);
    let r3 = &mut s;

如果中间没有那几次使用, 是不允许在定义可变引用的, 正是看似无聊的 print 最后使用了变量, 从而宣告了变量得作用域的结束.

所有权以及可变引用的使用规则上和作用域紧密关联:

  • 同一作用域只有一个拥有所有权的变量
  • 同一作用域只有一个可变引用, 不可变引用与可变引用不能在同一作用域共存

所有权 ownership

类似 c 的引用计数指针, 只是计数器只能是 0 或者 1

只有堆(Heap)数据需要关心所有权, 本质上所有权是指向堆数据的指针, rust 只允许一个(计数器 1)变量存储指向堆的指针.

下面是之前写的, 好像理解的还是比较透彻的, 搬过来好了:

以前弄 c 还要自己构建引用计数指针来清除不用的变量.

rust 想了一个更优雅的方式, 相当于引用计数永远是 1 或者 0, 一旦变量离开作用域就清零回收了

所有堆存储的数据类型的变量, 其实都是指向那个堆的指针.

rust 设定”引用计数只有 1”, 那么两个变量都指向同一个引用, 假如都释放时候, 岂不是要对一个堆进行两次释放了?

rust 的处理更巧妙, 一个堆存储的变量, 给一个另一个变量负值的时候, 自身就失效了, 丧失了对堆的有效引用. 这个可以被称作所有权的转移. 所以下面的代码会报错.

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

随着所有权的转移, 永远保证只有一个有效的堆引用, 简直优雅得不得了.

变量传递给函数也同样丧失了所有权.

    let s2 = String::from("hello");     // s2 进入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移动到

函数结束时候, 如果没有返回, 那么这个引用计数相当于 0, 会导致堆资源释放. 如果想继续处理, 那么得把变量返回, 外部用另外一个变量来接收所有权.

引用 references

这样的代码未免太不优雅了. 得有一种能告知函数参数的值, 又不把所有权移交给函数的方式: 引用

引用可以理解为指向指针的指针:

trpl04-05.svg

let s1 = String::from("hello");

let len = calculate_length(&s1);

把这样的值送入函数, 称之为借用(borrowing), 所有权没有转移, 函数也完全无权限修改. 这个方式和很多语言相反的, 比如 go, 向函数传递指针, 函数的修改才能有效的影响外部的变量.

要在函数内修改传入值的时, 需要使用可变引用.

首先变量得是 mut 的, borrowing 时也声明为可变.

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

    change(&mut s);

同一个作用域内可变引用只能有一个, 毕竟一个东西不能借两次嘛

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

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

这个代码会报错, 可见 rust 谨慎的保持唯一性, 极力避免数据竞争(data race)

  • 不同作用域可以各有可变引用
  • 只读引用可以有无数个
  • 同一作用域不能同时有可变引用和只读引用

引用的作用域有两种结束方式:

  1. 离开代码块
  2. 最后一次使用

    let mut s = String::from("hello");
    
    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    println!("{} and {}", r1, r2);
    // 此位置之后 r1 和 r2 不再使用
    
    let r3 = &mut s; // 没问题
    println!("{}", r3);
    

悬垂引用(Dangling References) 其实就是 c 中野指针的概念, rust 编译器会做检查来避免出现悬垂引用

字符串 slice 结构上就是指向字符串对应两个位置的引用, 所以天然不具备所有权.

这个报错的代码要注意

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

    let word = first_word(&s);

    s.clear(); // error!

    println!("the first word is: {}", word);
}

clear 操作本质上是获取一个可变引用, 这个情况下和只读引用 word 在同一个作用域, 因为引用和作用域的规则, 编译报错了; 规则天然就保证了不会出现使用一个被 clear 了的 slice 的情况. 优雅.

结构体 struct

可以定义常见带字段名的结构体.

参数名和字段名一直, 可以简写 k:v 赋值

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

可以继承同类型结构体实例的字段

let user2 = User {
    email: String::from("[email protected]"),
    username: String::from("anotherusername567"),
    ..user1
};

可以定义没有字段名的元组结构体

    struct Color(i32, i32, String);
    let black = Color(0, 0, "big".to_string());

还可以定义没有任何字段的 类单元结构体(unit-like structs)

结构体可以存储引用, 这时需要指定生命周期.

debug 时候会想打印结构体出来看看, 需要做的事情也挺多的

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!("rect1 is {:#?}", rect1);
}
  1. 结构体要加入显示的开关 #[derive(Debug)] 来允许打印
  2. 占位符还要指定使用Debug的输出模式: {:?} 或者 {:#?}

可以在结构体中用关键字 impl 定义函数, rust 中叫做方法语法和关联函数

方法语法对应实例函数

关联函数对应静态函数(类方法)

rust 的 struct 更趋近于 Java 或者 Python 中的 Class 的概念, 而不是 golang 中的单纯的 struct

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
    fn area_static() -> u32 {
        1000 * 1000
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels. {}",
        rect1.area(),
        Rectangle::area_static()
    );
}

枚举 enum

和其他语言弱到没存在感的枚举不同, rust 的枚举强大而且核心.

enum IpAddrKind {
    V4,
    V6,
}

枚举可以定义不同的成员(variants), 这和 struct 的字段名看起来像, 但完全是两个概念

可以指定 variants 的数据类型:

enum IpAddr {
    V4(String),
    V6(i32),
}

而枚举的实例化其实是实例化 variants, variants 的实例化由自身的类型决定.

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

存储的不同数据类型的 enum 却被看做同一个类型, 这里无论是用 V4 还是 V6 实例化, 都是得到 IpAddr 类型, 可以放入同一个 mut 变量中.

和 struct 一样可以用 impl 来定义方法

Some 和 None 以及 Option

Option<T> 是一个标准的枚举类型, 枚举类型的 variants 就是 Some 和 None

enum Option<T> {
    Some(T),
    None,
}

唯一的不同在于这货太常用, 所以使用Option 时候不需要引入作用域, 成员的使用也不需要 Option 前缀.

let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;

这里的 None 就是 rust 中的空值, 用泛型枚举来定义None, 既保存了数据类型信息, 以及可能为空的数值, 又和原本的数据类型变量区分开来, 避免预计不为空而实际为空的情况.

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

这里就是一个允许参数为None 的函数 plus_one, 结合 enum match 强制安全的实现 None 及 非 None 的不同处理情况

trait


comments powered by Disqus