[Rust] 快速基础入门教程

Rust 是一个无运行时的强类型语言, 包含很多高级特性, 例如泛型, lambda 等. 又因为其独有的所有权机制, 所以 Rust 的内存安全要比 C++ 完善许多.


风格

Rust 与 C 族语言不一样, C 族语言在定义方法, 变量时, 都是 类型 关键字 这样的格式, 也就是类型前置. Rust 采用的是类型后置的风格, 即 关键字: 类型


基本结构

Rust 的结构与 C++ 是差不多的, 一个文件的顶部写要引入的内容, 下面是结构, 函数, 特征的声明.

use std::io::stdout;
use std::io::Write;

fn main() {
    let hello_str = "Hello world";
    let bytes = hello_str.as_bytes();

	stdout().write_all(bytes).unwrap();
}

在上面代码中, std::io::stdoutstd::io::Write 都是在 std::io 下, 它们可以通过花括号合并为以下语句:

use std::io::{Write, stdout};

基本类型

数字类型

长度有符号无符号
8 位i8u8
16 位i16u16
32 位i32u32
64 位i64u64
128 位i128u128
平台大小isizeusize

以及三十二位浮点数 f32 与六十四位浮点数 f64

布尔(逻辑)值 bool, 字符(Unicode)值 char.

Rust 中, 一个字符占四个字节, 可以表达任何 Unicode 字符, 包含 Emoji 表情.


函数声明

使用 fn 关键字来声明一个函数.

fn test1() {
	println!("hello");
}

如果要带参数, 直接按照类型后置风格在括号内写明.

fn test2(number: i32) {
	println!("number: {}", number);
}

如果要带返回值, 直接使用箭头 -> 来指定.

fn test3(num1: i32, num2: i32) -> i32 {
	return num1 + num2;
}

当返回值是最后一行的时候, 你可以省略 return 和结尾的分号, 直接将表达式作为返回值返回.

fn test3(num1: i32, num2: i32) -> i32 {
	num1 + num2
}

如果参数或者返回值是函数类型, 使用 fn 关键字即可, 下面的例子中, resolver 是一个函数, 这个函数有两个 i32 参数, 返回值是 i32

fn test4(num1: i32, num2: i32, resolver: fn(i32, i32) -> i32) {
	println!("{}", resolver(num1, num2));
}

变量声明

使用 let 声明一个变量.

let num: i32 = 114514;

大部分情况, 你都可以省略掉类型标记, Rust 会自动推导它的类型.

let num = 114514;

上面声明的变量, 是不可变的. 如果你希望声明可变的变量, 需要使用 mut 关键字.

let mut num = 114514;

// 更改其值
num = 666;

流程控制

Rust 中的 if 不使用括号, 直接跟表达式以及语句即可.

let num = 114514;
if num == 114514 {
	println!("value is 114514");
}

同样, Rust 中也有 else, else if 可用, 和 C 族语言类似, 只不过是少了括号.

if num == 114514 {
	println!("value is 114514");
} else if num == 1919810 {
	println!("value is 1919810");
} else {
	println!("invalid value");
}

Rust 中的 if 也可以实现根据条件返回特定值的需求.

let num = 114514;
let tip = if num == 114514 {
	"哼哼哼"
} else {
	"啊啊啊"
};

在上面的例子中, 对 num 进行判断, 如果值为 114514, tip 的值会是 “哼哼哼”, 否则为 “啊啊啊”. 需要注意的是, 当你希望 if 语句将语句作为结果返回时, 不要在语句末尾添加分号.

Rust 中的 for 用来对一个实例进行迭代. 使用 起始值..结束值 这样的语法可以创建简单的数值范围. 搭配 for 即可实现简单的数值循环.

for i in 0..10 {
	println!("current value: {}", i);
}

同样的, Rust 中, continuebreak 也可用.

for i in 0..10 {
    println!("current value: {}", i);

    if i == 3 {
    	continue;
    }

    if i == 7 {
    	break;
    }
}

值得一提的是, Rust 在循环时, 是允许对集合元素进行修改的. 只需要将迭代变量使用 mut 修饰.

let arr = [1, 2, 3, 4];
for mut ele in arr {
	ele = ele * 2;
}

如果你需要一个 ‘死循环’, 可以直接使用 loop 语句.

let mut i = 0;
loop {
    println!("current value: {}", i);

    if i == 3 {
    	continue;
    }

    if i == 10 {
    	break;
    }
}

Rust 的 loop 还支持给循环语句加上标签, 然后在内部循环中直接中断指定标签的循环.

'loop_out: loop {
    loop {
        // 在内部循环直接中断最外部循环
        break 'loop_out;
    }
}

Rust 的 loop 还可以作为一个带返回值的表达式使用. 只需要在 break 的时候提供返回值即可.

let mut value = 0;
let result = loop {
    value += 1;

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

Rust 的 match 语句可以近似理解为 if 的高级语法. 传入一个值, 以及匹配条件和语句, 可以执行对应语句.

let num = 114514;
match num {
    114514 => println!("hello"),
    1919810 => println!("world"),
    _ => { }
}

在上面的例子中, 会对 num 进行匹配, 并且在值为 114514 和 1919810 时执行不同的语句, 如果所有条件都没有匹配到, 则会使用 _ => { } 表示的默认情况, 在这里是空语句, 也就是什么也不执行.

同时, Rust 的 match 语句也可以作为表达式返回一个值, 只需要 match 内的语句是有返回值的表达式即可.

let result = match num {
    114514 => "hello",
    1919810 => "world",
    _ => ""
};

在上面的示例中, match 对 num 进行匹配, 并且在值为 114514 和 1919810 的时候返回不同的字符串, 最终赋值给 result. 如果没有匹配到指定条件, 则是使用默认语句 _ => "" 返回一个空的字符串.

需要注意的是, 在 match 语句中, 使用的是 => 而不是 ->.


字符串 / Strings

在 Rust 中, 字符串分两种, 一种是 str, 它表示字符串本身, 不可变.
由于 str 作为字符串本身, 其大小是不确定的, 所以它无法作为本地变量存储. 我们在使用时, 使用的都是 &str, 也就是 str 的引用.

let hello1 : &str = "你好世界";

另一种是 String, 本质是数组的包装, 它是可变的. 你可以对其进行更改. 你可以将它理解为其他语言中常见的 StringBuilder

let hello2 : String = String::new();

hello2.add("向字符串中添加一些内容");
hello2.add(", 你好吗?")

如果你需要将字符串编码为字节数组, 可以直接使用 as_bytes 函数

由于 Rust 中字符串使用 UTF-8 存储, 所以该函数的结果即为字符串使用 UTF-8 编码后的结果.

let tip = "hello world";
let bytes = tip.as_bytes();

如果希望从 UTF-8 转为 Rust 字符串, 可以使用 std::str::from_utf8 函数进行转换.

// bytes 为需要解码的数据
let bytes : &[u8];
let some_str = std::str::from_utf8(bytes);

数组 / Arrays

在 Rust 中, 数组时长度不可变的容器, 并且其大小必须在编译时确定. 其类型表达为: [类型; 长度].

let arr : [i32; 4];

在使用这个数组之前, 我们还需要对其进行初始化, 可以使用中括号指定其每一个元素的值.

let arr : [i32; 4] = [1, 2, 3, 4];

当然, 这里的数组类型也可以被省略掉.

let arr = [1, 2, 3, 4];

如果你希望直接初始化一个指定长度的数组, 可以使用中括号以及分号. 就像数组的类型表示.

let arr : [i32; 4] = [0; 4];
let arr = [0; 4];

容器 / Collections

如果你需要可变的容器, 可以使用 Vec<T>, 当然, 只有在声明时使用 mut 关键字, 它才可变.

let mut v : Vec<i32> = Vec::<i32>::new();

你可以将它简写为这样:

// 指定变量类型, Vec 的泛型参数会自动推导
let mut v : Vec<i32> = Vec::new();

// 指定泛型参数, 变量的类型会自动推导.
let mut v = Vec::<i32>::new();

使用 len 函数获取其长度:

let len = v.len();

pushpop 方法可以在 Vec<T> 的结尾增删元素.

v.push(114514);
v.pop();

使用 insertremove 可以在指定位置增删元素.

v.insert(0, 114514);
v.remove(0);

哈希映射(HashMap)用于存储基于哈希值的键值映射, 像是其他语言中的 “Dictionary” 或者 “Hashtable”,

let mut hm : HashMap<&str, &str> = HashMap::<&str, &str>::new();

简写:

let mut hm : HashMap<&str, &str> = HashMap::new();
let mut hm = HashMap::<&str, &str>::new();

使用 len 函数获取其长度:

let len = hm.len();

insert() 方法用于插入或更新一个键值对到哈希映射中, 如果键已经存在, 则更新为新的键值对, 并则返回旧的值. 如果键不存在则执行插入操作并返回 None.

hm.insert("qwq", "awa");

从哈希映射中获取和删除值.

let valueOption = hm.get(&"qwq");
let valueOption = hm.remove(&"qwq");

也可以使用 for 对哈希映射进行循环:

for (k, v) in hm { 
	println!("Key: {}, Value: {}", k, v);
}

哈希集合是基于哈希值的元素不重复容器, 常用于去重或快速查找元素是否存在.

let mut hs: HashSet<i32> = HashSet::new();

获取长度, 插入数据, 删除数据, 判断数据是否已经存在:

let len = hs.len();
let result = hs.insert(123);
let result = hs.remove(&123);
let result = hs.contains(&123);

结构 / Structures

使用 struct 关键字可以创建一个结构体.

struct Point {
    x: i32,
    y: i32,
}

在使用结构体类型的变量时, 该变量必须被初始化.

let p : Point = Point { x: 1, y: 3 };
let p = Point { x: 1, y: 3 };

你可以对它的成员进行赋值, 取值.

p.x = 114514;
let x = p.x;

如果需要为该类型添加一些方法, 使用 impl 关键字.

impl Point {
    fn new(x: i32, y: i32) -> Point {
    	Point { x: x, y: y }
    }
}

现在, 你可以使用 Point::new 来创建一个 Point 了.

let p = Point::new(123, 456);

如果你要为该类型的实例创建一些函数, 只需要在编写函数时, 将第一个参数声明为 self 即可.

impl Point {
    fn output(self: &Self) {
    	println!("Point, x: {}, y: {}", self.x, self.y);
    }
}

现在你可以通过一个 Point 实例来调用 output 函数进行输出了.

p.output();

在上述代码中, self 关键字表示当前实例, Self 关键字表示当前类型, 当然, 你也可以将它写成具体的类型. 下面的代码都是有效的实例函数定义:

impl Point {
    // 不使用 Self 关键字, 而是使用具体的 Point 类型
    fn output1(self: &Point) { }

    // 不使用 Self 关键字, 而是让其自动推导类型
    fn output2(&self) { }
}

特征 / Traits

在 Rust 中, trait 表示某种特征. 例如 “可迭代”, “可显示”, “可调试”. 它类似于其他编程语言的接口. 使用 trait 关键字创建一个特征.

trait TestTrait {
	fn some_func();
}

在上面的例子中, 我们创建了一个名为 TestTraittrait, 它规定, 需要有一个名为 some_func 的无参无返回值函数.

要使某个结构实现一个 trait, 使用 impl ... for ... 语法.

impl TestTrait for Point {
    fn some_func() {
    	println!("hello world from struct Point");
    }
}

trait 主要是与泛型搭配使用. 同时和其他编程语言不一样的是, trait 无法直接作为一个函数的参数类型. 你需要使用泛型, 然后指定泛型需要实现某 trait, 然后将该泛型作为函数的参数类型使用.

trait 可以用内置的实现, 在这方面, 它又像其他语言的抽象类. 例如, 某个 trait 需要类型实现函数 A, 而函数 B 由该 trait 自己实现, 内部逻辑依赖于函数 A. 这时, 只要某个类型实现了这个 trait 并编写函数 A 的实现, 他就可以直接使用 trait 内的函数 B.

trait TestTrait {
    fn get_string(&self) -> String;
    fn print_string(&self) {
    	println!("{}", self.get_string());
    }
}

impl TestTrait for Point {
    fn get_string(&self) -> String {
    	return format!("Point, x: {}, y: {}", self.x, self.y);
    }
}
let p = Point::new(1, 2);
p.print_string();

特性 / Attributes

Rust 中的特性(Attribute)是一种标记. 类似于 C# 的 Attribute 或者 Python 中的 Decorators, 在做上标记后, 即可拥有某种行为.

下面使用 derive 特性演示特性的使用.

#[derive(PartialEq, Eq)]
struct Point {
    x: i32,
    y: i32
}

在以上代码中, 我们为 Point 结构添加了 derive 特性, 这个特性用于自动实现指定的 trait, 在这里, 我们指定了 PartialEqEq.

在实现了 PartialEqEq 后, 我们的 Point 结构现在可以使用 ==!= 运算符了.

let p1 = Point { x: 123, y: 456 };
let p2 = Point { x: 345, y: 829 };

if p1 == p2 {
	println!("两点相等")
}

枚举 / Enumerations

Rust 中的枚举和其他语言中的枚举有很大不同. 它枚举可理解为 “情况”, 既可以作为类似于 C# 中的纯值类型使用, 也可以像 Java 的枚举一样在枚举中存储数据.

Rust 对于枚举的优化是很好的, 不像 Java 一般是基于堆中存储的.

enum ColorChannel {
	Red, Green, Blue
}

在以上的例子中, 我们声明了一个最简单的枚举, 这个枚举仅包含三种情况, 即 ‘红’, ‘绿’, ‘蓝’. 在这种情况下, 你可以理解为我们定义了三个数字值常量, 通过 ColorChannel 可以访问它们.

let color_channel1 = ColorChannel::Red;
let color_channel2 = ColorChannel::Green;
let color_channel3 = ColorChannel::Blue;

然后使用模式匹配进行判断.

match color_channel1 {
    ColorChannel::Red => println!("is red"),
    _ => {}
}

在这里我们不能使用 if 语句进行判断, 因为我们定义的枚举没有实现名为 PartialEqtrait, 这是内置于 rusttrait, 用于重载 ==!= 运算符.

下面我们将以一个不同情况的颜色讲述 Rust 中枚举存储值的用法.

enum Color {
    Rgb(u8, u8, u8),
    Channel(ColorChannel)
}

在上面的示例中, Color 分成了两种情况, 一种是 Rgb, 一种是 Channel. 当是 Rgb 的时候, 它存储三个无符号八位整数, 当是 Channel 的时候, 它存储一个 ColorChannel 枚举.

我们可以这样使用它:

let color = Color::Rgb(89, 43, 233);

match color {
    Color::Rgb(r, g, b) => {
    	println!("颜色是 RGB 值. R: {}, G: {}, B: {}", r, g, b);
    },

    Color::Channel(channel) => {
    	println!("颜色是通道, {}", channel);
    }
}

注意, 因为这里需要将 ColorChannel 打印输出, 所以 ColorChannel 需要实现名为 Displaytrait.

在这种有存储值的情况下, 我们也可以使用 if let 的语句对其进行判断:

if let Color::Rgb(r, g, b) = color {
	println!("颜色是 RGB 值. R: {}, G: {}, B: {}", r, g, b);
}

我们还可以为枚举中的值命名, 这样就可以:

enum Color {
    Rgb { r: u8, g: u8, b:u8 },
    Channel { channel: ColorChannel }
}

不过这样的话, 使用方式也需要做些改动:

let color = Color::Rgb { r: 23, g: 12, b: 129 };

match color {
    Color::Rgb { r, g, b } => {
    	println!("颜色是 RGB 值. R: {}, G: {}, B: {}", r, g, b);
    },

    Color::Channel { channel }=> {
    	println!("颜色是通道, {}", channel);
    }
}

if let Color::Rgb { r, g, b } = color {
	println!("颜色是 RGB 值. R: {}, G: {}, B: {}", r, g, b);
}

泛型 / Generic

泛型是编程语言中极重要的一个概念. 通过使用泛型, 可以实现一些逻辑的复用. 例如, 当我们自定义的结构实现 Rust 内置的某些 trait 时, 我们也可以使用 Rust 内的某些函数.

下面我们将自己定义一个简单的 trait 和一个简单的泛型函数.

trait I32Printer {
	fn print(&self, value: i32);
}

fn print_i32<Printer: I32Printer>(value: i32, printer: Printer) {
	printer.print(value);
}

在上面的逻辑中, 我们在 print_i32 后添加了尖括号, 尖括号中, 冒号的前半部分表示需要使用的泛型类型, 冒号后面是对泛型类型的约束, 表示该泛型类型必须实现 I32Printer 这个 trait.

在参数列表中, 我们定义了 printer 参数, 指定其类型为我们定义的泛型类型. 这样, 它就可以接受任何实现了 I32Printer 的类型.

接下来我们定义两个结构, 实现 I32Printer, 编写不同的打印逻辑.

struct SimpleI32Printer;
struct AnotherI32Printer<'a> {
	prompt: &'a str,
}

impl I32Printer for SimpleI32Printer {
    fn print(&self, value: i32) {
    	println!("{}", value);
    }
}

impl I32Printer for AnotherI32Printer<'_> {
    fn print(&self, value: i32) {
    	println!("{}: {}", self.prompt, value);
    }
}

在上面的例子中, 我们定义了 SimpleI32PrinterAnotherI32Printer 两个结构, 并且都为它们实现了 I32Printertrait.

现在, 可以调用方法, 传入不同的 printer, 然后查看运行结果了.

let printer1 = SimpleI32Printer;
let printer2 = AnotherI32Printer { prompt: "Number:" };

print_i32::<SimpleI32Printer>(114514, printer1);
print_i32::<AnotherI32Printer>(1919810, printer2);

可以看到, 它根据我们传入的不同类型实例, 有了不同的行为.

在其他语言, 例如 C# 和 Java 中, 你可以直接将接口作为参数类型指定. 但是在 Rust 中, 你必须创建一个泛型参数来做这样的逻辑.
Rust 中, 一切参数, 变量的大小都应该是固定的. 倘若我们允许 trait 作为参数类型, 那么类型的大小将不再确定. 而泛型则类似于 C++ 的模板, 在编译时, Rust 编译器会对其做处理, 生成能使用多个类型进行调用的函数.

接下来就是泛型类型了, 在定义类型的时候, 我们也可以使用泛型.

struct TwoValues<T1, T2> {
    value1: T1,
    value2: T2
}

使用起来也很简单:

let two_values = TwoValues::<i32, u8> {
    value1: 123,
    value2: 123
};

// 或者
let two_values : TwoValues<i32, u8> = TwoValues {
    value1: 123,
    value2: 123
};

// 也可以自动推导类型, 这里将会被推导为 TwoValues<i32, i32>
let two_values = TwoValues {
    value1: 123,
    value2: 123
};

为泛型类型实现方法, 需要这样写:

impl<T1, T2> TwoValues<T1, T2> {
    fn common_fn(&self) {
    	println!("common func");
    }
}

在上面的例子中, 由于我们不知道泛型类型具体类型, 所以在 impl 语句后还是需要声明两个泛型类型, 然后传入到类型.

但如果你希望为带有指定泛型参数的泛型类型定义一些函数, 可以这样写:

impl TwoValues<&str, i32> {
    fn test_output(&self) {
    	println!("{}: {}", self.value1, self.value2);
    }
}

在上面的例子中, 因为我们只想为泛型类型参数为 &stri32TwoValues 定义函数, 泛型类型已知, 所以不必再定义泛型类型.

下面还有个例子可供参考, 第一个泛型类型参数我们指定为 &str, 第二个指定为实现了 Display 的泛型类型.

impl<T: Display> TwoValues<&str, T> {
    fn test_output2(&self) {
    	println!("{}: {}", self.value1, self.value2)
    }
}

需要注意的是, 与其他语言不一样, Rust 在构造类型实例或者调用泛型函数的时候, 需要使用两个冒号以及尖括号来指定泛型类型参数.

// 正确使用
let two_values = TwoValues::<&str, i32> {
    value1: "Tip",
    value2: 10
}

print_i32::<SimpleI32Printer>(114514, printer1);

// 错误使用
let two_values = TwoValues<&str, i32> {
    value1: "Tip",
    value2: 10
}

print_i32<SimpleI32Printer>(114514, printer1);

之所以强调这点, 是因为其他语言, 诸如 C#, Java, Kotlin, 它们在构造类型实例和调用泛型方法的时候, 都是直接使用尖括号来指定泛型类型参数的. Rust 需要多加两个冒号, 初学者可能会忘记这点.


所有权 / Ownership

为了保证内存安全, Rust 引入了 ‘所有权’ 的概念. 其大概思想为:

  1. 一个类型实例有唯一的作用域, 当离开其作用域时, 该实例会被销毁
    这个作用域称为它的 ‘所有者’, 该作用域持有该实例的 ‘所有权’
  2. 所有权可以转交给另一个作用域, 转交后, 当前作用域将无法继续使用该实例
  3. 所有权可以借用, 并且指定一定的访问权限, 当前作用域仍持有该实例的 ‘所有权’

大多数编程语言都有作用域的概念, 离开作用域后, 值将作废:

if true {
    let some_integer = 114514;
}

// 这里将报错, 因为已经脱离了 some_integer 的作用域
println!("value: {}", some_integer);

当一个值直接传入到另外一个函数中, 那么这个值的所有权也将转交到另外一个函数中:

struct MyValue {
    value: i32
}

fn print_value(value: MyValue) {
    println!("value: {}", value.value);
}

fn main() {
    let my_value = MyValue { value: 114514 };
    print_value(my_value);

    // 这里将报错, 因为在执行 print_value 的时候, 所有权已经被转让
    // 当前作用域不再持有 my_value, 也就无法再使用它
    println!("value: {}", my_value.value);
}

如果希望函数不转让传入参数的所有权, 可以将参数类型定义为 ‘引用’. 你可以将其理解为其他语言中的 ‘指针’. 只需要在类型前加 & 符号即可.

struct MyValue {
	value: i32
}

fn print_value(value: &MyValue) {
	println!("value: {}", value.value);
}

fn main() {
    let my_value = MyValue { value: 114514 };
    print_value(&my_value);

    // 这时, 你仍然可以使用 my_value
    // 因为当前作用域持有 my_value 的所有权
    println!("value: {}", my_value.value);
}

虽然我们将值借给了 print_value, 但在 print_value 内部, 它只能读取参数的值, 而不能对参数进行修改. 如果你希望它能够修改该实例的值, 需要在类型前添加 mut 关键字.

fn change_value(value: &mut MyValue) {
  value.value = 123123;
}

在调用时也应该使用 &mut xxx 来获取该实例的可修改引用.

fn main() {
    let my_value = MyValue { value: 114514 };

    change_value(&mut my_value);
}

如果你需要一个能够对值本身进行更改, 那么在赋值时, 需要在变量名前添加 * 符号.

fn change_int_value(value: &mut i32) {
    *value = 114514;
}

错误处理

在 Rust 中, 错误分为两种: 可恢复的错误以及不可恢复的错误. 例如, 在将字符串解析为数字时, 如果数字格式不正确, 所引发的错误是程序逻辑上可以处理的. 而类似于内存访问冲突, 栈溢出这种, 就是无法恢复的错误.

不可恢复的错误会直接导致程序崩溃. 你可以使用 panic 宏手动引发错误.

panic!("oops");
println!("test");   // 这里代码不会被执行, 因为程序已经崩了

而对于可恢复的错误, Rust 中的函数都会返回一个 Result<T, E> 来表示可能包含错误值的返回值. 它是一个枚举, 包含两种取值: Ok(T)Err(E), 我们可以通过 match 语句对其两种情况分别进行处理.

let origin_str = "123";
let parse_result = origin_str.parse::<i32>();

match parse_result {
    Ok(value) => println!("Value is: {}", value),
    Err(err) => println!("Error: {}", err)
}

如果你确定该方法的执行不会出现错误, 也可以使用 unwrap 函数直接取得正确的值.

let origin_str = "123";
let parsed_value : i32 = origin_str.parse().unwrap();

但是如果尝试对一个错误值使用 unwrap, 就会引发 panic 了.

let origin_str = "不是数字";
let parsed_value: i32 = origin_str.parse().unwrap();   // 这里会直接崩溃, 因为解析是失败的, 无法取得结果值

Rust 还提供了一个 ? 操作符用于简化异常处理. 下面的代码是不使用 ? 的.

fn mul_input_with_10() -> Result<i32, ParseIntError> {
    let mut input = String::new();
    std::io::stdin().read_line(&mut input).unwrap();

    let valueResult = input.parse::<i32>();

    match valueResult {
        Ok(value) => Ok(value * 10),
        Err(err) => Err(err),
    }
}

如果使用 ? 的话, 则是这样. 当结果为 Err(E) 的时候, 会直接将结果作为当前函数的返回值返回, 表达式的结果则是正确的值.

fn mul_input_with_10() -> Result<i32, ParseIntError> {
    let mut input = String::new();
    std::io::stdin().read_line(&mut input).unwrap();

    Ok(input.parse::<i32>()? * 10)
}

模块

模块是 Rust 中组织源代码的方式. 在 Rust 中, 一个文件或者文件夹都可以叫做一个 “模块”.

例如, 当我有一个 main.rs, 我希望在里面使用 test.rs 的成员时:

// 这里是 test.rs 的内容

// 公开一个函数
pub fn test_fn() {
    println!("test fn");
}

下面是 main.rs, 使用 mod 语句引入模块, 然后使用 use 语句使用模块中的成员:

// 引入 test 模块
mod test;

// 使用模块中的成员
use test::test_fn;

fn main() {
    test_fn();
}

如果希望将一个文件夹暴露为一个模块的话, 你需要先创建一个文件夹, 然后在文件夹下创建 mod.rs, 然后编写内容. 在该文件下向外暴露的成员, 即为该模块的成员.

// 这里是 test2/mod.rs 的内容

// 公开一个函数
pub fn test_fn() {
    println!("test fn");
}

引用的时候和之前的代码一样, 只需要使用 mod test2 即可引入 test2 模块.

如果你希望在 test2 文件夹下编写更多的文件, 并向外暴露:

|- test2
|  |- another.rs
|  -- mod.rs
|
-- main.rs

那么任何你想要向外暴露的内容, 都应该在 test2/mod.rs 下声明好.

// 这里是 test2/another.rs 的内容

// 公开一个结构
pub struct AnotherStruct {
    
}

test2/mod.rs 中, 你需要导入并公开 another 这个模块.

// 这里是 test2/mod.rs 的内容

// 导入并公开 another 模块
pub mod another;

// 公开属于 test2 的成员
pub fn test_fn() {
    println!("test fn");
}

于是, 你就可以在 main.rs 中, 使用 AnotherStruct 这个类型了.

// 导入 test2 模块
mod test2;

// 使用 test2/another 中的 AnotherStruct
use test2::another::AnotherStruct;

fn main() {
    let value = AnotherStruct {};
}

但是, 如果你希望在使用 AnotherStruct 时, 直接通过 test2::AnotherStruct 导入, 也可以在 mod.rs 这样向外公开:

// 这里是 test/mod.rs 的内容

// 导入 another 模块, 但是不公开
mod another;

// 使用并公开 another 下的结构
pub use another::AnotherStruct;

这样 AnotherStruct 可以通过 use 语句直接向外暴露, 使用时就可以直接 use test2::AnotherStruct

// 导入 test2 模块
mod test2;

// 使用 test2 直接暴露的 AnotherStruct
use test2::AnotherStruct;

fn main() {
    let value = AnotherStruct {};
}

方便起见, 你也可以直接用 *mod.rs 直接向外暴露某个模块的所以成员:

mod another;

// 向外暴露 another 中的所有成员
pub use another::*;

如果你在使用多个模块时, 它们的类型名称相同, 你可以在 use 的使用, 使用 as 为其取别名:

mod test2;

use test2::AnotherStruct as qwq;

fn main() {
    let value = qwq {};
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/204143.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

零代码集成自动化的实现逻辑是什么?

零代码的概念是什么&#xff1f; 零代码平台是一种软件开发工具或平台&#xff0c;非技术人员能够创建和部署应用程序&#xff0c;而无需编写任何代码。它提供了可视化的界面和拖拽式的操作&#xff0c;使用户能够通过简单的配置和组合&#xff0c;以图形化的方式构建应用程序…

vue2常见的语法糖

Vue.js 2 提供了一些语法糖&#xff08;syntactic sugar&#xff09;来简化常见的操作。以下是一些 Vue.js 2 中常用的语法糖&#xff1a; v-bind 简写&#xff1a; <!-- 完整语法 --> <a v-bind:href"url">Link</a><!-- 简写 --> <a :hr…

Jmeter进阶使用:BeanShell实现接口前置和后置操作!

一、背景 我们使用Jmeter做压力测试或者接口测试时&#xff0c;除了最简单的直接对接口发起请求&#xff0c;很多时候需要对接口进行一些前置操作&#xff1a;比如提前生成测试数据&#xff0c;以及一些后置操作&#xff1a;比如提取接口响应内容中的某个字段的值。举个最常用…

nexus制品库的介绍及详细部署使用

一、nexus 介绍 Nexus 是一个强大的仓库管理工具&#xff0c;用于管理和分发 Maven、npm、Docker 等软件包。它提供了一个集中的存储库&#xff0c;用于存储和管理软件包&#xff0c;并提供了版本控制、访问控制、构建和部署等功能。 Nexus 可以帮助开发团队提高软件包管理的效…

构建强大的接口自动化测试框架:Pytest实践指南!

一. 背景 Pytest目前已经成为Python系自动化测试必学必备的一个框架&#xff0c;网上也有很多的文章讲述相关的知识。最近自己也抽时间梳理了一份pytest接口自动化测试框架&#xff0c;因此准备写文章记录一下&#xff0c;做到尽量简单通俗易懂&#xff0c;当然前提是基本的py…

DevEco Studio设置每次进入 是否自动进入上一次的项目

首先 我们第一次创建项目 并不是这个界面 如果我们想在这个界面创建项目的话 可以 点击左上角 File 下的 New 下的 Create Project 这里 我们可以点击左上角 File 选择下面的 Settings… 这个界面就有非常多的配置 然后 我们选择到下图操作的位置 这里有一个Reopen projects…

适用于 Windows 和 Mac 电脑的最佳数据恢复软件

当我们的电脑上的文件被错误删除时&#xff0c;总是很难恢复该文件&#xff0c;或者除非您进行了系统还原&#xff0c;否则一切都会恢复到删除恢复的文件或文件夹之前的状态。 拥有合适的 PC 软件始终可以帮助您改善 PC 用户的体验&#xff0c;而适用于 Windows 10 和 11 的良…

SQL 金额数值转换成中文大写

需求&#xff1a;将金额转换成中文大写格式填入单据合计行&#xff1a; _佰_拾_万_仟_佰_拾_元_角_分 1234567.89 壹佰贰拾叁万肆仟伍佰陆拾柒元捌角玖分 1.函数转换 drop function n2C;CREATE FUNCTION n2C (num numeric(14,2)) RETURNS VARCHAR(20) AS BEGIN …

linux NAT网卡配置static

由于是内网&#xff0c;资料无法拷贝&#xff0c;借助参考资料&#xff0c;整理发出。 镜像安装 基本操作。 查看VM配置 图1&#xff0c;有几个信息。一个是NAT借用了网卡里的VMnet8适配器。 子网IP是从192.168.142.0 子网掩码255.255.255.255&#xff0c;对应下面配置的N…

【云平台】STM32微信小程序阿里云平台汇总——持续更新

【云平台】STM32微信小程序阿里云平台汇总——持续更新 文章目录 前言总结 前言 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 【云平台】STM32微信小程序阿里云平台学习板 【云平台】小白从零开始&#xff1a;小程序阿里云平台控制STM32&#xff08…

WordPress批量上传文章和自动发布文章的方法

专业介绍&#xff1a;WordPress批量上传文章技术解析 在现代数字时代&#xff0c;内容创作是网络存在的驱动力之一。对于博客作者、新闻编辑和内容管理员而言&#xff0c;高效地批量上传文章至WordPress平台是提高工作效率的一个关键方面。WordPress作为最受欢迎的内容管理系统…

48、Flink DataStream API 编程指南(3)- 完整版

Flink 系列文章 1、Flink 部署、概念介绍、source、transformation、sink使用示例、四大基石介绍和示例等系列综合文章链接 13、Flink 的table api与sql的基本概念、通用api介绍及入门示例 14、Flink 的table api与sql之数据类型: 内置数据类型以及它们的属性 15、Flink 的ta…

LeetCode Hot100 101.对称二叉树

题目&#xff1a; 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 代码&#xff1a; class Solution {public boolean isSymmetric(TreeNode root) {if(rootnull || (root.leftnull && root.rightnull)) {return true;}//用队列保存节点LinkedList<…

【网络安全】meterpreter攻击实战

1.meterpreter 攻击成功后可以做什么指令&#xff1f; 远程控制命令执行摄像头监控密码获取创建后门用户破坏篡改系统。 2.创建后门用户并开启远程连接&#xff1a; net user zhangsan 123456/add && net localgroup adminstrators zhangsan/add exit run getgul -…

面向对象编程(结合GPT):写给小女朋友的保姆级Python软件开发教程01:pyside6环境的配置和工具的使用

参考&#xff1a; 库的安装和两个在pycharm工具的配置 B站教程&#xff1a;可以练听力&#xff1a;【5个小时 PySide6完全开发指南 使用 Qt 进行 Python GUI 桌面应用开发&#xff08;中英字幕&#xff09;】 CSDN也有配置 安装完以后其实这个就是路径。 打开可以看到pyside…

进阶C语言-字符函数和字符串函数

字符函数和字符串函数 &#x1f388;1.函数介绍&#x1f50e;1.1strlen函数&#x1f52d;1.1.1strlen函数的模拟实现&#x1f4d6;1.计数器法&#x1f4d6;2.递归法&#x1f4d6;3.指针-指针 &#x1f50e;1.2strcpy函数&#x1f52d;1.2.1strcpy函数的模拟实现 &#x1f50e;1…

VGN S99快捷键,说明书

VGN S99快捷键-说明书 按键说明灯光效果常见疑难 按键说明 切换关闭电量指示灯&#xff1a;Fn home 灯光效果 常见疑难

burpsuite issue definitions

https://portswigger.net/burp/documentation/scanner/vulnerabilities-list 先从高危的开始学(四十能学剑&#xff0c;时人无此心)&#xff1a; os command injection todo 未完待续

一些好用的12款前端小插件

1. cropper.js Cropper.js 2.0 是一系列用于图像裁剪的 Web 组件。 官网地址&#xff1a;https://fengyuanchen.github.io/cropperjs/v2/zh/ 2. Vditor Vditor是一款浏览器端的 Markdown 编辑器&#xff0c;支持所见即所得、即时渲染&#xff08;类似 Typora&#xff09;和分…

php请求okx接口获取比特币价格数据、k线数据

php请求okx接口获取比特币价格数据 环境配置请求头、签名设置签名配置代理 全部代码 环境 我本地用的是thinkphp框架和guzzle 安装guzzle composer require guzzlehttp/guzzle 配置请求头、签名 我们需要准备api_key&#xff0c;secret_key&#xff0c;passphrase api_key…
最新文章