参考文献1:https://course.rs/first-try/installation.html
使用wsl2 ubuntu 20.04,照着教程直接
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
然后无脑回车安装完成。
基础1 牛刀小试
cargo new <project-name> # 创建新项目(自动git)
cargo run # 直接编译运行,可添加--release生成高性能代码
cargo build # 先编译,可添加--release生成高性能代码
./target/debug/<project-name> # 运行可执行文件
cargo check # 在不编译的情况下快速检查能否编译通过【神器】,但比Go差太远
Cargo.toml:项目描述,跟package.xml差不多。
依赖直接以如下形式给出:
[dependencies]
rand = "0.3" # 官方包,只需指定版本
hammer = { version = "0.5.0"} # 指定版本的另一种方法
color = { git = "https://github.com/bjz/color-rs" } # 网上的包,指定url
geometry = { path = "crates/geometry" } # 本地的包,指定文件路径
Cargo.lock:根据toml生成的项目以来详细清单,基本不需要手动修改。
基础2 Rust入门(乱序笔记,建议跳过)
let绑定的变量不可变,let mut的可变,并且可以一个let赋多个值。
我秉承实用主义解释方法,rust的话术后面深入了解之后再接受。
let {mut} <var-name>: <type>{ = <value>};
let ({mut} <var>, {mut} <var>, ...): (<type>, <type>, ...){ = (<value>, <value>, ...)};
这多个值实际上构成了一个复杂变量,但可以单独使用。这种随地凑出向量的搞法比较优雅。
下划线开头的变量不会因为未使用而被警告。
定义成mut但没变过的变量会被警告。
非常古典的类型名,i32、f64这种。
后定义的同名变量会把之前的覆盖,且不会报警。
数字可以在后面加上下划线(也可以不加)+类型规定为对应类型的数字。
序列用法:
1..5 // 代表1~4
1..=5 // 代表1~5
// 字符也可以用
fn func(var: type, ...) {
let x = {
let y = 3;
y * 2
};
x + 3
}
不加分号的表达式会返回一个值,可以作为函数返回值,也可以作为语块表达式返回值。
if-else语块也可以返回值,可以用来赋值。
Rust也可以用return,只不过可以不用。
let x = 5;
let y = x; // 直接拷贝值
let x = String::from("Hello"); // 堆上分配内存,创建String对象
let y = x; // 把x绑定的值的所有权移交给y,x被废黜
let x: &str = "Hello"; // 字符串字面量只读,放在静态数据区域,此处的x只是一个引用
let y = x; // 相当于x是一个指针,然后指针值拷贝给了x
基本类型注意:
let a = [1, 1, 4, 5, 1, 4]; // 用.len()获取数组长度
let b = &a[2..4]; // 切片以引用的方式获取,注意左开右闭。左右界限均可以省略。
let c = (1, 1, 4, 5); // 用c.2获取第3个数
let v = vec![10, 20, 30, 40]; // 宏定义向量
for e in v.iter_mut() { // 要允许值可变,要用iter_mut()
*e *= 2; // 用的是引用值,要用*解引用?
}
v.iter().map(|element| { // 对迭代器中每个元素应用一个匿名函数
element << 1
}).collect() // 将迭代器的结果收集起来组成新的集合
fn main() {
let vec0 = Vec::new();
let mut vec1 = fill_vec(vec0);
println!("{} has length {}, with contents: `{:?}`", "vec0", vec0.len(), vec0);
vec1.push(88);
println!("{} has length {}, with contents `{:?}`", "vec1", vec1.len(), vec1);
}
fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
// 这里把vec0的数据借调了,所以vec0访问不了。所以上面的fill_vec得是深拷贝。
let mut vec = vec;
vec.push(22);
vec.push(44);
vec.push(66);
vec
}
结构体的clone方法需要额外#[derive(Clone)]才能使用。
用impl <结构体名>来定义方法。
需要用&的情景:
- 借用数据,实际上相当于取地址。要使用数据(比如算术运算)时需要加*解引用,但是rust居然会自己解引用。感觉这个设计非常nt,落了下乘。也许我后面会理解的吧。
- 避免数据复制(传递复杂数据结构的引用)
- 共享不可变数据
- 可变引用&mut:每个数据同一时间只能有一个&mut存在,而且被引用的必须是mut变量。
- 参数中用&可以避免拥有数据所有权,特别是在只需要读取的时候。其实还是传地址,只是rust会自己解引用。
rust的枚举类型可以定义:
- 结构体变体,即不加struct前缀的三种结构体
- 方法,跟struct类似,要用impl在外面定义
- 用Nested(<其他枚举类型>)进行嵌套枚举
match的标准用法:
fn process_message(msg: Message) {
match msg {
Message::Quit => println!("Quit message"),
Message::ChangeColor(r, g, b) => {
println!("Change color to RGB({}, {}, {})", r, g, b);
}
Message::Move { x, y } => {
println!("Move to ({}, {})", x, y);
}
}
}
let a = "test";
let b = a.to_string(); // 字符串字面量转String
let a = String::from("test");
let b = &a; // String转字符串字面量:直接引用地址
不允许let mut (l, r) = (0, 0);,非常抽象。
调用成员方法的时候会自动解引用,与是否是函数参数无关。&会直接作用于调用过方法后的整体。
字符串操作:
let s = String::from("test");
// 遍历:直接得到字符值
for c in s.chars() {
if c == ' ' {
break;
}
}
s.replace(<要替换的字符串>, <要换成的字符串>);
s.replacen(<要替换的字符串>, <要换成的字符串>, <替换数目>);
// 只要有一个String类型,其他的即使是&str也可以相加
不加pub全是隐藏,包括use。
序列要遍历需要.iter(),但hashmap遍历不需要,自身就是一个集合。
遇到任何不需要转移值的地方都要打&。
Some()把一个变量变成optional变量。
if let和match都是模式匹配的方法。
如果被匹配的option变量在模式匹配后还要使用,那么不能够使用Some(v),而是要使用Some(ref v),避免所有权被夺走。
抛出错误用Err(String),不抛出用Ok(String),返回值都是Result<成功时返回的类型,错误时返回的类型>。
parse自己返回值就是Result类型。
?很厉害,对于返回Result的,如果Err就立刻返回Err,如果有值就取出值,继续执行;对于返回Option的,如果None就立刻返回None,如果有值就解出Some(v)的v,继续执行。
但使用?的函数必须是Result返回值,包括main本身。在非错误情形下,要调用Ok(())来返回一个空单元。
?遇到错误或None会立刻返回,但Err并不会,后续的代码会继续执行。
注意:只有函数结束时的表达式才能作为返回值。
用Result<type, Box<dyn std::error::Error>>装各种错误。
泛型写法:
- struct泛型直接在名字后面跟<>
- impl<T> Struct<T> {}里面的函数名字后面不用跟<>,调用的时候Struct::method()完全不用写<T>,大概是都可以自己推断?
trait感觉像C++的虚函数,声明但不实现。trait xxx由后面的impl xxx for type来实现。
但是trait似乎是任何对象都可以通用的,作为公共的.xxx()接口。
self是当前对象,Self是当前类型。
trait的默认函数跟C++虚函数一样,可以在声明虚函数处定义默认操作。
若只关心对象是否有特定trait,只需把对象类型写成impl <trait名称>即可。比较灵活。
要是关心对象是否有多个特定trait,只需一个impl,后面把trait名称用+连起来即可。
用#[should_panic(expected = "...")]捕获panic信息,使得panic不显示。即“预料之中的panic”。
unsafe操作:用unsafe <不安全函数定义>或者unsafe{<不安全运算/函数调用>},一般把&mut用as转成*mut使用。例程:
unsafe fn modify_by_address(address: usize) {
unsafe {
let ptr = address as *mut u32;
*ptr = 0xAABBCCDD;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_success() {
let mut t: u32 = 0x12345678;
unsafe { modify_by_address(&mut t as *mut u32 as usize) };
assert!(t == 0xAABBCCDD);
}
}
也可以用::into_raw()生成裸指针,然后用::from_raw()解引用。不过这个应该需要derive?
#[cfg(<逻辑表达式>)] 实际上就是条件编译。
cargo:rustc-env=<环境变量名>=<值> # 定义环境变量
cargo:rustc-cfg=<属性名>{=<值>} # 相当于define,值可以省略
extern引入的函数默认是unsafe的,
extern "<ABI类型>" {
<Rust下的函数声明>;
...
}
相关的函数attribute
#[no_mangle] // 不让外部函数覆盖本地函数名称
#[link_name = "xxx"] // 让这个外部函数在本地可以用xxx的名字被调用
生命周期标记:'<字母和数字的组合,或是下划线>。一般来说'a、'b这一类就够用了。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str // 让函数参数和返回值的生命周期一致
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str // 让返回值和第一个参数的生命周期一致
rust里的花括号不能乱加,因为作用域和生命周期强绑定。
对于形如
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is '{}'", result);
}
造成的生命周期不统一问题只能靠移动string2或者println!来实现,非常抽象。
struct Book<'a> {
++++
author: &'a str,
++
title: &'a str,
++
}
fn main() {
let name = String::from("Jill Smith");
let title = String::from("Fish Flying");
let book = Book { author: &name, title: &title };
// 此后未再用过name和title,于是name和title的符号被连同数据一起销毁
// 然而它们的数据还会被book使用,所以要让author和title的生命周期跟book一样长
// 即,author和title借来的数据不会在name和title的销毁时失去
println!("{} by {}", book.title, book.author);
}
一个典型的vec操作:
fn main() {
let my_fav_fruits = vec!["banana", "custard apple", "avocado", "peach", "raspberry"];
let mut my_iterable_fav_fruits = my_fav_fruits.iter(); // TODO: Step 1
assert_eq!(my_iterable_fav_fruits.next(), Some(&"banana"));
assert_eq!(my_iterable_fav_fruits.next(), Some(&"custard apple")); // TODO: Step 2
assert_eq!(my_iterable_fav_fruits.next(), Some(&"avocado"));
assert_eq!(my_iterable_fav_fruits.next(), Some(&"peach")); // TODO: Step 3
assert_eq!(my_iterable_fav_fruits.next(), Some(&"raspberry"));
assert_eq!(my_iterable_fav_fruits.next(), None); // TODO: Step 4
}
需要注意的是,一个.iter()得到的迭代器是空头的,即,要再next()一下才得到第一个元素。
沙比String只能+= &str,不能加String。感觉跟shell语言有相似之处,等号右侧使用的实际上是$var而不是var。
enum里的结构体要用<enum名>::<结构体名> {构造}来引用,而不是<enum名>(<结构体名> { .. })。
注意以下区别:
for ref x in set.iter() = for x in set.iter()
for mut x in set.iter() // 这种写法没用
for x in set.iter_mut() // 这才是正确的
这里面的x是迭代器,即&<set元素类型>,直接调用x.method()会自动解引用,但调用函数得用func(*x)。
不用for/while,纯用迭代器遍历有以下几种方法:
let mut vec = Vec::new();
let mut map = HashMap::new();
// for_each,此处写两种,以下只写vec
vec.iter().for_each(|element| {
<执行操作>
});
map.iter().for_each(|(key, value)| {
<执行操作>
});
vec.iter().map(|element| {
<执行操作>
element
}).collect(); // 执行体返回element的话相当于消耗了vec之后又得到一个vec
vec.iter().inspect(|&element| {
<执行操作>
}).count(); // 执行count来消费迭代器
// 对于字符串是string.chars()而不是iter(),后面差不多
最好用的还是for_each。
thread::spawn(|| {})借用外部变量,thread::spawn(move || {})获取外部变量所有权。
spawn返回handle,handle.join()跟踪线程,并获取Result形式的返回值。
let a = Arc::new(value); // 创建线程安全的共享变量
let b = Arc::new(Mutex::new(value)); // 创建线程安全的共享互斥变量
let c = a.lock().unwrap(); // 互斥变量只有lock了才能确定,共享变量必须unwrap
for x in xxx.iter()和for x in &xxx是等效的,不过传出来的东西有点区别。
定义结构体的时候可以类型名挨着<xxx>,但是调用的时候中间全都得加上::。
几种智能指针:
let a = <值>;
let shared_a = Arc::new(a); // 生成线程安全变量,并夺权
let thread_a = Arc::clone(&shared_a); // 对每个线程生成单独的变量
// 一般来说,只有.clone()和传引用可以不夺权,其余的都会夺权。
// Box = unique_ptr in C++
enum List {
Cons(i32, Box<List>),
Nil
} // Box可以允许嵌套定义
let a = Box::new(<值>);
println!("{}", *a); // 相当于生成值对应的指针,并且会自动释放
// Box相当于是Rust的常用指针
// Box<List>用于定义或声明,Box::<List>::..用于调用
let a = Cow::new(<变量>); // 直接夺权
let b = Cow::new(<引用>); // 借用,如果经由b想要修改数据,则夺权
// Rc = shared_ptr in C++
let a = Rc::new(<变量>); // 创建共享变量
let b = Rc::clone(&a); // 指向同一个变量
println!("{}", Rc::strong_count(&a)); // 输出引用计数
宏定义:
macro_rules! my_macro {
() => {
println!("Check out my macro!");
}; // 不同分支之间要用分号隔开
($val:expr) => {
println!("Look at this other macro: {}", $val);
}
}
可以定义在module里,宏上方写#[macro_export],然后在外面直接用my_macro!()调用。
Clippy很强,多用。
as可以用来转换类型,也可以用来改变引进的包名。
写类型转换:
use std::convert::{TryFrom, TryInto};
impl TryFrom<源类型> for 目标类型 {
fn try_from(参数: 源类型) -> Result<Self, Self::错误类型> {
...
}
}
let a = 目标类型::try_from(源类型对象);
let b: Result<目标类型, _> = 源类型对象.try_into(); // 允许失败的尝试类型转换,可以不写impl
对于泛型函数,用<T: AsRef<具体类型>>来在函数中使用as_ref(),用<T: AsMut<具体类型>>来在函数中使用as_mut()。这两种方法同时限制了泛型类型,因为例如AsRef<str>只有str类型有。
From/Into只需要定义其一就够了,TryFrom/TryInto同理。只是From/Into不会处理出错情况。
杂乱无章的知识学习结束了,接下来是算法部分。
基础3 算法实现
其实主要是数据结构,大部分算法所有语言都差不多。
冒泡排序:
fn sort<T: std::cmp::PartialOrd + Clone>(array: &mut [T]){
//TODO
let n = array.len();
for i in 0..n {
for j in i + 1..n {
if array[i] > array[j] {
// 单纯的变量交换和数组元素交换略有不同,前者第三步可以靠重新绑定tmp的值来
// 完成,不需要克隆两次,但array[j]不能let重绑定。
let tmp = array[i].clone();
array[i] = array[j].clone();
array[j] = tmp;
}
}
}
}