快速掌握Rust(需要C++基础,不多废话)

436 阅读8分钟

本文内容摘自《Rust 程序设计语言》

Hello World

fn main() {
    println!("Hello, world!");
}

使用rustc main.rs编译。

!意味着使用一个宏,而非一个函数。

Cargo

用途一:用于构建项目。

cargo new hello构建一个名为hello的项目。

cargo check编译检查,cargo build编译当前项目,cargo run编译并运行当前项目。

cargo clean清理目标文件。

用途二:用于管理依赖。

rand是一个rust crate,可以理解为库、代码包,都是类似的概念。

首先需要在Cargo.toml中引入依赖。

[dependencies]
rand = "0.9.0"

[source.crates-io]
replace-with = 'tuna'

[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

cargo build时会自动从crates.io上获取依赖,国内网络最好还是换个源。

Applets

通过一个猜数字的小程序来快速上手Rust语言。

use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");
    let randnum = rand::thread_rng().gen_range(1..101);

    println!("Please input your guess.");

    let mut guess = String::new();
    io::stdin().read_line(&mut guess).expect("Failed to read line.");
    let guess: u32 = guess.trim().parse().expect("Please type a number.");
    
    println!("You guessed: {}, actually: {}.", guess, randnum);
}

let mut i = 0;可以理解为int i = 0;let i = 0;可以理解为int const i = 0;

fun(&mut i);类似fun(int const& i);

read_line()parse()返回一个enum Result {Ok, Err}expect()在返回Err后会终止程序并报错,这可比if err != nil优雅多了。

let guess: u32显示定义了一个类型为u32的新变量,Rust允许使用相同的变量名,尤其是在类型转换这种场景下。

trim()终于去除字符串前后的空白字符,parse()用于解析字符串为数值。

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");
    let randnum = rand::thread_rng().gen_range(1..101);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();
        io::stdin().read_line(&mut guess).expect("Failed to read line.");

        let guess:u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };
        
        match guess.cmp(&randnum) {
            Ordering::Less => println!("Too small."),
            Ordering::Greater => println!("Too big."),
            Ordering::Equal => {
                println!("You win.");
                break;
            }
        }
    }
}

loop这里是个无限循环,通过break跳出。

match会执行匹配的那一项,需要cover所有可能。这里的cmp()parse()都返回一个枚举值。

Const

let i = 0;let mut i = 0;容易让人理解为常量和变量,但实际上这两者都是变量,Rust中通过const i: u32 = 0;定义一个常量。

let i = 0;
let i = 1;
let mut i = 2;
let i = "3";

区别在于const必须指定类型,且不能使用相同的名字,类似上面这样。

Type

整型

i8i16i32i64i128isizeu8u16u32u64u128isize

数值不论机器默认类型i32,其中isizeusize在32位机上是32位,64位机上是64位。

浮点型

f32f64

布尔型

bool

字符型

char

Rust中的字符型是4个字节,采用unicode编码,可以表示更多的字符,u8字面值则需要b'A'这样表示。

元组

let tup: (i32, f32, u32) = (18, 1.0, 36);
let x = tup.0;
let y = tup.1;
let z = tup.2;
let (x, y, z) = tup;

数组

let arr: [i32; 5] = [0, 0, 0, 0, 0];
let arr: [0; 5];
let first = arr[0];

数组所有元素是相同类型,这是和元组的区别。

Function

语句是有分号结尾的,而表达式没有。

let i = {
    let i = 0;
    i + 1
};

i + 1让代码块有了返回值,但如果i + 1;则编译不通过。

无返回值的函数

fn foo(i: i32, j: i64) {
    println!("{} {}", i, j);
}

fn main() {
    foo(32, 64);
}

有返回值的函数


fn foo(i: i32, j: i32) -> i32 {
    println!("{} + {}", i, j);
    i + j
}

fn lot() -> (i32, f32) {
    (1, 1.0)
}

fn main() {
    println!("{} {} {}", foo(1, 2), lot().0, lot().1);
}

Control

if else

let i = if true {
    1
} else if false {
    2
} else {
    3
};

loop

上面提过了,死循环,这里可以通过break让loop返回一个值。

let result = loop {
    break counter * 2;
};

while

和go里的while一样。

while true {}

for

let a = [10, 20, 30, 40, 50];

for element in a.iter().rev() {
    println!("the value is: {}", element);
}

for i in (0..5).rev() {
    println!("the value is: {}", a[i]);
}

这里rev()反向遍历,去掉就是正向了。

Ownership

不同于使用GC或手动管理内存,Rust通过所有权系统管理内存。

Rust中每一个值,在任意时刻,有且仅有一个Owner,因此Rust中没有浅拷贝的概念。

{
    let s = String::from("hello");  // s 生效
}                                   // s 失效

离开作用域时会自动调用drop()清理堆内存。

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

上面的代码既不是浅拷贝,也不是深拷贝,而是移动,所有权转交给s2,s1失效。

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

深拷贝语义需要显示指定,clone()应该谨慎调用,耗费资源。

上面所说不论是move还是clone都是指堆数据,栈数据都是copy语义。

fn main() {
    let s = String::from("hello");
    takes_ownership(s);             // s 所有权转移

    // println!("{}", s);           // s 失效
}                                   // s 失效因此不会drop

fn takes_ownership(s: String) {
    println!("{}", s);
}                                   // drop释放堆内存

Reference

fn main() {
    let s = String::from("hello");
    takes_ownership(&s);
    println!("{}", s);              // s 依旧有效
}                                   // s drop

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

引用允许使用值但不发生所有权的转移,获取引用的操作称为借用。

上面代码中借用来的s是不可变的,如需修改使用可变引用即可。

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

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

需要注意的是,Rust中一个作用域内不允许相同数据的两次可变借用,也不允许一个作用域内相同数据的借用和可变借用共存,有点类似读写锁的意思了,下面两段代码都不行。

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

let r1 = &mut s;
let r2 = &mut s;    // 不行

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

let r1 = &s;     // 可以
let r2 = &s;     // 可以
let r3 = &mut s; // 不行

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

需要注意引用的作用域从声明开始,到最后一次使用终止,而不是代码块,如果上面代码中没有println!使用引用,那么引用的作用于就只在声明那一行。

Rust在编译层面不允许悬垂引用的发生。

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");
    &s    // 不行
}

Slice

Slice允许引用集合中的连续一段元素,也是引用,是不需要所有权的。

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

let hello = &s[..5];
let world = &s[6..];

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

字符串Slice的类型为&str,下面代码用于获取第一个单词。

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

    let hello = first_word(&s);

    // s.clear(); // 不行

    println!("{}", hello);
}

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

Rust保证Slice有效,因此s.clear()是不被允许的,原理是需要获取一个可变引用。

字符串字面值也是一个Slice,字符串的操作可以更简便了。

fn main() {
    let s = "hello world";

    let hello = first_word(s);
    let hello = first_word("hello world");

    println!("{}", hello);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

Struct

fn main() {
    let mut user = User {
        username: String::from("admin"),
        password: String::from("12345"),
    };

    user.password = String::from("54321");
}

struct User {
    username: String,
    password: String,
}

let mut user设置user整体可变,不能单独设置某个字段可变。

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

元组结构体可以给元组一个名字,black和origin不是一个类型。

结构体支持定义方法,create在这里就类似静态方法,Rust称为关联函数。

fn main() {
    let mut user = User::create("admin", "12345");

    user.modify_password("54321");

    println!("{:#?}", user);
}

#[derive(Debug)]
struct User {
    username: String,
    password: String,
}

impl User {
    fn modify_password(&mut self, new_password: &str) {
        self.password = String::from(new_password);
    }

    fn create(username: &str, password: &str) -> User {
        User {
            username: String::from(username),
            password: String::from(password),
        }
    }
}

想直接输出一个结构体需要重载Display方法,但也可以把结构体定义为Debug模式,通过{:?}{:#?}来输出。

Enum

应用场景是某一样事物其类型是有限个的,比如IP地址只有IPv4和IPv6两种类型。

枚举的精髓在于其成员都是相同类型,这里有点多态的意思了。

enum Message {
    Quit,
    Move{x: i32, y: i32},
    Write(String),
    ChangeColor(i32, i32, i32),
}

impl Message {
    fn call(&self) {}
}

fn foo(m: &Message) {}

fn main() {
    let m = Message::Write(String::from("hello"));
    foo(&m);
    m.call();
}

Rust中没有null的概念,如果需要一个对象为空,要使用Option枚举类型。

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

上文提到过match,用于匹配枚举成员,我们可以使用match从枚举中取值。

impl Message {
    fn call(&self) {
        match self {
            Message::Quit => println!("quit"),
            Message::Move{x, y} => println!("{} {}", x, y),
            Message::Write(s) => println!("{}", s),
            Message::Color(r, g, b) => println!("{} {} {}", r, g, b),
        }
    }
}

match需要cover所有可能,那对于非枚举类型怎么办呢?可以使用_通配。

match value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

如果只匹配一种可能可以使用if let,下面两种方式等价。

match opt {
    Some(3) => println!("three"),
    _ => (),
}

if let Some(3) = opt {
    println!("three");
}

if let工作方式和match一致,也可以获取枚举成员值,也可以和else联合使用。

if let Message::Write(s) = m {
    println!("{}", s);
} else {
    println!("not write");
}

Package

一个package有n个crate(bin),至多1个crate(lib),至多1个cargo.toml。cargo.toml用于描述如何构建crate,package中最少有一个crate,无论是crate(bin)或crate(lib)。

src/main.rs是crate(bin) root,这个文件是必须有的;src/lib.rs是crate(lib) root,这个文件可以没有。这两个crate root名字都和package相同。src/bin/*.rs用于构建crate(bin)。

一个crate可以有n个module,按照一棵树去组织。

mod mod1 {
    pub mod mod1_1 {
        pub fn test() {}
    }
}

mod mod2 {
    pub fn test() {
        super::mod1::mod1_1::test();
    }
}

pub fn test() {
    crate::mod2::test();
    mod2::test();
}

和C++类似,也可以使用use、use as关键字来导入,pub use可以让使用者不需要导入就可以使用。

vector

fn main() {
    let v: Vec<i32> = Vec::new();
    let mut v = vec![1, 2, 3];

    v.push(4);

    let third: &i32 = &v[2];
    println!("{}", third);

    match v.get(0) {
        Some(&i) => println!("{}", i),
        None => println!("none"),
    }

    for i in &mut v {
        *i += 1;
    }

    for i in &v {
        println!("{}", i)
    }
}