简介 Rust是一门系统编程语言,专注于安全 ,尤其是并发安全,支持函数式和命令式以及泛型等编程范式的多范式语言。Rust在语法上和C++类似,但是设计者想要在保证性能的同时提供更好的内存安全。 Rust最初是由Mozilla研究院的Graydon Hoare设计创造,然后在Dave Herman, Brendan Eich以及很多其他人的贡献下逐步完善的。 Rust的设计者们通过在研发Servo网站浏览器布局引擎过程中积累的经验优化了Rust语言和Rust编译器。
Rust编译器是在MIT License 和 Apache License 2.0双重协议声明下的免费开源软件。 Rust已经连续七年(2016,2017,2018,2019,2020, 2021, 2022)在Stack Overflow开发者调查的“最受喜爱编程语言”评选项目中折取桂冠。
安装 1.进入官网下载 https://www.rust-lang.org/
1 curl --proto '=https' --tlsv1.2 -sSf https:
2.查看版本
语法 HelloWorld 1 2 3 fn main() { println !("Hello, world!" ); }
关键字 下面的关键字目前可以完成对应描述中的功能。
• as:执行基础类型转换,消除包含条目的指定trait的歧义,在 use与extern crate语句中对条目进行重命名。 • break:立即退出一个循环。 • const:定义常量元素或不可变裸指针。 • continue:继续下一次循环迭代。 • crate:连接一个外部包或一个代表了当前包的宏变量。 • dyn:表示trait对象可以进行动态分发。 • else:if和if let控制流结构的回退分支。 • enum:定义一个枚举。 • extern:连接外部包、函数或变量。 • false:字面量布尔假。 • fn:定义一个函数或函数指针类型。 • for:在迭代器元素上进行迭代,实现一个trait,指定一个高 阶生命周期。 • if:基于条件表达式结果的分支。 • impl:实现类型自有的功能或trait定义的功能。• in:for循环语法的一部分。 • let:绑定一个变量。 • loop:无条件循环。 • match:用模式匹配一个值。 • mod:定义一个模块。 • move:让一个闭包获得全部捕获变量的所有权。 • mut:声明引用、裸指针或模式绑定的可变性。 • pub:声明结构体字段、impl块或模块的公共性。 • ref:通过引用绑定。 • return:从函数中返回。 • Self:指代正在其上实现trait的类型别名。 • self:指代方法本身或当前模块。 • static:全局变量或持续整个程序执行过程的生命周期。 • struct:定义一个结构体。 • super:当前模块的父模块。 • trait:定义一个trait。 • true:字面量布尔真。 • type:定义一个类型别名或关联类型。 • unsafe:声明不安全的代码、函数、trait或实现。 • use:把符号引入作用域中。 • where:声明一个用于约束类型的从句。 • while:基于一个表达式结果的条件循环。
下面的关键字目前还没有任何功能,但它们被Rust保留下来以备 将来使用。
• abstract • async • become • box • do • final • macro • override • priv • try • typeof • unsized • virtual • yield
变量 变量可变性由mut
关键字修饰的变量可变,否则是常量无法修改。但是rust支持重复定义同名变量,会进行覆盖,如果是类型相同的我们可以使用重新定义变量覆盖
案例
1 2 let foo = 5 ; let mut bar = 5 ;
数据类型 标量类型 (scalar)和复合类型 (compound)
标量类型 (scalar) 标量 类型是单个值类型的统称。Rust中内建了4种基础的标量类型:整数、浮点数、布尔值及字符。
整数
浮点数 1 2 let x = 2.0 ; let y : f32 = 3.0 ;
布尔类型 1 2 let t = true ; let f : bool = false ;
字符类型
复合类型 (compound) 复合类型 (compound type)可以将多个不同类型的值组合为一个类型。Rust提供了两种内置的基础复合类型:元组 (tuple)和数组 (array)。
元组类型
元组是一种相当常见的复合类型,它可以将其他不同类型的多个值组合进一个复合类型中。元组还拥有一个固定的长度:你无法在声明结束后增加或减少其中的元素数量。
1 2 3 4 let tup : (i32 , f64 , u8 ) = (500 , 6.4 , 1 );let tup = (500 , 6.4 , 1 );let (x, y, z) = tup;println! ("The value of y is: {}" , y);
可以通过索引并使用点号(.)来访问元组中的值:
1 2 3 4 let tup : (i32 , f64 , u8 ) = (500 , 6.4 , 1 );println! ("tup.0 : {}" ,tup.0 );println! ("tup.1 : {}" ,tup.1 );println! ("tup.2 : {}" ,tup.2 );
**数组类型 **
我们同样可以在数组 中存储多个值的集合。与元组不同,数组中的每一个元素都必须是相同的类型。Rust中的数组拥有固定的长度,一旦声明就再也不能随意更改大小,这与其他某些语言有所不同。
1 2 3 4 5 6 7 let a = [1 ,2 ,3 ,4 ,5 ,6 ];let months = ["January" , "February" , "March" , "April" , "May" , "June" , "July" , "August" , "September" , "October" , "November" , "December" ];let arr : [i32 ;5 ] = [1 ,2 ,3 ,4 ,5 ];println! ("{},{}" , arr[0 ],arr[4 ]);
函数 Rust代码使用蛇形命名法 (snake case)来作为规范函数和变量名称的风格。蛇形命名法只使用小写的字母进行命名,并以下画线分隔单词。
函数定义 1 2 3 fn 函数名(参数:参数类型) -> 返回类型 { }
案例
1 2 3 4 5 6 7 8 fn method(num:i32)->i32{ if num == 1 { return num} return method(num-1 ) * num } fn m()->i32{ 5 }
流程控制 if表达式 1 2 3 4 5 6 let number = 10 ;if number<5 { println !("true" ); } else { println !("false" ) }
1 2 3 4 5 6 7 8 9 10 let number = 6 ;if number % 4 == 0 { println !("number is divisible by 4" ); } else if number % 3 == 0 { println !("number is divisible by 3" ); } else if number % 2 == 0 { println !("number is divisible by 2" ); } else { println !("number is not divisible by 4, 3, or 2" ); }
1 let number = if 1 <2 { 5 }else { 6 };
循环控制 Rust提供了3种循环:loop、while和for。
1 2 3 4 5 6 7 8 9 10 11 12 13 fn loop_m(){ loop { println !("loop" ) } let mut idx = 0 ; while idx<100 { println !("while" ); idx+=1 ; } for i in 0. .100 { print !("for" ); } }
结构体 定义 1 2 3 4 5 [pub] struct StructName { fieldName: Type, fieldName: Type, ...... }
案例
1 2 3 4 struct Person { name: String, age: u8, }
实例化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 fn build_user(email: String, username: String) -> User { User { email, username, active: true , sign_in_count: 1 , } }struct User { username: String, email: String, sign_in_count: u64, }
使用元组定义结构体并实例化
1 2 3 4 5 6 7 struct Color(i32, i32, i32);struct Point(i32, i32, i32); fn main() { let black = Color(0 , 0 , 0 ); let origin = Point(0 , 0 , 0 ); }
需要注意的是,一旦实例可变,那么实例中的所有字段都将是可变的。Rust不允许我们单独声明某一部分字段的可变性。如同其他表达式一样,我们可以在函数体的最后一个表达式中构造结构体实例, 来隐式地将这个实例作为结果返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct Rectangle{ width: u32, height: u32, } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height } fn main() { let rectangle = Rectangle{ width:100 , height:20 , }; println !("rectangle area = {}" ,area(&rectangle)) }
格式化输出(通过derive注解来派生的trait)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #[derive(Debug)]struct Rectangle{ width: u32, height: u32, } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height } fn main() { let rectangle = Rectangle{ width:100 , height:20 , }; println !("rectangle = {:?}" ,rectangle); println !("rectangle = {:#?}" ,rectangle); println !("rectangle area = {}" ,area(&rectangle)) }
方法 方法与函数十分相似:它们都使用fn关键字及一个名称来进行声明;它们都可以拥有参数和返回值;另外,它们都包含了一段在调用时执行的代码。但是,方法与函数依然是两个不同的概念,因为方法总是被定义在某个结构体的上下文中,并且它们的第一个参数永远都是self,用于指代调用该方法的结构体实例。
定义方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #[derive(Debug)]struct Rectangle{ width: u32, height: u32, } impl Rectangle { pub fn area(&self) -> u32 { self.width * self.height } } fn main() { let rectangle = Rectangle{ width:100 , height:20 , }; println !("rectangle = {:?}" ,rectangle); println !("rectangle = {:#?}" ,rectangle); println !("rectangle area = {}" ,rectangle.area()); }
我们可以在类型名称后添加::
来调用关联函数,就像let sq = Rectangle:: square(3);
一样。这个函数位于结构体的命名空间中, 这里的::
语法不仅被用于关联函数,还被用于模块创建的命名空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #[derive(Debug)]struct Rectangle{ width: u32, height: u32, } impl Rectangle { pub fn area(&self) -> u32 { self.width * self.height } fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size } } } fn main() { let rectangle = Rectangle{ width:100 , height:20 , }; println !("rectangle = {:?}" ,rectangle); println !("rectangle = {:#?}" ,rectangle); println !("rectangle area = {}" ,rectangle.area()); let s = Rectangle::square(10 ); }
枚举 定义枚举 1 2 3 4 5 6 7 8 9 enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; }
案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 enum IpAddrKind { V4, V6, }struct IpAddr { kind: IpAddrKind, address: String, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1" ), }; let loopback = IpAddr { kind: IpAddrKind::V6, address: String::from("::1" ), }; }
1 2 3 4 5 6 enum Message { Quit, Move { x: i32 , y: i32 }, Write (String ), ChangeColor (i32 , i32 , i32 ), }
Option 1 2 3 4 fn main () { let x : Option <u32 > = Some (2 ); assert_eq! (x.is_some (), true ); }
Match 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 , } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 fn plus_one (x: Option <i32 >) -> Option <i32 > { match x { None => None , Some (i) => Some (i + 1 ), } }fn main () { let five = Some (5 ); let six = plus_one (five); println! ("six={:?}" ,six); let none = plus_one (None ); println! ("six={:?}" ,none); }
通配符(下划线) 1 2 3 4 5 6 7 8 9 10 11 12 13 fn main () { let some_u8_value = 0u8 ; match some_u8_value { 1 => println! ("one" ), 3 => println! ("three" ), 5 => println! ("five" ), 7 => println! ("seven" ), _ => (), } if let Some (3 ) = some_u8_value { println! ("three" ); } }
引用模块
案例
特性 所有权
Rust中的每一个值都有一个对应的变量作为它的所有者 。
在同一时间内,值有且仅有一个所有者。
当所有者离开自己的作用域时,它持有的值就会被释放掉。
变量作用域
内存与分配 1 let s = String::from("hello" );
Rust提供了另一套解决方案:内存会自动地在拥有它的变量离开作用域后进行释放。下面的代码类似于示例中的代码,不过我们将字符串字面量换成了String类型:
1 2 3 4 { let s = String::from("hello" ); }
有一个很适合用来回收内存给操作系统的地方:变量s离开作用域的地方。Rust在变量离开作用域时,会调用一个叫作drop的特殊函数。String类型的作者可以在这个函数中编写释放内存的代码。记住,Rust会在作用域结束的地方(即}处)自动调用drop函数。
所有权与函数 下面是一些拥有Copy这种trait的类型:
所有的整数类型,诸如u32。
仅拥有两种值(true和false)的布尔类型:bool。
字符类型:char。
所有的浮点类型,诸如f64。
如果元组包含的所有字段的类型都是Copy的,那么这个元组也是Copy的。例如,(i32, i32)是Copy的,但(i32, String)则不是。
将值传递给函数在语义上类似于对变量进行赋值。将变量传递给函数将会触发移动或复制,就像是赋值语句一样。 尝试在调用takes_ownership后使用变量s会导致编译时错误。这类静态检查可以使我们免于犯错。你可以尝试在main函数中使用s和x变量,来看一看在所有权规则的约束下能够在哪些地方合法地使用它们。
如果我们想进行函数调用后还继续使用该变量,那我们可以将它调用完进行返回
1 2 3 4 5 6 7 8 9 10 11 12 13 fn m2(){ let mut s = String::from("hello" ); s = takes_ownership(s); println !("s={}" ,s); let x = 5 ; makes_copy(x); println !("x={}" ,x); } fn takes_ownership(s:String)->String{ println !("{}" ,s); s }
但是如果我又想当前传入函数的变量不被回收又能返回计算结果,那么我们可以同时对其进行返回(多返回值)
1 2 3 4 5 6 7 8 9 fn main() { let s1 = String::from("hello" ); let (s2, len ) = calculate_length(s1); println !("The length of '{}' is {}." , s2, len ); } fn calculate_length(s: String) -> (String, usize) { let length = s.len (); (s, length) }
上面的方法都显得十分的繁杂,下面的这种方法更加简单,我们在传入函数的变量时,我们传入变量的地址
1 2 3 4 5 6 7 8 9 10 11 12 fn m4(){ let mut s = String::from("hello" ); let l = takes_own(&s); println !("s={},l={}" ,s,l); let x = 5 ; makes_copy(x); println !("x={}" ,x); } fn takes_own(s:&String)->usize{ println !("{}" ,s); s.len () }
与使用&进行引用相反的操作被称为解引用(dereferencing), 它使用 * 作为运算符。
这里的&s1语法允许我们在不转移所有权的前提下,创建一个指向 s1值的引用。由于引用不持有值的所有权,所以当引用离开当前作用 域时,它指向的值也不会被丢弃。 同理,函数签名中的&用来表明参数s的类型是一个引用。
1 2 3 4 fn calculate_length(s: &String) -> usize { s.len () }
此处,变量s的有效作用域与其他任何函数参数一样,唯一不同的是,它不会在离开自己的作用域时销毁其指向的数据,因为它并不拥有该数据的所有权。当一个函数使用引用而不是值本身作为参数时, 我们便不需要为了归还所有权而特意去返回值,毕竟在这种情况下, 我们根本没有取得所有权。
这 种 通 过 引 用 传 递 参 数 给 函 数 的 方 法 也 被 称 为 借 用 (borrowing)。在现实生活中,假如一个人拥有某件东西,你可以从他那里把东西借过来。但是当你使用完毕时,就必须将东西还回去。
那我们是否可以在方法体里面修改该变量的值呢
1 2 3 4 5 6 7 fn main() { let s = String::from("hello" ); change(&s); } fn change(some_string: &String) { some_string.push_str(", world" ); }
此时修改是无法修改的,我们得用可变的引用才允许修改
1 2 3 4 5 6 7 fn main() { let mut s = String::from("hello" ); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world" ); }
悬垂引用 使用拥有指针概念的语言会非常容易错误地创建出悬垂指针 。这类指针指向曾经存在的某处内存地址,但该内存已经被释放掉甚至是被重新分配另作他用了。而在Rust语言中,编译器会确保引用永远不会进入这种悬垂状态。假如我们当前持有某个数据的引用,那么编译器可以保证这个数据不会在引用被销毁前离开自己的作用域。
1 2 3 4 5 6 7 fn main(){ let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello" ); &s }
此时无法通过编译 this function's return type contains a borrowed value, but there is no value for it to be borrowed from
由于变量s创建在函数dangle内,所以它会在dangle执行完毕时随之释放。但是,我们的代码依旧尝试返回一个指向s的引用,这个引用指向的是一个无效的String,这可不对!Rust成功地拦截了我们的危险代码。
解决这个问题的方法也很简单,直接返回String就好:
1 2 3 4 fn no_dangle() -> String { let s = String::from("hello" ); s }
引用的规则
在任何一段给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用。
引用总是有效的。
切片 除了引用,Rust还有另外一种不持有所有权的数据类型:切片(slice)。切片允许我们引用集合中某一段连续的元素序列,而不是整个集合。
编写一个搜索函数,它接收字符串作为参数,并将字符串中的首个单词作为结果返回。如果字符串中不存在空格,那么就意味着整个字符串是一个单词,直接返回整个字符串作为结果即可。
1 2 3 4 5 6 7 8 9 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 () }
这段代码首先使用as_bytes方法将String转换为字节数组,因为我们的算法需要依次检查String中的字节是否为空格。接着,我们通过iter方法创建了一个可以遍历字节数组的迭代器。
1 2 3 4 5 6 7 8 9 fn main() { let mut s = String::from("hello world" ); let word = first_word(&s); s.clear(); println !("idx={}" ,word); println !("s={}" ,s); }
这种API的设计方式使我们需要随时关注word的有效性,确保它与s中的数据是一致的,类似的工作往往相当烦琐且易于出错。这种情况对于另一个函数second_word而言更加明显。
字符串切片:字符串切片是指向String对象中某个连续部分的引用,它的使用方式如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 fn slice(){ let s = String::from("hello world" ); let hello = &s[0. .5 ]; let world = &s[6. .11 ]; let full = &s[..]; let hi = &s[..5 ]; println !("{}" ,hello); println !("{}" ,hi); println !("{}" ,world); println !("{}" ,full); }
重构first_word函数
1 2 3 4 5 6 7 8 9 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[..] }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 fn main() { let my_string = String::from("hello world" ); let word = first_word(&my_string[..]); let my_string_literal = "hello world" ; let word = first_word(&my_string_literal[..]); let word = first_word(&my_string_literal[..]); } 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[..] }
其他类型的切片 从名字上就可以看出来,字符串切片是专门用于字符串的。但实际上,Rust还有其他更加通用的切片类型,以下面的数组为例:
1 let a = [1 , 2 , 3 , 4 , 5 ];
就像我们想要引用字符串的某个部分一样,你也可能会希望引用数组的某个部分。这时,我们可以这样做:
1 2 let a = [1 , 2 , 3 , 4 , 5 ]; let slice = &a[1. .3 ];
这里的切片类型是&[i32],它在内部存储了一个指向起始元素的引用及长度,这与字符串切片的工作机制完全一样。你将在各种各样的集合中接触到此类切片
闭包
闭包(closure),一个类似于函数且可以存储在变量中的结构。
Rust中的闭包是一种可以存入变量或作为参数传递给其他函数的 匿名函数 。你可以在一个地方创建闭包,然后在不同的上下文环境中 调用该闭包来完成运算。和一般的函数不同,闭包可以从定义它的作 用域中捕获值。
我们希望在程序中将代码定义在一处,但只在真正需要结果时才执行 相关代码。而这正是闭包的用武之地!
迭代器
迭代器(iterator),一种处理一系列元素的方法。
管理
包(package) :一个用于构建、测试并分享单元包的Cargo功能。
单元包(crate) :一个用于生成库或可执行文件的树形模块结构。
模块(module) 及use关键字: 它们被用于控制文件结构、 作用域及路径的私有性。
路径(path) :一种用于命名条目的方法,这些条目包括结构体、函数和模块等。
包与单元包 1.rust中声明包的关键字是mod,如果是公共的,则需要声明为pub mod。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 pub mod game { use std::cmp::Ordering; use std::io; use rand::Rng; use crate::example; pub fn guess () { example::example::say_hello (); println! ("guess number 1..10" ); println! ("please enter you number:" ); let mut cnt = 0 ; loop { 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); let secret_number = rand::thread_rng ().gen_range (1 ..=10 ); println! ("The secret number is: {}" , secret_number); match guess.cmp (&secret_number) { Ordering::Less => println! ("Too small!" ), Ordering::Greater => println! ("Too big!" ), Ordering::Equal => { println! ("You win!" ); break ; } } cnt+=1 ; if cnt==10 { println! ("game over" ); break } } } }
rust中,每个文件都是一个包,文件名就是包名。如果是和main.rs同级的文件,可以直接使用文件名作为包名。如果有多级目录,那么每个目录下,都要有一个mod.rs作为包的入口,rust的mod.rs文件中需要显式地说明当前目录下的包名,以及是否是pub类型的。
2.use关键字用于引用,当需要使用其他模块的方法或者结构体时需要先进行导入
1 2 3 4 5 use xxx::xxxuse std::io;use rand::Rng;use std::cmp::Ordering;
3.导入后进行调用
1 2 3 4 5 6 7 8 9 10 11 12 mod chapter4;mod chapter3;mod example;mod guess;use guess::guess::game as g;fn main () { guess::guess::game::guess (); g::guess (); }
4.关于导入的路径问题
use::crate:: 来引用上级的模块
如果是main文件直接使用mod声明模块就行,main文件比较特殊
5.相同模块了怎么办?
不担心rust提供了as关键字来别名模块
Cargo rust使用cargo来管理项目依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 Rust's package manager USAGE: cargo [+toolchain] [OPTIONS] [SUBCOMMAND] OPTIONS: -V, --version Print version info and exit --list List installed commands --explain <CODE> Run `rustc --explain CODE` -v, --verbose Use verbose output (-vv very verbose/build.rs output) -q, --quiet Do not print cargo log messages --color <WHEN> Coloring: auto, always, never --frozen Require Cargo.lock and cache are up to date --locked Require Cargo.lock is up to date --offline Run without accessing the network --config <KEY=VALUE> Override a configuration value -Z <FLAG> Unstable (nightly-only) flags to Cargo, see ' cargo -Z help' for details -h, --help Print help information Some common cargo commands are (see all commands with --list): build, b Compile the current package check, c Analyze the current package and report errors, but don' t build object files clean Remove the target directory doc, d Build this package 's and its dependencies' documentation new Create a new cargo package init Create a new cargo package in an existing directory add Add dependencies to a manifest file run, r Run a binary or example of the local package test, t Run the tests bench Run the benchmarks update Update dependencies listed in Cargo.lock search Search registry for crates publish Package and upload this package to the registry install Install a Rust binary. Default location is $HOME/.cargo/bin uninstall Uninstall a Rust binary See 'cargo help <command>' for more information on a specific command.
创建项目 编译项目 查看包文档
集合
动态数组(vector)可以让你连续地存储任意多个值。
字符串(string)是字符的集合。
哈希映射(hash map)可以让你将值关联到一个特定的键上,它是另外一种数据结构—映射 (map)的特殊实现。
动态数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pub fn m1 (){ let mut v :Vec <i32 > = Vec ::new (); println! ("v={:?}" ,v); for i in 0 ..10 { v.push (i); } println! ("v={:?}" ,v); let third : &i32 = &v[2 ]; println! ("The third element is {}" , third); match v.get (2 ) { Some (third) => println! ("The third element is {}" , third), None => println! ("There is no third element." ), } for i in &v { println! ("{}" , i); } }
字符串 String实际上是一个基于Vec的封装类型。
1 2 3 pub struct String { vec: Vec <u8 >, }
相关操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 pub fn m1 (){ let mut s = String ::new (); let data = "initial contents" ; let s = data.to_string (); let s = "initial contents" .to_string (); let hello = String ::from ("Dobrý den" ); let hello = String ::from ("Hello" ); let hello = String ::from ("こんにちは" ); let hello = String ::from ("안녕하세요" ); let hello = String ::from ("你好" ); let hello = String ::from ("Olá" ); let hello = String ::from ("Здравствуйте" ); let hello = String ::from ("Hola" ); let mut s = String ::from ("foo" ); s.push_str ("bar" ); println! ("s={}" ,s); let s1 = String ::from ("Hello, " ); let s2 = String ::from ("world!" ); let s3 = s1 + &s2; println! ("s2={}" ,s2); println! ("s2={}" ,s3); let s1 = String ::from ("hello" ); let s2 = String ::from ("world" ); let s3 = format! ("{}-{}" ,s1,s2); println! ("{}" ,s3); let len = s1.len (); println! ("hello len = {}" ,len); }
字符串切片
1 2 3 4 5 6 7 8 9 10 let hello = "hello rust" ;let slide = &hello[1 ..5 ];println! ("slide = {}" ,slide);for c in "hello world" .chars () { println! ("{}" , c); }for c in "hello world" .as_bytes () { println! ("{}" , c); }
哈希映射 创建哈希映射(首次插入会确定其kv值)
1 2 3 4 5 6 7 use std::collections::HashMap;let mut scores = HashMap::new (); scores.insert (String ::from ("Blue" ), 10 ); scores.insert (String ::from ("Yellow" ), 50 );let mut map = HashMap::new (); map.insert (1 ,1 );
生命周期
1 2 3 4 5 6 let field_name = String ::from ("Favorite color" );let field_value = String ::from ("Blue" );let mut map = HashMap::new (); map.insert (field_name, field_value);
案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 pub fn m1 (){ use std::collections::HashMap; let mut scores = HashMap::new (); scores.insert (String ::from ("Blue" ), 10 ); scores.insert (String ::from ("Yellow" ), 50 ); println! ("{:?}" ,scores); let teams = vec! [String ::from ("Blue" ), String ::from ("Yellow" )]; let initial_scores = vec! [10 , 50 ]; let scores2 : HashMap<_, _> = teams.iter ().zip (initial_scores.iter ()).collect (); let len = scores.len (); println! ("hashmap length = {}" ,len); let blue = scores.get ("Blue" ); println! ("blue = {:?}" ,blue); let field_name = String ::from ("Favorite color" ); let field_value = String ::from ("Blue" ); let mut map = HashMap::new (); map.insert (field_name, field_value); let mut scores = HashMap::new (); scores.insert (String ::from ("Blue" ), 10 ); scores.insert (String ::from ("Yellow" ), 50 ); let team_name = String ::from ("Blue" ); let score = scores.get (&team_name); println! ("score = {:?}" ,score); for (key, value) in &scores { println! ("{}: {}" , key, value); } scores.entry (String ::from ("Yellow" )).or_insert (50 ); scores.entry (String ::from ("Blue" )).or_insert (50 ); println! ("{:?}" , scores); let text = "hello world wonderful world" ; let mut map = HashMap::new (); for word in text.split_whitespace () { let count = map.entry (word).or_insert (0 ); *count += 1 ; } println! ("{:?}" , map); }
错误
错误处理
在Rust中,我们将错误分为两大类:可恢复 错误与不可恢复 错误。对于可恢复错误,比如文件未找到等,一般需要将它们报告给用户并再次尝试进行操作。而不可恢复错误往往就是bug的另一种说法, 比如尝试访问超出数组结尾的位置等。虽然Rust没有类似的异常机制,但它提供了用于可恢复错误的类型Result<T, E>
,以及在程序出现不可恢复错误时中止运行的panic! 宏。
不可恢复错误与panic! 程序会在panic!
宏执行时打印出一段错误提示信息,展开并清理当前的调用栈,然后退出程序。这种情况大部分都发生在某个错误被检测到,但程序员却不知该如何处理的时候。
当panic发生时,程序会默认开始栈展开。这意味着Rust会沿着调用栈的反向顺序遍历所有调用函数,并依次清理这些函数中的数据。但是为了支持这种遍历和清理操作,我们需要在二进制中存储许多额外信息。除了展开,我们还可以选择立即终止程序,它会直接结束程序且不进行任何清理工作,程序所使用过的内存只能由操作系统来进行回收。假如项目需要使最终二进制包尽可能小,那么你可以通过在Cargo.toml
文件中的[profile]
区域添加panic = 'abort'
来将panic的默认行为从展开切换为终止。例如,如果你想要在发布模式中使用终止模式,那么可以在配置文件中加入:
1 2 [profile.release] panic = 'abort '
案例
1 2 3 4 5 pub fn m1 (){ let v = vec! [1 , 2 , 3 ]; v[99 ]; }
可恢复错误与Result 大部分的错误其实都没有严重到需要整个程序停止运行的地步。 函数常常会由于一些可以简单解释并做出响应的原因而运行失败。例如,尝试打开文件的操作会因为文件不存在而失败。你也许会在这种情形下考虑创建该文件而不是终止进程。
1 2 3 4 5 6 7 8 9 pub fn m (){ let f = File::open ("hello.txt" ); let f = match f { Ok (file) => file, Err (error) => { panic! ("There was a problem opening the file: {:?}" , error) }, }; }
匹配不同的错误
1 2 3 4 5 6 7 8 9 10 11 12 13 pub fn m (){ let f = File::open ("hello.txt" ); let f = match f { Ok (file) => file, Err (error) => match error.kind () { ErrorKind::NotFound => match File::create ("hello.txt" ) { Ok (fc) => fc, Err (e) => panic! ("Tried to create file but there was a problem:{:?}" , e), }, other_error => panic! ("There was a problem opening the file: {:?}" , other_error), }, }; }
用不同的方式处理不同的错误类型
1 2 3 4 5 6 7 8 9 10 11 pub fn m (){ let f = File::open ("hello.txt" ).map_err (|error| { if error.kind () == ErrorKind::NotFound { File::create ("hello.txt" ).unwrap_or_else (|error| { panic! ("Tried to create file but there was a problem: {:?}" , error); }) } else { panic! ("There was a problem opening the file: {:?}" , error); } }); }
虽然使用match运行得很不错,但使用它所编写出来的代码可能会显得有些冗长,且无法较好地表明其意图。类型Result<T, E>本身也定义了许多辅助方法来应对各式各样的任务。其中一个被称为unwrap的方法实现了我们在示例9-4中编写的match表达式的效果。当Result的返回值是Ok变体时,unwrap就会返回Ok内部的值。而当Result的返回值是Err变体时,unwrap则会替我们调用panic! 宏。下面是一个在实际代码中使用unwrap的例子:
1 2 3 fn main () { let f = File::open ("hello.txt" ).unwrap (); }
还有另外一个被称作expect的方法,它允许我们在unwrap的基础上指定panic! 所附带的错误提示信息。使用expect并附带上一段清晰的错误提示信息可以阐明你的意图,并使你更容易追踪到panic的起源。下面演示了expect的使用语法:
1 2 3 4 use std::fs::File;fn main () { let f = File::open ("hello.txt" ).expect ("Failed to open hello.txt" ); }
什么时候应该使用panic!,而什么时候又应该返回Result呢?代码一旦发生panic,就再也没有恢复的可能了。只要你认为自己可以代替调用者决定某种情形是不可恢复的,那么就可以使用panic!,而不用考虑错误是否存在可以恢复的机会。
传播错误 1 2 3 4 5 6 7 8 9 10 11 12 fn read_username_from_file () -> Result <String , io::Error> { let f = File::open ("hello.txt" ); let mut f = match f { Ok (file) => file, Err (e) => return Err (e), }; let mut s = String ::new (); match f.read_to_string (&mut s) { Ok (_) => Ok (s), Err (e) => Err (e), } }
泛型 泛型解决的其实就是代码的复用性问题,针对一些可以共有的代码(入参类型不同但是逻辑一样)可以采用泛型来进行
泛型定义 在函数中定义 1 2 3 4 5 6 7 8 9 fn largest <T>(list: &[T]) -> T { let mut largest = list[0 ]; for &item in list.iter () { if item > largest { largest = item; } } largest }
在结构体中定义 1 2 3 4 5 6 7 8 9 struct Point <T> { x: T, y: T, }fn main () { let integer = Point { x: 5 , y: 10 }; let float = Point { x: 1.0 , y: 4.0 }; }
在枚举中定义 1 2 3 4 enum Option <T> { Some (T), None , }
在方法中定义 1 2 3 4 5 6 7 8 9 struct Point <T> { x: T, y: T, }impl <T> Point<T> { fn x (&self ) -> &T { &self .x } }
泛型原理 当你使用泛型参数时,你也许会好奇这种机制是否存在一定的运行时消耗。好消息是,Rust实现泛型的方式决定了使用泛型的代码与使用具体类型的代码相比不会有任何速度上的差异。 为了实现这一点,Rust会在编译时执行泛型代码的单态化(monomorphization)。单态化 是一个在编译期将泛型代码转换为特定代码的过程,它会将所有使用过的具体类型填入泛型参数从而得到有具体类型的代码。
在这个过程中,编译器所做的工作与我们在创建泛型函数时相反:它会寻找所有泛型代码被调用过的地方,并基于该泛型代码所使用的具体类型生成代码。
trait:定义共享行为 trait(特征)被用来向Rust编译器描述某些特定类型拥有的且能够被其他类型共享的功能,它使我们可以以一种抽象的方式来定义共享行为。我们还可以使用trait约束来将泛型参数指定为实现了某些特定行为的类型。
注意 :trait与其他语言中常被称为接口(interface)的功能类似,但也不尽相同。
定义 trait 类型的行为由该类型本身可供调用的方法组成。当我们可以在不同的类型上调用相同的方法时,我们就称这些类型共享了相同的行为。trait提供了一种将特定方法签名组合起来的途径,它定义了为达成某种目的所必需的行为集合。
打个比方,假如我们拥有多个结构体,它们分别持有不同类型、 不同数量的文本字段:其中的NewsArticle结构体存放了某地发生的新 闻故事,而Tweet结构体则包含了最多280个字符的推文,以及用于描述该推文是一条新推文、一条转发推文还是一条回复的元数据。
此时,我们想要创建一个多媒体聚合库,用来显示存储在NewsArticle或Tweet结构体实例中的数据摘要。为了达到这一目标, 我们需要为每个类型都实现摘要行为,从而可以在实例上调用统一的summarize方法来请求摘要内容。展示了用于表达这一行为的Summary trait定义。
1 2 3 pub trait Summary { fn summarize (&self ) -> String ; }
一个trait可以包含多个方法:每个方法签名占据单独一行并以分号结尾。
为类型实现trait 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 pub trait Summary { fn summarize (&self ) -> String ; }pub struct NewsArticle { pub headline: String , pub location: String , pub author: String , pub content: String , }impl Summary for NewsArticle { fn summarize (&self ) -> String { format! ("{}, by {} ({})" , self .headline, self .author, self .location) } }pub struct Tweet { pub username: String , pub content: String , pub reply: bool , pub retweet: bool , }impl Summary for Tweet { fn summarize (&self ) -> String { format! ("{}: {}" , self .username, self .content) } }pub fn m () { let n = NewsArticle{ headline: "123" .to_string (), location: "456" .to_string (), author: "789" .to_string (), content: "0" .to_string () }; let t = Tweet{ username: "1" .to_string (), content: "2" .to_string (), reply: false , retweet: false }; println! ("summarize = {}" ,n.summarize ()); println! ("summarize = {}" ,t.summarize ()); }
提供默认实现的trait
1 2 3 4 5 6 pub trait Summary { fn summarize (&self ) -> String { String ::from ("(Read more...)" ) } }
使用trait作为参数 1 2 3 pub fn notify (item: impl Summary ) { println! ("Breaking news! {}" , item.summarize ()); }
使用trait作为泛型约束 1 2 3 pub fn notify <T: Summary>(item: T) { println! ("Breaking news! {}" , item.summarize ()); }
通过+
语法来指定多个trait约束
1 2 3 pub fn notify <T: Summary + Display>(item: T) { }
使用where从句来简化trait约束
1 2 3 4 5 6 7 8 9 10 11 fn some_function0 <T: Display + Clone , U: Clone + Debug >(t: T, u: U) -> i32 { 0 }fn some_function1 <T, U>(t: T, u: U) -> i32 where T: Display + Clone , U: Clone + Debug { 0 }
使用trait来修饰重写largest
函数
1 2 3 4 5 6 7 8 9 fn largest <T: PartialOrd + Copy >(list: &[T]) -> T { let mut largest = list[0 ]; for &item in list.iter () { if item > largest { largest = item; } } largest }
使用trait约束来有条件地实现方法 通过在带有泛型参数的impl代码块中使用trait约束,我们可以单独为实现了指定trait的类型编写方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct Pair <T> { x: T, y: T, }impl <T> Pair<T> { fn new (x: T, y: T) -> Self { Self { x, y, } } }impl <T: Display + PartialOrd > Pair<T> { fn cmp_display (&self ) { if self .x >= self .y { println! ("The largest member is x = {}" , self .x); } else { println! ("The largest member is y = {}" , self .y); } } }
只有在内部类型T实现了PartialOrd(用于比较)与 Display(用于打印)这两个trait的前提下,才会实现cmd_display方法
生命周期 使用生命周期保证引用的有效性 Rust的每个引用都有自己的生命周期(lifetime),它对应着引用保持有效性的作用域。在大多数时候,生命周期都是隐式且可以被推导出来的,就如同大部分时候类型也是可以被推导的一样。当出现了多个可能的类型时,我们就必须手动声明类型。类似地,当引用的生命周期可能以不同的方式相互关联时,我们就必须手动标注生命周期。
Rust需要我们注明泛型生命周期参数之间的关系,来确保运行时实际使用的引用一定是有效的。
生命周期的概念不同于其他编程语言中的工具,从某种意义上说,它也是Rust最与众不同的特性。
使用生命周期来避免悬垂(空)引用 1 2 3 4 5 6 7 8 9 10 pub fn m () { let r ; { let x = 5 ; r = &x; } println! ("r = {}" ,r); }
借用检查器 Rust编译器拥有一个借用检查器 (borrow checker),它被用于比较不同的作用域并确定所有借用的合法性。
函数签名中的生命周期标注 生命周期的标注使用了一种明显不同的语法:它们的参数名称必须以撇号(')
开头,且通常使用全小写字符。与泛型一样,它们的名称通常也会非常简短。'a
被大部分开发者选择作为默认使用的名称。我们会将生命周期参数的标注填写在&引用运算符之后,并通过一个空格符来将标注与引用类型区分开来。
1 2 3 &i32 &'a i32 &'a mut i32
单个生命周期的标注本身并没有太多意义,标注之所以存在是为了向Rust描述多个泛型生命周期参数之间的关系。例如,假设我们编写了一个函数,这个函数的参数first是一个指向i32
的引用,并且拥有生命周期'a
。它的另一个参数second同样也是指向i32且拥有生命周期'a
的引用。这样的标注就意味着:first和second的引用必须与这里的泛型生命周期存活一样长的时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 fn longest <'a >(x: &'a str , y: &'a str ) -> &'a str { if x.len () > y.len () { x } else { y } }pub fn m (){ let s1 = "abc" ; let s2 = "opqsdf" ; let l = longest (&s1,&s2); println! ("the longest is : {}" ,l) }
结构体定义中的生命周期标注 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct ImportantExcerpt <'a > { part: &'a str , }pub fn m () { let novel = String ::from ("Call me Ishmael. Some years ago..." ); let first_sentence = novel.split ('.' ) .next () .expect ("Could not find a '.'" ); let i = ImportantExcerpt { part: first_sentence }; println! ("{}" , i.part); }
静态生命周期 Rust中还存在一种特殊的生命周期’static,它表示整个程序的执行期。所有的字符串字面量都拥有’static生命周期,我们可以像下面一样显式地把它们标注出来:
1 let s : &'static str = "I have a static lifetime." ;
字符串的文本被直接存储在二进制程序中,并总是可用的。因此,所有字符串字面量的生命周期都是’static。
同时使用泛型参数、trait约束与生命周期 1 2 3 4 5 6 7 8 fn longest_with_an_announcement <'a , T>(x: &'a str , y: &'a str , ann: T) -> &'a str where T: Display { println! ("Announcement! {}" , ann); if x.len () > y.len () { x } else { y } }
测试 1 2 3 4 5 6 7 8 9 10 11 12 13 #[cfg(test)] mod tests { use crate::gp::gp::longest; use super::*; #[test] fn it_works () { let s1 = "hello world" ; let s2 = "hello rust" ; let result = longest (&s1,&s2); assert_eq! (result, s1); } }
运行cargo test
会执行当前项目下所有的测试用例
1 2 3 4 5 running 2 tests test gp::traited::tests::test_same ... ok test gp::traited::tests::it_works ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00 s
should_panic 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 pub struct Guess { value: u32 , }impl Guess { pub fn new (value: u32 ) -> Guess { if value < 1 || value > 100 { panic! ("Guess value must be between 1 and 100, got {}." , value); } Guess { value } } }#[cfg(test)] mod tests { use super::*; #[test] #[should_panic] fn greater_than_100 () { Guess::new (200 ); } }
使用Result<T, E>编写测试 1 2 3 4 5 6 7 8 9 10 11 #[cfg(test)] mod tests { #[test] fn it_works () -> Result <(), String > { if 2 + 2 == 4 { Ok (()) } else { Err (String ::from ("two plus two does not equal four" )) } } }
控制测试的运行方式 并行或串行地进行测试
当你尝试运行多个测试时,Rust会默认使用多线程来并行执行它 们。这样可以让测试更快地运行完毕,从而尽早得到代码是否能正常 工作的反馈。但由于测试是同时进行的,所以开发者必须保证测试之 间不会互相依赖,或者依赖到同一个共享的状态或环境上,例如当前工作目录、环境变量等。
1 cargo test -- --test-threads=1
显示函数输出
1 cargo test -- --nocapture
只运行部分特定名称的测试
通过过滤名称来运行多个测试
通过显式指定来忽略某些测试
1 2 3 4 5 6 7 8 9 #[test] fn it_works () { assert_eq! (2 + 2 , 4 ); }#[test] #[ignore] fn expensive_test () { }
集成测试 1.创 建 一 个 tests 文 件 夹 , 并 创 建 文 件 tests/integration_test.rs
(对应的测试文件),将示例11-13中的代码输入其中。
1 2 3 4 5 use adder;#[test] fn it_adds_two () { assert_eq! (4 , adder::add_two (2 )); }
与单元测试不同,集成测试需要在代码顶部添加语句use adder。 这是因为tests 目录下的每一个文件都是一个独立的包,所以我们需要将目标库导入每一个测试包中。 我们不需要为tests/integration_test.rs
中的任何代码标注# [cfg(test)]
。Cargo对tests 目录进行了特殊处理,它只会在执行 cargo test
命令时编译这个目录下的文件。
案例 猜数字 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 use std::io;use rand::Rng;use std::cmp::Ordering;fn main () { guess () }fn guess () { println! ("guess number 1..10" ); println! ("please enter you number:" ); loop { 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); let secret_number = rand::thread_rng ().gen_range (1 ..=10 ); println! ("The secret number is: {}" , secret_number); match guess.cmp (&secret_number) { Ordering::Less => println! ("Too small!" ), Ordering::Greater => println! ("Too big!" ), Ordering::Equal => { println! ("You win!" ); break }, } } }
minigrep 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 use std::error::Error;use std::{env, fs};pub fn run (config:Config) -> Result <(), Box <dyn Error>>{ let contents = fs::read_to_string (config.filename)?; let results = if config.case_sensitive { search (&config.query, &contents) } else { search_case_insensitive (&config.query, &contents) }; for line in results { println! ("{}" , line); } Ok (()) }pub fn search <'a >(query: &str , contents: &'a str ) -> Vec <&'a str > { let mut ret = Vec ::new (); for line in contents.lines () { if line.contains (query) { ret.push (line); } } ret }pub fn search_case_insensitive <'a >(query: &str , contents: &'a str ) -> Vec <&'a str > { let query = query.to_lowercase (); let mut results = Vec ::new (); for line in contents.lines () { if line.to_lowercase ().contains (&query) { results.push (line); } } results }pub struct Config { pub query: String , pub filename: String , pub case_sensitive: bool , }impl Config { pub fn new (args: &[String ]) -> Result <Config, &'static str > { if args.len () < 3 { return Err ("not enough arguments" ); } let query = args[1 ].clone (); let filename = args[2 ].clone (); let case_sensitive = env::var ("CASE_INSENSITIVE" ).is_err (); Ok (Config { query, filename,case_sensitive }) } }#[cfg(test)] mod tests { use super::*; #[test] fn one_result () { let query = "duct" ; let contents = "\ Rust: safe, fast, productive. Pick three." ; assert_eq! ( vec! ["safe, fast, productive." ], search (query, contents) ); } #[test] fn case_insensitive () { let query = "rUsT" ; let contents = "\ Rust: safe, fast, productive. Pick three. Trust me." ; assert_eq! ( vec! ["Rust:" , "Trust me." ], search_case_insensitive (query, contents) ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 use std::{env, process};use minigrep::Config;use minigrep::run;fn main () { let args : Vec <String > = env::args ().collect (); let config = Config::new (&args).unwrap_or_else (|err| { eprintln! ("Problem parsing arguments: {}" , err); process::exit (1 ) }); println! ("Searching for = {}" , config.query); println! ("In file = {}" , config.filename); if let Err (e) = run (config) { eprintln! ("Application error: {}" , e); process::exit (1 ); } }
资料 官网:https://www.rust-lang.org/ 视频:https://www.bilibili.com/video/BV1hp4y1k7SV/ 案例:https://doc.rust-lang.org/stable/rust-by-example/ 书籍:rust权威指南.pdf 教程:https://course.rs/about-book.html