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 时会把类型注解显示出来, 一开始觉得杂乱, 后来觉得很好.

另一个必须声明类型的地方就是函数的参数
表达式 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
这样的代码未免太不优雅了. 得有一种能告知函数参数的值, 又不把所有权移交给函数的方式: 引用
引用可以理解为指向指针的指针:
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)
- 不同作用域可以各有可变引用
- 只读引用可以有无数个
- 同一作用域不能同时有可变引用和只读引用
引用的作用域有两种结束方式:
- 离开代码块
- 最后一次使用
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);
}
- 结构体要加入显示的开关
#[derive(Debug)]
来允许打印 - 占位符还要指定使用
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 的不同处理情况