Rust代码高亮还没支持,后面会修复这个问题

因为是速通,不能涵盖到语言所有特性,但是保证能写出能跑的代码。详细教程可以看Rust官方出的The Rust Programming Language

阅读建议

有一定的C/C++基础,了解 指针,Struct,OOP的基本概念以及用法。边读边写代码。

包管理

类似于npm, pipenv Rust 用 Cargo 进行包管理,常用指令如下:

新建项目:

cargo new   

运行完后,会在项目文件根目录生成一个TOML文件,用过npm的话应该会很熟悉:

[package]
name = "ProjectName"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

运行项目:

cargo new过后会生成src/main.rsmain.rs 是项目的入口文件,里面会初始化自带一个Hello World程序,现在咱尝试运行它

cargo build # 编译
当然你也能用rustc进行古法编译。具体见文档

编译后的binary在./target/debug/ 目录下。

如果不想这么麻烦,cargo还提供一个指令能一键运行:

cargo run   # 直接运行

如果你项目已经写完了准备发布,可以用cargo build --release来优化编译,缺点是比普通build慢。

Debug:

cargo check # 查bug

这个指令因为跳过了编译成可执行文件的步骤,所以比cargo run/build快,debug的时候用很合适

安装依赖:

这里先介绍一个额外的概念cratecrate是 Rust 源代码文件的集合。crate可以是二进制的,也就是一个可执行文件。crate 也可以是一个库(library),其中包含了用于其他程序的代码,不能单独执行(类似 .so, .dll, .h)。

比如现在我们要装一个生成随机数的library crate:,咱先编辑Cargo.toml加一个依赖:

[dependencies]
rand = "0.8.5"

然后重新用cargo build重新编译项目,done!

更新依赖:

更新项目中的所有依赖:

cargo update

为什么用cargo:

对于网上任意一个rust项目,如果你想在本地运行,现在不需要手动装依赖、解决版本冲突...只需要三个命令直接秒了:

git clone example.org/someproject
cd someproject
cargo build

基本语法

咱写个基本的猜数字游戏(Guessing Game)来快速过一遍基本语法,这个游戏用python写出来长这样:

import random

magic_number = random.randint(1, 10)

while True:
    guess = input("Your Guess: ")
    
    if guess < magic_number:
        print("too small")
    elif guess > magic_number:
        print("too large")
    else:
        print("you win")
        break

接下来我们用Rust实现同样的功能,我们先实现获取用户输入:

use std::io;

fn main() {
    println!("Guess the number!");

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

    let mut guess = String::new();

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

    println!("You guessed: {guess}");
}

相信大家看到如上代码应该不会特别陌生,但有两个地方需要着重说一下:

let mut guess = String::new();  // 定义变量

在Rust中,变量初始情况都是immutable的(类似const),如果想定义变量就得像如上一样加个mut关键字。

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

这段代码通过stdin获取用户输入,并传递给&mut guess这个reference,没错,Rust里的&也表示Reference。

错误处理:

read_line(&mut guess)的返回值是ResultResult是一个enum = {Ok, Err}Ok代表指令成功执行,Err表示出错。除此之外Result里还有一个方法expect,如果 Result 的这个实例是 Ok,expect 将获取 Ok 所保存的返回值并将该值返回给您。在本例中,该值是用户输入中的字节数;如果是Err,程序就会crash然后打印你给expect(msg)方法传的信息。

println!

类似于printf,支持如下语法:

let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);

现在咱的猜谜游戏能接收用户输入了,下一步生成magic number:

let secret_number = rand::thread_rng().gen_range(1..=100);

gen_range 方法接受一个范围表达式(range expression)作为参数,并生成在该范围内的随机数。这里使用的范围表达式类型采用了 start..=end 的形式,包含了下界和上界,因此我们需要指定 1..=100 来请求一个介于 1 和 100 之间的数字。

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

fn main() {
    // --snip--

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

这段代码虽然看着陌生,但其实很好理解。guess.cmp(&secret_number)比较guesssecret_number并返回enum Orderingmatch 类似其他语言的switch,block内讨论三个不同case。

现在感觉都没毛病了,但是如果run代码的话会报错。

22 |     match guess.cmp(&secret_number) {
   |                 --- ^^^^^^^^^^^^^^ expected struct `String`, found integer
   |                 |
   |                 arguments to this function are incorrect

从如上报错信息可以看出是数据类型对不上的原因。Rust是一个强类型语言,guessString,而secret_numberi32既是int32,所以不能放在一起比大小,正确做法是用parseguess进行类型转换

let guess: u32 = guess.trim().parse().expect("Please type a number!");

字符串上的 parse 方法能将一个字符串转换为另一种类型。在这里,我们使用它将一个字符串转换为一个数字类型(因为let guess: u32这里指定了类型)。

现在一个基本的猜数字游戏已经做好了,为了能让玩家能够多次尝试,我们还得写个for循环

    // --snip--

    println!("The secret number is: {secret_number}");

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

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

到这里程序就写完了!接下来开始优化程序,比如处理异常输入:

        // --snip--

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

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        // --snip--

其中Ok(num)/Err(_)类似于解包操作,获取enum里面带的message,Err(_)_和Python一样,代表这个解包的数据不重要。

最终程序

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

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    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,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

Ownership

说到Rust,大家想到的第一个词应该改是safety,但为啥它很安全呢,原因就是因为它Ownership概念引入,先来看定义:

Ownership is a set of rules that govern how a Rust program manages memory

Ownership 是一系列用于Rust做内存管理的规则

C里面,我们用malloc/calloc/realloc等命令声明内存,当内存不在被使用时,我们需要手动free掉。如果不free就会内存溢出。同样在管理heap上的数据结构(比如实现一个linklist),稍有不注意就会segfault... 为了解决这个问题,Rust引入了Ownership这个概念,让内存错误不再是个问题。

References and Borrowing

先来看代码:

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

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

没错,这段代码和C/C++很像,用&s1表示变量s1的reference。在Rust里,每当你给方法传递参数,参数就属于(owned by)这个方法了,当方法运行结束后就会自动把这个参数drop掉。所以如果你这么写的话:

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

println!("{s1}");

fn calculate_length(s: String) {
    // do something
}

程序就会报错,为什么呢,因为let len = calculate_length(s1)已经把s1的ownership给了calculate_length这个方法,当这个方法运行结束后,s1就会被自动回收,这个时候如果再println!("{s1}")就会找不到s1这个变量。

让我们回到最开始的代码,如果我们用reference的话calculate_length(&s1);就会没上述问题,因为Rust管这叫借(borrow),当方法去一个变量的话就不会take ownership了。

是reference,但又不完全是:

不同于C,Rust里:传入方法的reference在普通情况下不能被方法修改,比如如下代码就会报错:

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

    change(&s);
}

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

为什么说是在普通情况下呢,因为就像变量能被定义为mut,reference也可以被定义为可变的:

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

    change(&mut s);
}

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

在上述代码中,我们除了把s变成mut,还把reference也变成&mut s。这个时候传入change就能更改s的值了。

mut reference的限制:

对于mutable reference,Rust对其有一个限制:对于一个变量,如果你已经有一个关于它的mut reference,在同一个作用域(scope)里就不能再有第二个了,比如如下代码就违反了这个限制,代码会报错:

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

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

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

Rust之所以这么设计是为了能在如下情况避免Data Race:

  • 两个或更多指针同时访问相同的数据
  • 至少有一个指针正在用于写入数据。
  • 没有使用机制来同步对数据的访问。

上文说到: 同一个scope里就不能再有第二个了...,那如果不在同一个scope还能能定义多个mut reference吗,答案是肯定的:

// 不会报错
let mut s = String::from("hello");

{
    let r1 = &mut s;
} // 在这里,r1的作用域结束了,因此我们可以创建一个新的引用。

let r2 = &mut s;

Rust同样也不允许统一变量的mut reference和immut reference同时出现,比如如下代码会报错:

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

let r1 = &s; // OK
let r2 = &s; // OK
let r3 = &mut s; // 寄!

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

如果把上述代码改一下,代码还会报错吗?

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

let r1 = &s; 
let r2 = &s; 
println!("{} and {}", r1, r2);

let r3 = &mut s; 
println!("{}", r3);

答案是不会的!为啥呢,因为编译器发现在执行完println!("{} and {}", r1, r2);r1, r2就再没出现过,所以它就能足够聪明得把r1, r2 在这行过后drop掉。最后跑到let r3这行的时候r1,r2已经影响不到r3啦。

ownership转移:

当方法返回的变量最后return出去的话,这个变量的ownership就会转移到call它的地方。

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

fn main() {
    let result = no_dangle();
    println!("{}", result);
}

Slice

现在你准备写一个功能:给一个string,需要返回这个string的第一个单词,你可以这么写:

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

    let word = first_word(&s); // word = 5
}

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()
}

看着没毛病,但有个小问题,如果你后面对s进行了修改,之前定义的word就会失去意义

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

    let word = first_word(&s); 

    s.clear(); // 清空s
    
    // 靠,word还是等于5
}

如果是写小项目的话这样的问题还很容易发现,但如果是大项目的话就说不准了。这个时候用Rust的Slice结构就能很好的解决这个问题:

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

    let word = first_word(&s);

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

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[..]
}

这么写的话word就会从一个赋值过后永远不会变的量变成了reference,也就是说能实时根据s变化而变化了。现在问题又来了,如果在main最后call s.clear()会怎样?

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

    let word = first_word(&s);
    
    word.clear();
    
    println!("the first word is: {}", word);
}

答案是会报错,因为clear()这个方法的实现需要用到word的mut reference,然而之前已经定义了word这个immutable reference。

其他类型的Slice:

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);

Struct

像很多语言,Rust也有定义Struct

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // 必须是mutable,Rust不允许只指定部分元素为mutable
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("[email protected]"),
        sign_in_count: 1,
    };
}

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

build_user里面,因为 username, email 的parameter name和field name一样,所以方法可以简写成:

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

类似JavaScript, Rust也支持解包操作,比如如上代码你已经定义了一个user1,如果还想定义user2user2只有email字段和user1.email不一样的话就能简写成:

fn main() {
    // --snip--

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

Tuple Struct:

比如你想用C实现对一个Pixel的定义,可能会这么写:

struct Color {
    int32_t r;
    int32_t g;
    int32_t b;
};

但其实r,g,b压根儿没必要定义出来,因为大伙都知道Pixel是由这个三个元素组成的,这个时候你就能用到Rust的Tuple Struct来简写这样的数据结构了:

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

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Unit-Like Struct

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Debug:

如果我们想打印struct用于Debug的话(不只是struct能用),Rust提供了一个接口能很容易的实现:

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

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

    println!("rect1 is {:?}", rect1);
}

你也能用一个更吊的macro:dgb!(),和print不同,dgb能获取调试内容的ownership进而支持用户能写出width: dbg!(30 * scale)的代码,区别2是dgb的调试信息会写到stderr而不是stdout

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

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

Method:

和很多新语言一样,Rust支持给Struct定义方法(Method),这里我们先回顾一下方法和函数的区别

  1. 方法 (Method):

    • "方法"通常用于面向对象编程(例如Java、C++等),它是与特定对象或类相关联的功能。方法是一种操作,可以在对象上执行,通常可以访问该对象的属性或状态,并且通常与对象的行为相关。
    • 方法是面向对象编程中的一部分,它们是类的成员,因此它们可以通过类的实例来调用。
  2. 函数 (Function):

    • "函数"是一段独立的可执行代码,通常不与特定对象或类相关联。函数可以独立存在,接受输入参数(如果需要)并返回一个结果。函数可以在程序中的任何地方被调用,而不需要依赖于特定的对象。
    • 函数是通用的编程概念,可以在不同编程范式(如面向对象、过程式编程等)中使用。
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

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

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

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

看到这儿,大家可能会对self.width * self.height产生疑惑,为啥指针不用->而是直接.呢。得益于Rust的automatic referencing and dereferencing 特性,编译器能自动处理转换reference符号。也就是说如下两行是一样的:

p1.distance(&p2);
(&p1).distance(&p2);

关联函数(Associated Function):

有些时候方法的实现并不需要self参数(既 square(&self, size: u32))类似静态方法,你能这么写:

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

impl其他用法:

如下写法都是OK的:

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

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

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

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

Enum

Rust的枚举类型写法是这样的:

enum IpAddrKind {
    V4,
    V6,
}

struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};

当然如上是常规写法,Rust的enum支持你给里面的元素定义不同的数据类型以及赋值:

enum Message {
    Quit,                        // 普通enum元素
    Move { x: i32, y: i32 },    // name filed
    Write(String),                // 字符串类型
    ChangeColor(i32, i32, i32),    // 三个类型
}

fn main() {
    let quit_message = Message::Quit;
    let move_message = Message::Move { x: 10, y: 20 };
    let write_message = Message::Write(String::from("Hello, Rust!"));
    let color_message = Message::ChangeColor(255, 0, 0);
}

除此之外你还可以将各种类型的数据放入枚举的变体中,例如字符串、数值类型或结构体。甚至可以包含另一个枚举!

可选参数Option:

在Rust标准库中,Optional定义为一个枚举类型:

enum Option<T> {
    None,
    Some(T),
}
类似于C++的泛型,下一篇文章会介绍

使用方法和其他enum一样:

let some_number = Some(5);
let some_char = Some('e');

let absent_number: Option<i32> = None;

这么定义有什么用呢,比如你现在这么写代码就会报错:

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

原因是因为y之前被定义为一个Optional的数据,如果在let sum = x + y; 之前y没被定义那不是寄了,所以Rust如此考量是为了安全。

Enum和Match一起使用:

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);

上述写法是完全OK的但是不优雅,因为None => None完全没必要写出来,Optional这个enum只有两个field还好,如果enum贼大,但你又只想math其中一小部分就难搞了。所以Rust提供了 Catch-all 的语法(其实就是default):

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => reroll(),    // Catch All
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}

在Rust里面,Match必须讨论到所有可能性,如下代码就是错的:

    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
            // 没讨论是None的情况
        }
    }

if let:

这个操作符能简化Match的写法,比如如下代码:

let config_max = Some(3u8);
match config_max {
    Some(max) => println!("The maximum is configured to be {}", max),
    _ => (),

能简写成:

let config_max = Some(3u8);
if let Some(max) = config_max {
    println!("The maximum is configured to be {}", max);
}

看到这里可能有人会问为啥不直接if Some(max) == config_max,这里千万要注意Some(max)并不是一个变量,而是解包操作!

结语

相信大家看到这里已经对Rust有了一个初步的理解。然而除了这些内容咱还有很多知识点没能探讨到,比如:泛型,多线程,智能指针,错误处理等等...这些内容我会放在未来的“Rust后日谈”文章探讨,敬请期待!