cisen / blog

Time waits for no one.
132 stars 20 forks source link

Rust 入门相关 #612

Open cisen opened 5 years ago

cisen commented 5 years ago

总结

入门

为什么

优点

缺点

相关文章

cisen commented 4 years ago

语法

基础数据类型

有符号整形:i8i16i32i64isize(指针大小) 无符号整形:u8u16u32u64usize(指针大小) 浮点类型:f32f64(与IEEE-754单双精度数字相对应。) 字符类型是Unicode值,像‘a’,'α' 和 '∞' (4 字节长) bool类型非true即否 单元类型(),同时值也为() 数组,例如[1, 2, 3] 元组,例如(1, true)

常用类型转化

x类型\至类型 i32 u32 f64 String
i32 n/a x as u32 x as f64 x.to_string()
u32 x as u32 n/a x as f64 x.to_string()
f64 x as i32 x as u32 n/a x.to_string()
String* x.parse().unwrap() x.parse().unwrap() x.parse().unwrap() n/a

*可以看到i32, u32, f64(i代表init,u代表dobble,f代表float) 到String类型是同一个函数,这样的话,如何来确定要转换至的具体类型?就是类型推导!当然,如果rust无法通过上下文推导出类型,就需要给出提示,例如x.parse::().unwrap()。但是通常情况下,都不需要提示即可自动推导出类型。

&str/String/collections::string::String

x类型\至类型 String &str
String n/a &*x
&str x.to_string() n/a

Vec/&[T]/Box<[T]>

x类型\至类型 Vec &[T] Box<[T]>
Vec n/a &x[..] x.into_boxed_slice()
&[T] x.to_vec() n/a Box::new(*x)
Box<[T]> x.to_vec() &*x n/a

箭头 ->

双冒号 ::

#

:单冒号

// inheritance trait FooBar : Foo { fn foobar(&self); }


## 指针`*` ` &` `ref` `ref mut`
- 为什么要有这些指针符号?
- - 因为可以不用管cpu是多少位的,也就是寄存器是多少位的,也就是不用管栈是多大的
- &相等与定义了一个新的变量,只是这个变量是一个指针,而且这个变量的生命周期跟宿主是独立的

### `*`
- 解除引用,表示获取地址(指针)指向的值
```rust
fn test_ref(){
    let mut num = 5;
    // 指针的地址值
    let num_ref = &mut num;
    // 获取该指针指向的地址的值
    *num_ref = 100;
    println!("{} sizeof &i32 {}", num, std::mem::size_of::<&i32>())
    // output 100 sizeof &i32:8
}

&

ref

?问号

// 由于Rust中没有Exception异常处理的语法, // Rust只有panic报错, 并且panic不允许被保护, 因为没有提供 try 这种语法. // Rust的异常处理是通过 Result 的 Ok 和 Err 成员来传递和包裹错误信息. // 然而错误信息的处理一般都是要通过match来对类型进行比较, 所以很多时候 // 代码比较冗余, 通过?符号来简化Ok和Err的判断.

// 下面的例子提供了一个不使用?符号 以及 一个使用?符号的样例代码. fn halves_if_even<'a >(i: i32) -> Result<i32, &'a str> { // 取数值的二分之一. if i % 2 == 0 { Ok(i/2) } else { Err("error") } } fn not_use_question_mark() { let a = 10; // 把这里改成 9 就会报错. let half = halves_if_even(a); let half = match half { Ok(item) => item, Err(e) => panic!(e), }; assert_eq!(half, 5); } fn use_question_mark<'a >() -> Result<i32, &'a str> { // 这里必须要返回Result let a = 10; let half = halves_if_even(a)?; // 因为?要求其所在的函数必须要返回Result assert_eq!(half, 5); Ok(half)
} fn main() { not_use_questionmark(); let = use_question_mark(); }

## 数据类型
- [i是有符号,u是无符号](https://rustlang-cn.org/office/rust/book/common-programming-concepts/ch03-02-data-types.html#%E6%A0%87%E9%87%8F%E7%B1%BB%E5%9E%8B)

## 模块系统
- [使用`pub`, `use`, `as`导出和引入模块](https://rustlang-cn.org/office/rust/book/packages-crates-and-modules/ch07-02-modules-and-use-to-control-scope-and-privacy.html#%E6%A8%A1%E5%9D%97%E7%B3%BB%E7%BB%9F%E7%94%A8%E6%9D%A5%E6%8E%A7%E5%88%B6%E4%BD%9C%E7%94%A8%E5%9F%9F%E5%92%8C%E7%A7%81%E6%9C%89%E6%80%A7)
- 默认会加载`main.rs`, `lib.rs`

## impl
- 是为了在struct、enum或者trait对象添加方法
- https://rustcc.gitbooks.io/rustprimer/content/quickstart/function-method.html

## trait
- **trait+impl相当于TS的interface+泛型,比如可以实现text跟button共享drow函数。struct+impl相当于interface,只能实现smallbutton,middlebutton。**
- trait是多个函数的集合,可以根据参数静态派发(编译前知道是那个函数)指定函数。怎么解析呢?
```rust
trait Hash {
    fn to_hash(&self) -> u64;
}
impl Hash for bool {
    fn to_hash(&self) -> u64 {
        if *self {
            1
        } else {
            0
        }
    }
}
impl Hash for i64 {
    fn to_hash(&self) -> u64 {
        &self as u64
    }
}
// T是泛型
fn print_to_hash<T: Hash>(t: T) {
    // t会根据数据类型调用不同的to_hash方法
    println!("hash {}", t.to_hash());
}

宏调用

#[inline]

label

你也许会遇到这样的情形,当你有嵌套的循环而希望指定你的哪一个 break 或 continue 该起作用。就像大多数语言,默认 break 或 continue 将会作用于当前层的循环。当你想要一个 break 或 continue 作用于一个外层循环,你可以使用标签来指定你的 break 或 continue 语句作用的循环。 如下代码只会在 x 和 y 都为奇数时打印他们:

'outer: for x in 0..10 {
    'inner: for y in 0..10 {
        if x % 2 == 0 { continue 'outer; } // continues the loop over x
        if y % 2 == 0 { continue 'inner; } // continues the loop over y
        println!("x: {}, y: {}", x, y);
    }
}

字符串

str 和 String

数组 array

泛型


## `#[derive(Debug)]`打印调试内容
- [trait](https://rustlang-cn.org/office/rust/book/structs/ch05-02-example-structs.html#%E9%80%9A%E8%BF%87%E6%B4%BE%E7%94%9F-trait-%E5%A2%9E%E5%8A%A0%E5%AE%9E%E7%94%A8%E5%8A%9F%E8%83%BD)

## 返回值
- `{}`表示[语句](https://rustlang-cn.org/office/rust/book/common-programming-concepts/ch03-03-how-functions-work.html#%E5%8C%85%E5%90%AB%E8%AF%AD%E5%8F%A5%E5%92%8C%E8%A1%A8%E8%BE%BE%E5%BC%8F%E7%9A%84%E5%87%BD%E6%95%B0%E4%BD%93),如果最后一句没带分号就是返回值
- **[最后一句带分号,不带差别很大](https://rustlang-cn.org/office/rust/book/common-programming-concepts/ch03-03-how-functions-work.html#%E5%85%B7%E6%9C%89%E8%BF%94%E5%9B%9E%E5%80%BC%E7%9A%84%E5%87%BD%E6%95%B0)**

## 引用和借用
- https://segmentfault.com/a/1190000002902858

## 内存管理(stack 和Box)
- http://wiki.jikexueyuan.com/project/rust/the-stack-and-the-heap.html
- http://wiki.jikexueyuan.com/project/rust/box-syntax-and-patterns.html
- https://www.bookstack.cn/read/rust-by-example-cn/src-std-box.md
- https://doc.rust-lang.org/stable/std/boxed/
### Box
- 用于分配堆内存

## 闭包
- https://rustcc.gitbooks.io/rustprimer/content/closure/syntax.html
- 其实可以不使用闭包。函数也是rust的第一等公民,函数作为参数给其他函数使用时类型是`fn()->type`
- rust中闭包根函数的最重要区别是:闭包可以读取外部变量,函数不可以读取函数外部的变量。这跟js是不一样的。rust中,闭包是匿名函数,但是定义根函数完全不一样

## 保留字
### match
- 相当于switch
- https://blog.csdn.net/teamlet/article/details/51001314
- http://wiki.jikexueyuan.com/project/rust-primer/match/match.html
- https://rustcc.gitbooks.io/rustprimer/content/quickstart/control-flow.html
- https://rustcc.gitbooks.io/rustprimer/content/quickstart/control-flow.html

```rust
let x = 5;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    4 => println!("four"),
    5 => println!("five"),
    _ => println!("something else"),
}
let day = 5;

match day {
  0 | 6 => println!("weekend"),
  1 ... 5 => println!("weekday"),
  _ => println!("invalid"),
}

其中|用于匹配多个值,...匹配一个范围 (包含最后一个值),并且_在这里是必须的, 因为match强制进行穷尽性检查 (exhaustiveness checking),必须覆盖所有的可能值。 如果需要得到|或者...匹配到的值,可以使用@绑定变量:

let x = 1;

match x {
    e @ 1 ... 5 => println!("got a range element {}", e),
    _ => println!("anything"),
}

&self:Self

enum 枚举

vec

loop

if/let,while/let

if let 语法让我们以一种不那么冗长的方式结合 if 和 let,来处理只匹配一个模式的值而忽略其他模式的情况。考虑示例 6-6 中的程序,它匹配一个 Option 值并只希望当值为 3 时执行代码:

let some_u8_value = Some(0u8);
match some_u8_value {
    Some(3) => println!("three"),
    _ => (),
}

示例 6-6:match 只关心当值为 Some(3) 时执行代码

我们想要对 Some(3) 匹配进行操作但是不想处理任何其他 Some 值或 None 值。为了满足 match 表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上 _ => (),这样也要增加很多样板代码。

不过我们可以使用 if let 这种更短的方式编写。如下代码与示例 6-6 中的 match 行为一致:

# let some_u8_value = Some(0u8);
if let Some(3) = some_u8_value {
    println!("three");
}

if let 获取通过等号分隔的一个模式和一个表达式。它的工作方式与 match 相同,这里的表达式对应 match 而模式则对应第一个分支。

使用 if let 意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去 match 强制要求的穷尽性检查。match 和 if let 之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。

换句话说,可以认为 if let 是 match 的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。

可以在 if let 中包含一个 else。else 块中的代码与 match 表达式中的 _ 分支块中的代码相同,这样的 match 表达式就等同于 if let 和 else。回忆一下示例 6-4 中 Coin 枚举的定义,其 Quarter 成员也包含一个 UsState 值。如果想要计数所有不是 25 美分的硬币的同时也报告 25 美分硬币所属的州,可以使用这样一个 match 表达式:

# #[derive(Debug)]
# enum UsState {
#    Alabama,
#    Alaska,
# }
#
# enum Coin {
#    Penny,
#    Nickel,
#    Dime,
#    Quarter(UsState),
# }
# let coin = Coin::Penny;
let mut count = 0;
match coin {
    Coin::Quarter(state) => println!("State quarter from {:?}!", state),
    _ => count += 1,
}

或者可以使用这样的 if let 和 else 表达式:

# #[derive(Debug)]
# enum UsState {
#    Alabama,
#    Alaska,
# }
#
# enum Coin {
#    Penny,
#    Nickel,
#    Dime,
#    Quarter(UsState),
# }
# let coin = Coin::Penny;
let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}

如果你的程序遇到一个使用 match 表达起来过于啰嗦的逻辑,记住 if let 也在你的 Rust 工具箱中。

pub enum RealFileName {
    Named(PathBuf),
    // 对于非虚拟化路径(即已映射到本地主机文件系统上适当位置的libstd路径),
    Devirtualized {
        // 具体到某操作系统的真正路径
        local_path: PathBuf,
        /// build artifacts.
        // 虚拟路径
        virtual_name: PathBuf,
    },
}
pub fn into_local_path(self) -> PathBuf {
        match self {
            // 如果匹配到RealFileName::Named, 或者RealFileName::Devirtualized 则取出p
            RealFileName::Named(p)
            | RealFileName::Devirtualized { local_path: p, virtual_name: _ } => p,
        }
    }

匿名函数

匿名函数 Rust使用闭包 (closure) 来创建匿名函数:

let num = 5;
let plus_num = |x: i32| x + num;

其中闭包plus_num借用了它作用域中的let绑定num。如果要让闭包获得所有权, 可以使用move关键字:

let mut num = 5;

{
    let mut add_num = move |x: i32| num += x;   // 闭包通过move获取了num的所有权

    add_num(5);
}

// 下面的num在被move之后还能继续使用是因为其实现了Copy特性
// 具体可见所有权(Owership)章节
assert_eq!(5, num);

Rust标准库 std

std::io

io::stdin()

std::io::Write;

std::io::Cursor

std::fmt (format的缩写)

write!

std::thread

Rust 通过 spawn 函数提供了创建本地操作系统(native OS)线程的机制,该函数的参数是一个转移闭包(moving closure)。 Rust默认会开4个线程,新开的线程另算。

use std::thread;

static NTHREADS: i32 = 10;

// 这是主(`main`)线程
fn main() {
    // 提供一个 vector 来存放所创建的子线程(children)。
    let mut children = vec![];

    for i in 0..NTHREADS {
        // 启动(spin up)另一个线程
        children.push(thread::spawn(move || {
            println!("this is thread number {}", i)
        }));
    }

    for child in children {
        // 等待线程到结束。返回一个结果。
        let _ = child.join();
    }
}

std::option::Option.expect和

default 默认类型

Option

Rust 并没有很多其它语言中有的空值功能。不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option,而且它定义于标准库中,如下:

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

空值(Null) 是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。

Option 已被 prelude 自动引用,不需要显式导入它,它的成员也是如此,不需要 Option::前缀来直接使用Some和None。即便如此 Option 也仍是常规的枚举,Some(T)和None仍是Option的成员。

如果使用 None 而不是 Some,需要告诉 Rust Option是什么类型的,因为编译器只通过None值无法推断出Some成员的类型。

let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None; // 这里必须声明类型

当有一个 Some 值时,我们就知道存在一个值,而这个值保存在 Some 中。当有个 None 值时,在某种意义上它跟空值是相同的意义:并没有一个有效的值。

Option 和 T(这里T可以是任何类型)是不同的类型,编译器不允许像一个被定义的有效的类型那样使用 Option。例如,这些代码不能编译,因为它尝试将 Option与i8相比:

let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y; // error no implementation for `i8 + Option<i8>`

当在 Rust 中拥有一个像i8这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需判空。只有当使用Option(或者任何用到的类型)是需要担心可能没有一个值,而编译器会确保我们在使用值之前处理为空的情况。

错误处理

学习rust的朋友可能经常看到Result和Option,虽然不一定直接看到他们本身,但是他们的方法还是常见的,比如:xxx.ok().expect(“…”); 这个xxx一般就是某个函数返回的Result类型了,下面就详细的讲解下他们的来源

现在看看rust book里的那个guess game,有这么一段: http://doc.rust-lang.org/book/guessing-game.html

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)
        .ok()
        .expect("Failed to read line");

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

前面几行都好理解,我们看看10~12行的几个ok,expect到底是什么

理解这些内容最好的方法就是看源代码,不过大家别慌,不是要你从头开始啃rust所有源码,只需要有针对性的看就可以了 大家记住下面这个常用的链接,这儿可以查到rust的所有源代码,可以搜索,反应速度非常快: https://doc.rust-lang.org/std/

1、io::stdin() 现在从头开始,先看io::stdin()这个是什么

这个网页的最上方就是输入框了,我们一步步来,先输入 io::stdin 注意:不要加后面括号 下面就是搜索的结果,可以看到第二行就是我们要找的函数(第一行是结构体,我们要找的是函数)

点击进去就可以查看源代码了,这个函数声明很简单,只是返回了一个Stdin的结构体

如果想看源代码就点右上角的那个src,见上图右上角红色框

我们现在不需要去看源代码了,现在看看Stdin的介绍就可以了,看图上左边的红色框里的Stdin是可以点击的,点进去然后找到read_line方法:

fn read_line(&mut self, buf: &mut String) -> Result[−]

Locks this handle and reads a line of input into the specified buffer.

For detailed semantics of this method, see the documentation on BufRead::read_line. 1 2 3 4 5 上面是read_line的介绍,我们不去关心他的实现过程了,先看看他返回的类型是:

Result 1 2、Result 点进去看看Result的页面,这个就是这篇blog的重点了

type Result = Result<T, Error>;

A type for results generated by I/O related functions where the Err type is hard-wired to io::Error.

This typedef is generally used to avoid writing out io::Error directly and is otherwise a direct mapping to std::result::Result. 1 2 3 4 5 上面的介绍部分说的是io::Result其实是为了书写方便定义的,他用io::Error类型替代了std::result::Result<T,Error>里的Error类型

这样io::Result比std::result::Result更加具体化了,那么写起来也相对简单了,他只可能返回io::Error类型的错误

因为这儿io::Result只是个类型定义,所以我们要去看std::result::Result的源代码,搜索过程就不详述了,具体看源码:

pub enum Result<T, E> { /// Contains the success value

[stable(feature = "rust1", since = "1.0.0")]

Ok(T),

/// Contains the error value
#[stable(feature = "rust1", since = "1.0.0")]
Err(E)

} 1 2 3 4 5 6 7 8 9 上面就是定义了,可以看到他是个enum,有OK和Err类型,分别对应了Result泛型里的类型T和E,std::result::Result里并没有限制E和T的类型,但是io::Result就把E的类型限制成了io::Error,这个大家注意下就好

说了这么多我们再看看问题

io::stdin().read_line(&mut guess) .ok() .expect("Failed to read line"); 1 2 3 刚才我们确认了read_line返回的是io::Result类型,那么ok()函数肯定就是Result的一个方法了,继续看std::result::Result的方法实现的源代码:

pub fn ok(self) -> Option<T> {
    match self {
        Ok(x)  => Some(x),
        Err(_) => None,
    }
}

1 2 3 4 5 6 3、Option 可以看到ok函数返回的是Option,还是没有返回我们最终想要的类型T,我们还是先看看ok的代码吧。

其实这个函数非常简单,就是一个match,如果没有出错用Option::Some把我们要的数据用包装下返回;如果出错了就返回Option::None

这样皮球又提到了Option去了…我们再继续查Option:

pub enum Option { None, Some(T), } 1 2 3 4 上面就是Option的定义,也是个enum。其中None顾名思义就是“没有”,他没有包装类型T,所以他真的什么都没。Some带来我们的类型T,所以现在目标已经很静了,只要把T对应的数据弄出来就最终得到了我们要的数据了

继续看这个代码,ok()返回的是Option,那么expect肯定就是Option的方法了

io::stdin().read_line(&mut guess) .ok() .expect("Failed to read line"); 1 2 3 继续看Option源代码,看他方法的实现:

pub fn expect(self, msg: &str) -> T {
    match self {
        Some(val) => val,
        None => panic!("{}", msg),
    }
}

1 2 3 4 5 6 终于看到想要的返回类型了:T,这个就是我们最终要的数据了,看read_line返回的是io::Result,所以这儿返回的是一个uszie类型的长度,不过guess game里并没有使用他

可以看到这个函数也是个match,如果是Some能匹配了就把他携带的数据返回;如果是None类型说明这个Option根本就没携带数据,也就是前面的Result出错了,所以会调用panic!宏把你传递进去的字符串打印出来并且退出程序。

4、其他写法 现在绕了一大圈终于找到T了,其实ok().expect(…);这种只是偷懒的写法,出错了直接打印你的字符串就退出程序了。当然有更偷懒的,看std::result::Result的代码:

pub fn unwrap(self) -> T { match self { Ok(t) => t, Err(e) => panic!("called Result::unwrap() on an Err value: {:?}", e) } } 1 2 3 4 5 6 7 那么代码就可以写成下面这样就可以了:

io::stdin().read_line(&mut guess).unwrap(); 1 这个是非常不建议的用法,除非你非常肯定read_line不可能出错

虽然这个read_line结果我们并没有使用,但是还是需要处理一下,不然Result没有处理编译器会给警告的

处理result最直接和直观的方法就是直接match,不需要通过Option中转了:

match io::stdin().read_line(&mut guess) { Ok(size)=>xxxx, Err(e)=>xxx, } 1 2 3 4 xxx处换成你自己的代码就可以了

cisen commented 4 years ago

问题收集

后缀文字 搜索后,我在same book中找到了这个解释:

… all number literals except the byte literal allow a type suffix, such as 57u8…

所以0u8是0作为无符号8位整数.

这些被称为“后缀文字”,并在Rust By Example详细讨论.

cisen commented 4 years ago

rust Option/Result相关

总结

enum Result<T, E> {
    Ok(T),
    Err(E),
}

空值(Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。

这句话有点绕。实际上用过c++的同学们应该知道c++内的NULL的值是0,例如int *ptr = NULL,这里ptr的值就是0,这里的0不是实际意义上的0,而是用来表示一个空值,ptr是一个空指针。但是我们知道计算机里没有空这种东西,所以c++取了0来表示逻辑意义上的空。之所以取0,是因为在大多数操作系统里,程序是不允许访问地址为0的内存的,因为该内存是操作西用保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。当然现在c++有nullptr了,关于nullptr和NULL,0的关系我这里就不讲了…不是我的重点。

之前提到,rust里面是没有空值的,那么空值有什么问题呢?它最大的问题就在于如果像使用非空值一样使用一个空值,会导致错误。然而空和非空的属性无处不在,所以非常容易出现这种错误。但是空值的概念仍然是有意的:

空值是一个因为某种原因目前无效或缺失的值。

因此虽然rust没有空值,但是仍然存在一个可以表达存在或不存在的概念的枚举,Option

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

我们注意到,这里使用了范型。也就是说Option枚举的Some成员可以是任意的数据类型。需要注意的是如果使用None而不是Some时,需要告诉rustOption的类型。因为编译器只通过None是无法推断出Some的类型的。即

let x: Option<i32> = None;

这里的None就是和空值表达类似的东西。那为什么要使用None而不是空值呢?这里有一个很重要的一点,Option和T不是同一个类型。不要小看着一个简单的区别,这意味着Option和T是不能直接进行运算的,即Option和i32是不能直接相加的。实际上,更进一步的,在对Option进行T的运算时,必须先将Option转化成T类型。如此一来就可以帮助我们避免以为值非空而实际为空的情况。例如下面这段代码:

fn main() {
    let a: i32 = 1;
    let b: Option<i32> = Some(5);
    let c = a + b;
    println!("{}", c);
}

编译器会报如下错误

error[E0277]: cannot add `std::option::Option<i32>` to `i32`
 --> src/main.rs:4:15
  |
4 |     let c = a + b;
  |               ^ no implementation for `i32 + std::option::Option<i32>`
  |
  = help: the trait `std::ops::Add<std::option::Option<i32>>` is not implementedfor `i32`

error: aborting due to previous error

我们必须先将b从Option类型转化为i32才能进行i32类型的运算。如下

fn main() {
    let a: i32 = 1;
    let b: Option<i32> = Some(5);
    let c = a + b.unwrap();
    println!("{}", c);
}

另外,每当我们引入一个可能为空的值时,我们必须先把它放到Option里。当我们使用这个值时,我们必须先处理值为空的情况。也就是说,只要一个值不是Option类型的,我们就可以认定它的值不为空。

这个设计相当有意思,我又查了一些资料发现scala里就存在这个设计,rust应该就是借鉴的scala的做法,无怪乎有人说rust参考了c/c++和haskell/scala两类语言。如果有机会应该多见识一下其他的语言,开阔下思路(虽然工作估计还是c/c++,(-_-||)

解耦Some(T)中T的值方式:

第一种:

let sp = Some(111);
if let Some(b) = sp{//if let 是rust语法糖形式
   println!("{}", b);
 }else{
    println!("其它");
 }

第二种:

let sp = Some(111);
let v = sp.unwrap();//111

第三种:

let mut x = Some(2);
match x.as_mut() {
    Some(v) => *v = 42,
    None => {},
}
assert_eq!(x, Some(42));
shark2202 commented 4 years ago

cisen commented 4 years ago

编译相关

build.rs

lib.rs

8 ./Cargo.toml 8 ./src/bin.rs 8 ./src/lib.rs 16 ./src

Cargo.toml:
```toml
[package]
name = "mything"
version = "0.0.1"
authors = ["me <me@gmail.com>"]

[lib]
name = "mylib"
path = "src/lib.rs"

[[bin]]
name = "mybin"
path = "src/bin.rs"

src/lib.rs:

pub fn test() {
    println!("Test");
}

src/bin.rs:

extern crate mylib; // not needed since Rust edition 2018

use mylib::test;

pub fn main() {
    test();
}
cisen commented 4 years ago

编译脚本支持(Build script support) https://www.cnblogs.com/yishuyuan/p/7455777.html 查看原文:Build Script Support

有些包需要编译第三方的非Rust代码,比如说C库。其他包需要链接到C库,这些C库可能位于系统磁盘上,也有可能需要从源代码中构建。其他人仍然需要一些功能,比如在构建之前的代码生成(考虑解释器生成器)(没明白是什么意思)。

Cargo并不是为了取代这些针对这些任务进行了良好优化的其他工具,但是它确实与构建配置选项集成在一起了:

一、如何使用构建脚本

在Cargo.toml文件中

[package]

...

build = "build.rs" 构建命令(相对于包的根)指定的Rust文件,将在其他任何在包中东西被编译之前被编译和调用,这能允许你的Rust代码依赖于构建或生成的组件。

注意:如果你没有为构建命令指定值,但是你的包根目录下确实存在一个"build.rs"文件,Cargo将会为你编译并调用这个文件。

构建命令的一些用例如下:

构建一个绑定的C库 在主机系统上查找一个C库 从规范中生成一个Rust模块 执行Crate需要的任何针对特定平台的配置 下面详细介绍每个用例,以给出构建命令如何工作的示例。

一、构建脚本的输入

当构建脚本运行时,构建脚本有大量的输入,所有的输入都以环境变量的形式传递。而除了环境变量之外,构建脚本的当前目录是构建脚本包的源目录。

二、构建脚本的输出

通过构建脚本输出到stdout的所有行,都会被写到文件中如:target/debug/build//stdout(具体位置可能取决于你的配置)。任何以 "cargo:" 开头的行都会被Cargo直接解释。这一行必须是这种格式:

cargo:key=value

如下所示:

复制代码

specially recognized by Cargo

cargo:rustc-link-lib=static=foo cargo:rustc-link-search=native=/path/to/foo cargo:rustc-cfg=foo cargo:rustc-env=FOO=bar

arbitrary user-defined metadata

cargo:root=/path/to/foo cargo:libdir=/path/to/foo/lib cargo:include=/path/to/foo/include 复制代码 另外,打印到stderr的行会被写入到一个文件:target/debug/build//stderr,但是不会被Cargo解释。

Cargo会识别一些特殊的关键字,会一定程度上影响crate的构建方式:

rustc-link-lib=[KIND=]NAME 表示指定的值是一个库名,应该将其作为 -l 标识传递给编译器。可选的KIND可以是static,dylib(默认),或者framework(只适用于苹果系统),可以通过rustc --help查看更多细节。 rustc-link-search=[KIND=]PATH 表示指定的值是一个库的搜索路径,应该将其作为 -L 标识传递给编译器。可选的KIND可以是dependency,crate,native,framework或者是all(默认),查看rustc --help获取更多细节。 rustc-flags=FLAGS 是传递给编译器的一组标识,只有 -l 和 -L 标识被支持。 rustc-cfg=FEATURE 表示指定的特性将会被以一个 --cfg 标识传递给编译器。这通常用于进行各种特性的编译时检测。 rust-env=VAR=VALUE 表示指定的环境变量将被添加到编译器在其中运行的环境中。然后在编译的crate中,可以通过 env! 宏检索这个环境变量。这对于在crate的代码中嵌入额外的元数据非常有用,比如Git HEAD的hash或者持续集成服务器的唯一标识符。 rerun-if-changed=PATH 是一个文件或目录的路径,该路径指示构建脚本在更改时应该重新运行(通过文件的最近修改时间戳检测)。通常情况下,如果在crate的根目录下的任何文件发生变化,那么构建脚本就会重新运行,但是这可以用于将更改范围更改为一小部分文件。(如果该路径指向一个目录,那么整个目录将不会被遍历更改--而只更改目录本身的时间戳(对应于目录中的某些类型的更改,这取决于平台),这将触发重新构建。如果要请求对整个目录中的任何更改都进行重新构建,请递归的为该目录打印一行,并且为其内部的所有内容打印另一行)。注意,如果构建脚本本身(或它的一个依赖项)发生了更改,那么它将被无条件的重新构建和重新运行,因此:cargo:rerun-if-changed=build.rs几乎总是冗余的(除非你想忽略除build.rs之外所有其他文件的更改)。 rurun-if-env-changed=VAR 是一个环境变量的名称,它表示如果环境变量的值发生变换,应该重新运行构建脚本。这基本上工作方式与 rerun-if-changed 相同,只不过它与环境变量一起工作。注意,这里的环境变量是为诸如CC之类的全局环境变量而设计的,因此对于env

cisen commented 4 years ago

CORE 核心库

ptr 指针操作

copy_nonoverlapping

cell

Cell

slice

from_raw_parts

from_raw_parts_mut

mem

size_of

llvm_asm

llvm-as 是LLVM汇编程序。它读取包含人类可读LLVM汇编语言的文件,将其转换为LLVM字节码,并将结果写入文件或标准输出。

cisen commented 4 years ago

furures

async

cisen commented 3 years ago

生命周期

总结

使用

&i32        // 常规引用
&'a i32     // 含有生命周期注释的引用
&'a mut i32 // 可变型含有生命周期注释的引用
// 错误
fn longer(s1: &str, s2: &str) -> &str {
    if s2.len() > s1.len() {
        s2
    } else {
        s1
    }
}
// 可以,使用泛型来声明生命周期的名称,随后函数返回值的生命周期将与两个参数的生命周期一致
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s2.len() > s1.len() {
        s2
    } else {
        s1
    }
}
// 经典函数,使用了泛型、特性、生命周期机制
use std::fmt::Display;

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

在以下三种情况可以自动推导生命周期

问答

为什么要有生命周期?

cisen commented 2 years ago

rust

Empowering everyone to build reliable and efficient software.

特点

与 webassembly 结合

方向

安装

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
brew install rustup

rustup update # 更新
rustup self uninstall

rustc --version
cargo --version

rustup component add rustfmt --toolchain stable-x86_64-apple-darwin

rustup doc # 本地阅读核心文档

rustc  main.rs

./main

配置

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

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

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

[source.rustcc] registry = "git://crates.rustcc.cn/crates.io-index"

[source.aliyun] registry = "https://code.aliyun.com/rustcc/crates.io-index"

rustup换源

上海交通大学

export RUSTUP_DIST_SERVER=https://mirrors.sjtug.sjtu.edu.cn/rust-static export RUSTUP_UPDATE_ROOT=https://mirrors.sjtug.sjtu.edu.cn/rust-static/rustup

清华大学

echo 'export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup' >> ~/.bash_profile RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup

中国科学技术大学

RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static

官方

RUSTUP_UPDATE_ROOT=https://static.rust-lang.org/rustup RUSTUP_UPDATE_ROOT=https://static.rust-lang.org


### 环境

- [Playground](https://play.rust-lang.org/) 设置一些限制保护站点不被恶意使用
  - 将代码保存到 GitHub Gist 中,便于用户分享
- VSCode
  - rust-analyzer 实时编译和分析你的 Rust 代码,提示代码中的错误,并对类型进行标注。也可以使用官方的 rust 插件取代
  - rust syntax 为代码提供语法高亮
  - crates 帮助分析当前项目的依赖是否是最新的版本。
  - better toml Rust 使用 toml 做项目的配置管理。可以语法高亮,并展示 toml 文件中错误
  - rust test lens 快速运行某个 Rust 测试
  - Tabnine 基于 AI 自动补全,帮助更快地撰写代码

## [Cargo](https://github.com/rust-lang/cargo)

- Rust’s build system and package manager. <https://doc.rust-lang.org/cargo>
- 无法获取最新依赖版本
  - 提示版本不是真实使用版本
- 使用
  - cargo new 命令创建新项目模板
  - cargo build 编译项目
  - cargo run 命令编译并运行项目
  - cargo test 命令测试项目
  - cargo check 命令检查项目类型
  - cargo doc 命令编译项目的文档
  - cargo publish 命令将库发布到 crates.io

```sh
# Cargo.toml
cargo --verison
cargo new project
cargo run|check|update
cargo build --realse

cargo install wasm-pack          # Compile Rust to Wasm and generate JS interop code
cargo install cargo-make         # Task runner
cargo install simple-http-server # Simple server to serve assets

rustup component add rls --toolchain stable-x86_64-apple-darwin

Project Setup

cargo new --lib rustmart && cd rustmart

模块系统

第三方箱存储库 crates.io

Syntax

Const

Variables

类型系统

类型安全

类型推导

通过泛型支持参数多态
pub enum Cow<'a, B: ?Sized + 'a> where B: ToOwned,
{
    // 借用的数据
    Borrowed(&'a B),
    // 拥有的数据
    Owned(<B as ToOwned>::Owned),
}

通过 trait 支持特设多态

用 trait object 支持子类型多态

Data Types

![[../_static/rust_data_type.jpg]] ![[../_static/rust_extend_type.jpg]]

Boolean

Number

长度 有符号 无符号
8 bit i8 u8
16 bit i16 u16
32 bit i32 u32
64 bit i64 u64
128 bit i128 u128
与体系结构相关 isize usize
// Addition, Subtraction, and Multiplication
println!("1 + 2 = {} and 8 - 5 = {} and 15 * 3 = {}", 1u32 + 2, 8i32 - 5, 15 * 3);

// Integer and Floating point division
println!("9 / 2 = {} but 9.0 / 2.0 = {}", 9u32 / 2, 9.0 / 2.0);

String

元组 tuple

结构 struct

// Classic struct with named fields
struct Student { name: String, level: u8, remote: bool }

// Tuple struct with data types only
struct Grades(char, char, char, char, f32);

// Unit struct
struct Unit;

// Instantiate classic struct, specify fields in random order, or in specified order
let user_1 = Student { name: String::from("Constance Sharma"), remote: true, level: 2 };
let user_2 = Student { name: String::from("Dyson Tan"), level: 5, remote: false };

// Instantiate tuple structs, pass values in same order as types defined
let mark_1 = Grades('A', 'A', 'B', 'A', 3.75);
let mark_2 = Grades('B', 'A', 'A', 'C', 3.25);

println!("{}, level {}. Remote: {}. Grades: {}, {}, {}, {}. Average: {}",
         user_1.name, user_1.level, user_1.remote, mark_1.0, mark_1.1, mark_1.2, mark_1.3, mark_1.4);
println!("{}, level {}. Remote: {}. Grades: {}, {}, {}, {}. Average: {}",
         user_2.name, user_2.level, user_2.remote, mark_2.0, mark_2.1, mark_2.2, mark_2.3, mark_2.4);

枚举 enum

enum WebEvent {
    // An enum variant can be like a unit struct without fields or data types
    WELoad,
    // An enum variant can be like a tuple struct with data types but no named fields
    WEKeys(String, char),
    // An enum variant can be like a classic struct with named fields and their data types
    WEClick { x: i64, y: i64 }
}

// Define a tuple struct
struct KeyPress(String, char);

// Define a classic struct
struct MouseClick { x: i64, y: i64 }

// Redefine the enum variants to use the data from the new structs
// Update the page Load variant to have the boolean type
enum WebEvent { WELoad(bool), WEClick(MouseClick), WEKeys(KeyPress) }

let we_load = WebEvent::WELoad(true);
// Instantiate a MouseClick struct and bind the coordinate values
let click = MouseClick { x: 100, y: 250 };

// Set the WEClick variant to use the data in the click struct
let we_click = WebEvent::WEClick(click);

// Instantiate a KeyPress tuple and bind the key values
let keys = KeyPress(String::from("Ctrl+"), 'N');

// Set the WEKeys variant to use the data in the keys tuple
let we_key = WebEvent::WEKeys(keys);

Array

// Declare array, initialize all values, compiler infers length = 7
let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

// Declare array, first value = "0", length = 5
let bytes = [0; 5];

let arr = [1, 2, 3];
assert_eq!(arr[..], [1, 2, 3]);
assert_eq!(arr[0..=1], [1, 2]);

Vector

// Create empty vector, declare vector mutable so it can grow and shrink
let mut fruit = Vec::new();
// Push values onto end of vector, type changes from generic `T` to String
fruit.push("Apple");
fruit.push("Banana");
fruit.push("Cherry");
println!("Fruits: {:?}", fruit);
println!("Pop off: {:?}", fruit.pop());
println!("Fruits: {:?}", fruit);
泛型
// Declare vector, initialize with three values
let three_nums = vec![15, 3, 46];
println!("Initial vector: {:?}", three_nums);

// Declare vector, first value = "0", length = 5
let zeroes = vec![0; 5];
println!("Zeroes: {:?}", zeroes);

哈希映射

use std::collections::HashMap;
let mut reviews: HashMap<String, String> = HashMap::new();

reviews.insert("Ancient Roman History".to_string(), "Very accurate.".to_string());
reviews.insert("Cooking with Rhubarb".to_string(), "Sweet recipes.".to_string());
reviews.insert("Programming in Rust".to_string(), "Great examples.".to_string());

// Look for a specific review
let book: &str = "Programming in Rust";
println!("\nReview for \'{}\': {:?}", book, reviews.get(book));

reviews.insert("Ancient Roman History".to_string(), "Very accurate.".to_string());

Object

智能指针

control loop 控制流程

let mut counter = 1;
// stop_loop is set when loop stops
let stop_loop = loop {
    counter *= 2;
    if counter > 100 {
        // Stop loop, return counter value
        break counter;
    }
};
// Loop should break when counter = 128
println!("Break the loop at counter = {}.", stop_loop);

fn process_event(event: &Event) {
    match event {
        Event::Join((uid, _tid)) => println!("user {:?} joined", uid),
        Event::Leave((uid, tid)) => println!("user {:?} left {:?}", uid, tid),
        Event::Message((_, _, msg)) => println!("broadcast: {}", msg),
    }

 if let Event::Message((_, _, msg)) = event {
  println!("broadcast: {}", msg);
 }
}

函数 function

trait

错误处理

变量所有权

Copy

Borrow

fn main() {
    let data = vec![1, 2, 3, 4];
    let data1 = &data;
    // 值的地址是什么?引用的地址又是什么?
    println!(
        "addr of value: {:p}({:p}), addr of data {:p}, data1: {:p}",
        &data, data1, &&data, &data1
    );
    println!("sum of data1: {}", sum(data1));

    // 堆上数据的地址是什么?
    println!(
        "addr of items: [{:p}, {:p}, {:p}, {:p}]",
        &data[0], &data[1], &data[2], &data[3]
    );
}

fn sum(data: &Vec<u32>) -> u32 {
    // 值的地址会改变么?引用的地址会改变么?
    println!("addr of value: {:p}, addr of ref: {:p}", data, &data);
    data.iter().fold(0, |acc, x| acc + x)
}

//  data1、&data 和传到 sum() 里 data1’ 都指向 data 本身,值的地址是固定的
// 引用的地址都是不同的,印证 Copy trait 的时候,介绍过只读引用实现了 Copy trait,也就意味着引用的赋值、传参都会产生新的浅拷贝

![[../_static/principle_law.png]]

绕过“一个值只有一个所有者”限制

Rc<T>
Rc<Refcell<T>>

Arc<T>
Arc<Mutex<T>>
Arc<RwLock<T>>

生命周期 lifetime

![[../_static/value_lifecycle.png]]

内存管理

教程

Rust 编程第一课 · 陈天

前置知识篇

回顾软件开发基础概念:堆、栈、函数、闭包、虚表、泛型、同步和异步等。学好任意一门编程语言,首先要吃透涉及概念,因为编程语言,不过是这些概念的具体表述和载体。

内存:值放堆上还是放栈上

![[../_static/mem_stack_struct.png]]

串讲:编程开发中需要掌握的基本概念

基础知识篇

cargo install cargo-edit
cargo add anyhow colored jsonxf mime
cargo add clap --allow-prerelease
cargo add reqwest --features json
cargo add tokio --features full

cargo build --quiet && target/debug/httpie httpbin.org/post post a=1 b=2
cargo run -- post https://httpbin.org/post a=1 b=2

../httpie/target/debug/httpie get "http://localhost:3000/image/CgoKCAjYBBCgBiADCgY6BAgUEBQKBDICCAM/https%3A%2F%2Fimages%2Epexels%2Ecom%2Fphotos%2F2470905%2Fpexels%2Dphoto%2D2470905%2Ejpeg%3Fauto%3Dcompress%26cs%3Dtinysrgb%26dpr%3D2%26h%3D750%26w%3D1260"

进阶篇

并发篇

实战篇

测试

代码统计

项目

教程

图书

工具

框架

参考

cisen commented 2 years ago

The First Rust Class #开篇词 rust_learning_routes https://naturellee.github.io/v1/rust/RustFirstClass.html#%E5%BC%80%E7%AF%87%E8%AF%8D

#学习 Rust 的难点 Rust 中最大的思维转换就是变量的所有权和生命周期 #如何学好 Rust? firstPrinciple

#1. 精准学习 深挖一个个高大上的表层知识点,回归底层基础知识的本原,再使用类比、联想等方法,打通涉及的基础知识;然后从底层设计往表层实现,一层层构建知识体系 第一性原理:回归事物最基础的条件,将其拆分成基本要素解构分析,来探索要解决的问题。 #2. 刻意练习 用精巧设计的例子,通过练习进一步巩固学到的知识,并且在这个过程中尝试发现学习过程中的不自知问题,让自己从“我不知道我不知道”走向“我知道我不知道”,最终能够在下一个循环中弥补知识的漏洞。

#前置篇 #内存 每个线程分配一个 stack,每个进程分配一个 heap。stack 是线程独占,heap 是线程共用。 stack 大小是确定的,heap 大小是动态的。

栈上存放的数据是静态的,固定大小,静态生命周期;堆上存放的数据是动态的,不固定大小,动态生命周期。

#栈 栈是自顶向下增长; 每当一个函数被调用时,一块连续的内存(帧 frame)就会在栈顶被分配出来; 一个新的帧会分配足够的空间存储寄存器的上下文; 在编译时,一切无法确定大小或者大小可以改变的数据,都无法安全地放在栈上,最好放在堆上。 栈上的内存在函数调用结束之后,所使用的帧被回收,相关变量对应的内存也都被回收待用。 所以栈上内存的生命周期是不受开发者控制的,并且局限在当前调用栈。 对于存入栈上的值,它的大小在编译期就需要确定。栈上存储的变量生命周期在当前调用栈的作用域内,无法跨调用栈引用。 #堆 堆可以存入大小未知或者动态伸缩(动态大小、动态生命周期)的数据类型。 堆上分配出来的每一块内存需要显式地释放,这就使堆上内存有更加灵活的生命周期,可以在不同的调用栈之间共享数据。 #堆内存自动管理方式 Tracing GC: tracing garbage collection; 追踪式垃圾回收 ARC: Automatic Reference Counting; 自动引用计数 #数据 #值和类型 值是无法脱离具体的类型讨论的 #类型 原生类型

字符、整数、浮点数、布尔值、数组(array)、元组(tuple)、指针、引用、函数、闭包 所有原生类型大小都是固定的,因此它们可以被分配到栈上。 组合类型

结构体(structure type) -- struct 标签联合(tagged union) -- enum #指针和引用 指针是一个持有内存地址的值,可以通过 derefence 来访问它指向的内存地址,理论上可以解引用到任意数据类型。 比正常指针携带更多信息的指针称为胖指针。 #代码 #函数,方法,闭包 函数也是对代码中重复行为的抽象。 面向对象的编程语言中,在类或者对象中定义的函数,被称为方法(method)。方法往往和对象的指针发生关系 闭包引用的上下文中的自由变量,会被捕获到闭包的结构中,成为闭包类型的一部分。 #接口,虚表 作为一个抽象层,接口将使用方和实现方隔离开来,使两者不直接有依赖关系,大大提高了复用性和扩展性 在生成这个引用的时候,我们需要构建胖指针,除了指向数据本身外,还需要指向一张涵盖了这个接口所支持方法的列表。这个列表,就是我们熟知的虚表(virtual table)。 虚表一般存储在堆上 ??? 虚表是每个 impl TraitA for TypeB {} 时就会编译出一份。 比如 String 的 Debug 实现, String 的 Display 实现各有一份虚表,它们在编译时就生成并放在了二进制文件中(大多是 RODATA 段中)。 所以虚表是每个 (Trait, Type) 一份。并且在编译时就生成好了 #运行方式 #同步,异步 #编程范式 #泛型编程 #缺陷

#学习资料 rust book(opens new window) rustnomicon rust 死灵书(opens new window) docs.rs(opens new window) 标准库文档(opens new window) #基础篇 Rust 是一门基于表达式(expression-based)的语言 Rust is an expression-oriented language. 语句(Statements)是执行一些操作但不返回值的指令。表达式(Expressions)计算并产生一个值 #基本语法和基础数据类型 变量类型一般可以省略; const/static 变量必须声明类型; 函数参数的类型和返回值的类型都必须显示定义; 宏编程的主要流程就是实现若干 From 和 TryFrom

#所有权和生命周期 核心点:Rust 通过单一所有权来限制任意引用的行为

Copy trait 与 Drop trait 不能共存。 所有权转移时,优先使用 copy 语义, 默认使用 move 语义。 #所有权规则 一个值只能被一个变量所拥有,这个变量被称为所有者 一个值同一时刻只能有一个所有者 当所有者离开作用域,其拥有的值被丢弃 #Move 语义: 赋值或者传参会导致值 Move,所有权被转移,一旦所有权转移,之前的变量就不能访问。 #Copy 语义和 Clone 语义 符合 Copy 语义的类型,在你赋值或者传参时,值会自动按位拷贝。 原生类型,包括函数、不可变引用和裸指针实现了 Copy; 数组和元组,如果其内部的数据结构实现了 Copy,那么它们也实现了 Copy; 可变引用没有实现 Copy; 非固定大小的数据结构,没有实现 Copy。 Copy 语义仅拷贝栈上的内存。 Clone trait 是 copy 的 super trait, 深拷贝, 深拷贝得到的堆内存需用通过 Drop trait 来释放。 任何有资源需要释放(Drop trait)的数据结构,都无法实现 Copy trait #Borrow 语义 Borrow 语义通过引用语法(& 或者 &mut)来实现; 在 Rust 下,所有的引用都只是借用了“临时使用权”,它并不破坏值的单一所有权约束。 默认情况下,Rust 的借用都是只读的; Rust 所有的参数传递都是传值; 借用的生命周期及其约束: 借用不能超过(outlive)值的生存期。 在一个作用域内,仅允许一个活跃的可变引用 在一个作用域内,活跃的可变引用(写)和只读引用(读)是互斥的,不能同时存在。

#多个所有者 Rust 处理很多问题的思路:编译时,处理大部分使用场景,保证安全性和效率;运行时,处理无法在编译时处理的场景,会牺牲一部分效率,提高灵活性。 Arc(Atomic Reference Counter); Rc(Reference Counter): 对一个 Rc 结构进行 clone(),不会将其内部的数据复制,只会增加引用计数。Rc 是一个只读的引用计数器 Box::leak(),它创建的对象,从堆内存上泄漏出去,不受栈内存控制,是一个自由的、生命周期可以大到和整个进程的生命周期一致的对象。 Box

#内部可变性 Rc<RefCell>针对单线程 Arc<Mutex>/Arc<RwLock>针对多线程环境

#生命周期 一般来说,堆内存的生命周期,会默认和其栈内存的生命周期绑定在一起。 生命周期参数,描述的是参数和参数之间、参数和返回值之间的关系,并不改变原有的生命周期。 所有引用类型的参数都有独立的生命周期 'a 、'b 等。 如果只有一个引用型输入,它的生命周期会赋给所有输出。 如果有多个引用类型的参数,其中一个是 self,那么它的生命周期会赋给所有输出。 动态、静态生命周期

#类型系统 类型系统是一种对类型进行定义、检查和处理的工具; 类型,是对值的区分,它包含了值在内存中的长度、对齐以及值可以进行的操作等信息; Rust 下的内存安全更严格:代码只能按照被允许的方法和被允许的权限,访问它被授权访问的内存; Rust 中除了 let / fn / static / const 这些定义性语句外,都是表达式,而一切表达式都有类型; unit 是只有一个值的类型,它的值和类型都是 (); 即使上下文中含有类型的信息,也需要开发者为变量提供类型,比如常量和静态变量的定义;需要明确的类型声明。 原生类型: 组合类型: Rust 类型系统:

#多态 参数多态:代码操作的类型是一个满足某些约束的参数,而非具体的类型;=> 泛型 Rust Generic 特设多态: 一般指函数的重载;包括运算符重载 => Rust Trait 子类型多态:在运行时,子类型可以被当成父类型使用。=> Rust Trait Object #泛型数据结构 函数,是把重复代码中的参数抽取出来; 泛型,是把重复数据结构中的参数抽取出来; 生命周期标注也是泛型的一部分

#单态化 好处: 泛型函数的调用是静态分派(static dispatch); 缺点 1: 编译速度慢;一个泛型函数,编译器需要找到所有用到的不同类型,一个个编译; 缺点 2: 编译出的二进制代码会比较大,存在 N 份。 缺点 3: 代码以二进制分发会损失泛型的信息。单态化之后,原本的泛型信息就被丢弃了。 #trait 定义了类型使用这个接口的行为; 在 trait 中,方法可以有缺省的实现; 允许用户把错误类型延迟到 trait 实现时才决定,这种带有关联类型的 trait 比普通 trait,更加灵活,抽象度更高 trait 的”继承“: trait B 在定义时可以使用 trait A 中的关联类型和方法 #Trait Object 表现为&dyn Trait 或者 Box:(动态分派(dynamic dispatch));

底层逻辑就是胖指针:数据本身+虚函数表 vtable;

如果 trait 所有的方法,返回值是 Self(trait object 产生时原来的类型会被抹去) 或者携带泛型参数(trait object 是运行时的产物),那么这个 trait 就不能产生 trait object。

rust会为实现了trait object类型的trait实现,生成相应的vtable,放在可执行文件中(一般在TEXT或RODATA段)。

#Traits

send/sync: 如果一个类型 T: Send,那么 T 在某个线程中的独占访问是线程安全的;如果一个类型 T: Sync,那么 T 在线程间的只读共享是安全的;

Clone 是深度拷贝,栈内存和堆内存一起拷贝;

Copy 是按位浅拷贝,与 Drop 互斥;

不支持 Send / Sync 的数据结构主要有:

裸指针 const T / mut T。它们是不安全的,所以既不是 Send 也不是 Sync。 UnsafeCell 不支持 Sync。也就是说,任何使用了 Cell 或者 RefCell 的数据结构不支持 Sync。 引用计数 Rc 不支持 Send 也不支持 Sync。所以 Rc 无法跨线程。 只需要实现From, Into会自动实现;

#延迟绑定 从数据的角度看,[数据结构]是[具体数据]的延迟绑定,[泛型结构]是[具体数据结构]的延迟绑定; 从代码的角度看,[函数]是一组实现某个功能的[表达式]的延迟绑定,[泛型函数]是[函数]的延迟绑定; [trait] 是[行为]的延迟绑定 #数据结构 指针是一个持有内存地址的值,可以通过解引用来访问它指向的内存地址,理论上可以解引用到任意数据类型; 引用是一个特殊的指针,它的解引用访问是受限的,只能解引用到它引用数据的类型,不能用作它用

#智能指针: 是一个胖指针; 智能指针String 对堆上的值具有所有权,而普通胖指针&str没有所有权; 在 Rust 中,凡是需要做资源回收的数据结构,且实现了 Deref/DerefMut/Drop,都是智能指针 #Box在堆上创建内存 #Cow<'a, B>提供写时克隆 #分发手段 使用泛型参数做静态分发 使用 trait object 做动态分发 这种根据 enum 的不同状态来进行统一分发的方法是第三种分发手段,其效率是动态分发的数十倍。 #MutexGuard用于数据加锁 通过 Drop trait 来确保,使用到的内存以外的资源在退出时进行释放 #切片 Slice &[T] 只读切片,只是一个借用

&mut[T] 可写的切片

Box<[T]> 堆上分配的切片: 而 Box<[T]> 一旦生成就固定下来,没有 capacity,也无法增长;对数据具有所有权。

Vec 可以通过 into_boxed_slice() 转换成 Box<[T]>,Box<[T]> 也可以通过 into_vec() 转换回 Vec;

当我们需要在堆上创建固定大小的集合数据,且不希望自动增长,那么,可以先创建 Vec,再转换成 Box<[T]> ;

Box<[T]>和&[T]的区别:

Box<[T]>指针指向的是堆内存数据;&[T]指针指向的数据可以是堆、栈内存数据; Box<[T]> 对数据具有所有权;&[T]只是一个借用;

#哈希表 哈希表最核心的特点就是:巨量的可能输入和有限的哈希表容量。 Rust 哈希表算法的设计核心: 二次探查(quadratic probing) SIMD(单指令多数据) 查表(Single Instruction Multiple Data lookup) 解决哈希冲突机制 链地址法(chaining) 开放寻址法(open addressing) 通过 shrink_to_fit / shrink_to 释放掉不需要的内存 哈希冲突解决机制

SIMD 查表

#错误处理的主流方法 返回值 异常处理 类型系统 在 Rust 代码中,如果你只想传播错误,不想就地处理,可以用 ? 操作符 使用 Option 和 Result 是 Rust 中处理错误的首选 立刻暴露 Panic!, catch_unwind! #闭包

闭包是一种匿名类型,一旦声明,就会产生一个新的类型(调用闭包时可以直接和代码对应),但这个类型无法被其它地方使用。这个类型就像一个结构体,会包含所有捕获的变量。 不带 move 时,闭包捕获的是对应自由变量的引用; 带 move 时,对应自由变量的所有权会被移动到闭包结构中 闭包的大小跟参数、局部变量都无关,只跟捕获的变量有关,闭包捕获的变量都存储在栈上。 闭包是存储在栈上(没有堆内存分配),并且除了捕获的数据外,闭包本身不包含任何额外函数指针指向闭包的代码。 闭包的调用效率和函数调用几乎一致 #进阶篇 #类型系统 #泛型 架构师的工作不是作出决策,而是尽可能久地推迟决策,在现在不作出重大决策的情况下构建程序,以便以后有足够信息时再作出决策。 通过使用泛型参数,BufReader 把决策交给使用者。 泛型参数三种常见的使用场景: 使用泛型参数延迟数据结构的绑定; 使用泛型参数和 PhantomData,声明数据结构中不直接使用但在实现过程中需要用到的类型; 使用泛型参数让同一个数据结构对同一个 trait 可以拥有不同的实现。 PhantomData: 被广泛用在处理,数据结构定义过程中不需要,但是在实现过程中需要的泛型参数; 在定义数据结构时,对于额外的、暂时不需要的泛型参数,用 PhantomData 来“拥有”它们,这样可以规避编译器的报错。 实际长度为零,是个 ZST(Zero-Sized Type), 类型标记。 #Trait Object 使用 Trait Object 是有额外的代价的,首先这里有一次额外的堆分配,其次动态分派会带来一定的性能损失 当在某个上下文中需要满足某个 trait 的类型,且这样的类型可能有很多,当前上下文无法确定会得到哪一个类型时,我们可以用 trait object 来统一处理行为。 和泛型参数一样,trait object 也是一种延迟绑定,它让决策可以延迟到运行时,从而得到最大的灵活性。 后果是执行效率的打折。在 Rust 里,函数或者方法的执行就是一次跳转指令,而 trait object 方法的执行还多一步,它涉及额外的内存访问,才能得到要跳转的位置再进行跳转,执行的效率要低一些。 返回/线程间传递 trait object 都免不了使用 Box 或者 Arc,会带来额外的堆分配的开销。 #围绕trait来设计和架构系统 软件开发的整个行为,基本上可以说是不断创建和迭代接口,然后在这些接口上进行实现的过程。 用trait做桥接 SOLID原则 SRP:单一职责原则,是指每个模块应该只负责单一的功能,不应该让多个功能耦合在一起,而是应该将其组合在一起。 OCP:开闭原则,是指软件系统应该对修改关闭,而对扩展开放。 LSP:里氏替换原则,是指如果组件可替换,那么这些可替换的组件应该遵守相同的约束,或者说接口。 ISP:接口隔离原则,是指使用者只需要知道他们感兴趣的方法,而不该被迫了解和使用对他们来说无用的方法或者功能。 DIP:依赖反转原则,是指某些场合下底层代码应该依赖高层代码,而非高层代码去依赖底层代码。 #网络开发 应表会传网链 物 ISO/OSI七层模型及对应协议

#Unsafe Rust unsafe rust 场景

可以使用、也推荐使用 unsafe 的场景

实现 unsafe trait: 主要是Send / Sync 这两个 trait; 任何 trait,只要声明成 unsafe,它就是一个 unsafe trait; unsafe trait 是对 trait 的实现者的约束 unsafe fn 是函数对调用者的约束,需要加 unsafe block 调用已有的 unsafe 函数: 需要加 unsafe block; 定义 unsafe 函数,在其中调用 unsafe 函数; 对裸指针做解引用 使用 FFI 不推荐的使用 unsafe 的场景

访问或者修改可变静态变量 任何需要 static mut 的地方,都可以用 AtomicXXX / Mutex / RwLock 来取代。 在宏里使用 unsafe 使用 unsafe 提升性能 而有些时候,即便你能够使用 unsafe 让局部性能达到最优,但作为一个整体看的时候,这个局部的优化可能根本没有意义。 撰写 unsafe 代码

一定要用注释声明代码的安全性 #FFI(Foreign Function Interface) 一门语言,如果能跟 C ABI(Application Binary Interface)处理好关系,那么就几乎可以和任何语言互通。

处理 FFI 的注意事项

如何处理数据结构的差异? 谁来释放内存? 如何进行错误处理? rust 调用其他语言

Rust shim 主要做四件事情:

提供 Rust 方法、trait 方法等公开接口的独立函数。注意 C 是不支持泛型的,所以对于泛型函数,需要提供具体的用于某个类型的 shim 函数。 所有要暴露给 C 的独立函数,都要声明成 #[no_mangle],不做函数名称的改写。 数据结构需要处理成和 C 兼容的结构。 要使用 catch_unwind 把所有可能产生 panic! 的代码包裹起来。 FFI 的其它方式

通过网络:REST API、gRPC protobuf 来序列化 / 反序列化要传递的数据 #并发篇 并发concurrent:轮流处理,多队列一件事;并行parallel:同时执行,多队列多件事;

并发vs并行

并发和并行都是对“多任务”处理的描述,其中并发是轮流处理,而并行是同时处理。

在处理并发的过程中,难点并不在于如何创建多个线程来分配工作,在于如何在这些并发的任务中进行同步。

我们来看并发状态下几种常见的工作模式:

自由竞争模式、 map/reduce 模式、 DAG 模式: #Atomic Atomic 是一切并发同步的基础

#Mutex 用来解决这种读写互斥问题的基本工具

#RwLock #Semaphore #Condvar 典型场景是生产者 - 消费者模式

在实践中,Condvar 往往和 Mutex 一起使用:Mutex 用于保证条件在读写时互斥,Condvar 用于控制线程的等待和唤醒。

#Channel Channel 把锁封装在了队列写入和读取的小块区域内,然后把读者和写者完全分离

channels

channels2

#Actor actor 是一种有栈协程。每个 actor,有自己的一个独立的、轻量级的调用栈,以及一个用来接受消息的消息队列(mailbox 或者 message queue),外界跟 actor 打交道的唯一手段就是,给它发送消息。

Atomic 在处理简单的原生类型时非常有用,如果你可以通过 AtomicXXX 结构进行同步,那么它们是最好的选择。 当你的数据结构无法简单通过 AtomicXXX 进行同步,但你又的确需要在多个线程中共享数据,那么 Mutex / RwLock 可以是一种选择。不过,你需要考虑锁的粒度,粒度太大的 Mutex / RwLock 效率很低。 如果你有 N 份资源可以供多个并发任务竞争使用,那么,Semaphore 是一个很好的选择。比如你要做一个 DB 连接池。 当你需要在并发任务中通知、协作时,Condvar 提供了最基本的通知机制,而 Channel 把这个通知机制进一步广泛扩展开,于是你可以用 Condvar 进行点对点的同步,用 Channel 做一对多、多对一、多对多的同步。 如果说在做整个后端的系统架构时,我们着眼的是:有哪些服务、服务和服务之间如何通讯、数据如何流动、服务和服务间如何同步;那么在做某一个服务的架构时,着眼的是有哪些功能性的线程(异步任务)、它们之间的接口是什么样子、数据如何流动、如何同步。

#Future #Reactor Pattern(反应器模式) Reactor Pattern 包含三部分:

tasks:待处理任务 Executor: 调度执行tasks Reactor: 维护事件队列 reactor pattern

使用 Future 的注意事项

我们要避免在异步任务中处理大量计算密集型的工作; 在使用 Mutex 等同步原语时,要注意标准库的 MutexGuard 无法跨越 .await,所以,此时要使用对异步友好的 Mutex,如 tokio::sync::Mutex; 如果要在线程和异步任务间同步,可以使用 channel。 #状态机 #Pin Pin 是为了让某个数据结构无法合法地移动,而 Unpin 则相当于声明数据结构是可以移动的,它的作用类似于 Send / Sync,通过类型约束来告诉编译器哪些行为是合法的、哪些不是。

#自引用数据结构 #Generator rust中的生成器被实现为状态机。计算链的内存占用是由单个步骤所需的最大占用定义的 #async/await #Stream trait #实战篇 #生产环境

#数据处理 #软件架构

渐进式的架构设计,从 MVP 的需求中寻找架构的核心要素,构建一个原始但完整的结构(primitive whole),然后围绕着核心要素演进

分层结构、流水线结构和插件结构

#高级篇 #宏 syn/quote

cisen commented 2 years ago

rust中move、copy、clone、drop和闭包捕获

cisen commented 2 years ago

捋捋 Rust 中的 impl Trait 和 dyn Trait https://zhuanlan.zhihu.com/p/109990547 https://rustwiki.org/zh-CN/edition-guide/rust-2018/trait-system/impl-trait-for-returning-complex-types-with-ease.html

缘起 一切都要从年末换工作碰上特殊时期, 在家闲着无聊又读了几首诗, 突然想写一个可以浏览和背诵诗词的 TUI 程序说起. 我选择了 Cursive 这个 Rust TUI 库. 在实现时有这么一个函数, 它会根据参数的不同返回某个组件(如 Button, TextView 等). 在 Cursive 中, 每个组件都实现了 View 这个 trait, 最初这个函数只会返回某个确定的组件, 所以函数签名可以这样写

fn some_fn(param: SomeType) -> Button

随着开发进度增加, 这个函数需要返回 Button, TextView 等组件中的一个, 我下意识地写出了类似于下面的代码

fn some_fn(param1: i32, param2: i32) -> impl View {
    if param1 > param2 {
        // do something...
        return Button {};
    } else {
        // do something...
        return TextView {};
    }
}

可惜 Rust 编译器一如既往地打脸, Rust 编译器报错如下

--> src\main.rs:19:16
   |
13 | fn some_fn(param1: i32, param2: i32) -> impl View {
   |                                         --------- expected because this return type...
...
16 |         return Button {};
   |                --------- ...is found to be `Button` here
...
19 |         return TextView {};
   |                ^^^^^^^^^^^ expected struct `Button`, found struct `TextView`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

从编译器报错信息看函数返回值虽然是 impl View 但其从 if 分支推断返回值类型为 Button 就不再接受 else 分支返回的 TextView. 这与 Rust 要求 if else 两个分支的返回值类型相同的特性一致. 那能不能让函数返回多种类型呢? Rust 之所以要求函数不能返回多种类型是因为 Rust 在需要在 编译期确定返回值占用的内存大小, 显然不同类型的返回值其内存大小不一定相同. 既然如此, 把返回值装箱, 返回一个胖指针, 这样我们的返回值大小可以确定了, 这样也许就可以了吧. 尝试把函数修改成如下形式:

fn some_fn(param1: i32, param2: i32) -> Box<View> {
    if param1 > param2 {
        // do something...
        return Box::new(Button {});
    } else {
        // do something...
        return Box::new(TextView {});
    }
}

现在代码通过编译了, 但如果使用 Rust 2018, 你会发现编译器会抛出警告:

warning: trait objects without an explicit `dyn` are deprecated
  --> src\main.rs:13:45
   |
13 | fn some_fn(param1: i32, param2: i32) -> Box<View> {
   |                                             ^^^^ help: use `dyn`: `dyn View`
   |
   = note: `#[warn(bare_trait_objects)]` on by default

编译器告诉我们使用 trait object 时不使用 dyn 的形式已经被废弃了, 并且还贴心的提示我们把 Box 改成 Box, 按编译器的提示修改代码, 此时代码 no warning, no error, 完美.

但 impl Trait 和 Box 除了允许多种返回值类型的之外还有什么区别吗? trait object 又是什么? 为什么 Box 形式的返回值会被废弃而引入了新的 dyn 关键字呢?

埋坑 impl Trait 和 dyn Trait 在 Rust 分别被称为静态分发和动态分发. 在第一版的 Rust Book 这样解释分发(dispatch)

When code involves polymorphism, there needs to be a mechanism to determine which specific version is actually run. This is called ‘dispatch’. There are two major forms of dispatch: static dispatch and dynamic dispatch. While Rust favors static dispatch, it also supports dynamic dispatch through a mechanism called ‘trait objects’. 即当代码涉及多态时, 需要某种机制决定实际调用类型. Rust 的 Trait 可以看作某些具有通过特性类型的集合, 以上面代码为例, 在写代码时我们不关心具体类型, 但在编译或运行时必须确定 Button 还是 TextView. 静态分发, 正如静态类型语言的"静态"一词说明的, 在编译期就确定了具体调用类型. Rust 编译器会通过单态化(Monomorphization) 将泛型函数展开.

假设 Foo 和 Bar 都实现了 Noop 特性, Rust 会把函数

fn x(...) -> impl Noop

展开为

fn x_for_foo(...) -> Foo
fn x_for_bar(...) -> Bar

(仅作原理说明, 不保证编译会这样展开函数名).

通过单态化, 编译器消除了泛型, 而且没有性能损耗, 这也是 Rust 提倡的形式, 缺点是过多展开可能会导致编译生成的二级制文件体积过大, 这时候可能需要重构代码.

静态分发虽然有很高的性能, 但在文章开头其另一个缺点也有所体现, 那就是无法让函数返回多种类型, 因此 Rust 也支持通过 trait object 实现动态分发. 既然 Trait 是具有某种特性的类型的集合, 那我们可以把 Trait 也看作某种类型, 但它是"抽象的", 就像 OOP 中的抽象类或基类, 不能直接实例化.

Rust 的 trait object 使用了与 c++ 类似的 vtable 实现, trait object 含有1个指向实际类型的 data 指针, 和一个指向实际类型实现 trait 函数的 vtable, 以此实现动态分发. 更加详细的介绍可以在

Exploring Dynamic Dispatch in Rust ​alschwalm.com/blog/static/2017/03/07/exploring-dynamic-dispatch-in-rust/ 看到. 既然 trait object 在实现时可以确定大小, 那为什么不用 fn x() -> Trait 的形式呢? 虽然 trait object 在实现上可以确定大小, 但在逻辑上, 因为 Trait 代表类型的集合, 其大小无法确定. 允许 fn x() -> Trait 会导致语义上的不和谐. 那 fn x() -> &Trait 呢? 当然可以! 但鉴于这种场景下都是在函数中创建然后返回该值的引用, 显然需要加上生命周期:

fn some_fn(param1: i32, param2: i32) -> &'static View {
    if param1 > param2 {
        // do something...
        return &Button {};
    } else {
        // do something...
        return &TextView {};
    }
}

我不喜欢添加额外的生命周期说明, 想必各位也一样. 所以我们可以用拥有所有权的 Box 智能指针避免烦人的生命周期说明. 至此 Box 终于出现了. 那么问题来了, 为什么编译器会提示 Box 会被废弃, 特地引入了 dyn 关键字呢? 答案可以在 RFC-2113 中找到.

RFC-2113 明确说明了引入 dyn 的原因, 即语义模糊, 令人困惑, 原因在于没有 dyn 让 Trait 和 trait objects 看起来完全一样, RFC 列举了3个例子说明.

第一个例子, 加入你看到下面的代码, 你知道作者要干什么吗?

impl SomeTrait for AnotherTrait impl<T> SomeTrait for T where T: Another

你看懂了吗? 说实话我也看不懂 : ) PASS

第二个例子, impl MyTrait {} 是正确的语法, 不过这样会让人以为这会在 Trait 上添加默认实现, 扩展方法或其他 Trait 自身的一些操作. 实际上这是在 trait object 上添加方法.

如在下面代码说明的, Trait 默认实现的正确定义方法是在定义 Trait 时指定, 而不应该在 impl Trait {} 语句块中.

trait Foo {
    fn default_impl(&self) {
        println!("correct impl!");
    }
}

impl Foo {
    fn trait_object() {
        println!("trait object impl");
    }
}

struct Bar {}

impl Foo for Bar {}

fn main() {
    let b = Bar{};
    b.default_impl();
    // b.trait_object();
    Foo::trait_object();
}

Bar 在实现了 Foo 后可以通过 b.default_impl 调用, 无需额外实现, 但 b.trait_object 则不行, 因为 trait_object 方法是 Foo 的 trait object 上的方法.

如果是 Rust 2018 编译器应该还会显示一条警告, 告诉我们应该使用 impl dyn Foo {}

第三个例子则以函数类型和函数 trait 作对比, 两者差别只在于首字母是否大写(Fn代表函数trait object, fn则是函数类型), 难免会把两者弄混.

更加详细的说明可以移步

RFC-2113 ​github.com/rust-lang/rfcs/blob/master/text/2113-dyn-trait-syntax.md .

总结 impl trait 和 dyn trait 区别在于静态分发于动态分发, 静态分发性能 好, 但大量使用有可能造成二进制文件膨胀; 动态分发以 trait object 的概念通过虚表实现, 会带来一些运行时开销. 又因 trait object 与 Trait 在不引入 dyn 的情况下经常导致语义混淆, 所以 Rust 特地引入 dyn 关键字, 在 Rust 2018 中已经稳定.

引用 以下是本文参考的资料

impl Trait for returning complex types with ease ​doc.rust-lang.org/nightly/edition-guide/rust-2018/trait-system/impl-trait-for-returning-complex-types-with-ease.html#argument-position impl trait 社区跟踪 ​github.com/rust-lang/rust/issues/34511 rust-lang/rfcs ​github.com/rust-lang/rfcs/blob/master/text/2113-dyn-trait-syntax.md

Traits and Trait Objects in Rust ​joshleeb.com/posts/rust-traits-and-trait-objects/ Dynamic vs. Static Dispatch ​lukasatkinson.de/2016/dynamic-vs-static-dispatch/ Exploring Dynamic Dispatch in Rust ​alschwalm.com/blog/static/2017/03/07/exploring-dynamic-dispatch-in-rust/

cisen commented 8 months ago

帮我分析一下这句命令:cargo run -- demo.rs --crate-type=lib,特别是后面的--crate-type=lib

这个命令是用于在 Rust 编程语言环境中运行一个程序。让我们逐部分分析这个命令:

  1. cargo run:这是 Cargo 的一个命令,用于编译并运行 Rust 项目。Cargo 是 Rust 的包管理工具,它处理项目的编译、依赖管理等任务。

  2. --:这个符号在命令行中通常用来明确区分命令的参数和子命令的参数。在这个上下文中,-- 之后的部分不会被 Cargo 直接解释,而是会传递给由 Cargo 运行的程序。

  3. demo.rs:这似乎是要运行的 Rust 源文件。通常,使用 cargo run 时不需要指定源文件,因为 Cargo 会根据项目的配置文件 (Cargo.toml) 来确定要运行的主程序。但在某些情况下,可以直接提供一个文件名。

  4. --crate-type=lib:这是传递给 demo.rs 程序的一个参数。在 Rust 中,“crate” 是一个模块或库的概念,可以是可执行文件(binary)或库(lib)。这个参数表明 demo.rs 应该被当作一个库来处理。这在某些特定的使用场景中可能有意义,但通常在使用 cargo run 时,目标是运行一个可执行文件而非库。

总结:这条命令看起来是试图用 Cargo 运行一个 Rust 文件,同时指示这个文件应当以库的方式进行编译。这可能不是典型的用法,因为通常 cargo run 用于运行可执行项目。如果 demo.rs 确实是一个可执行文件的主要源文件,这个命令可能不会按预期工作。相反,如果意图是编译一个库,通常会使用 cargo build 而不是 cargo run

cisen commented 8 months ago

在Rust中,proc_macro(过程宏)是一种强大的特性,允许你在编译时执行代码来操作或生成Rust代码。下面是一个简单的例子,展示了如何创建和使用一个过程宏。

首先,你需要创建一个库项目,因为过程宏必须定义在它们自己的专用库中。这里我们将创建一个名为my_macros的库,并在其中定义一个过程宏。

  1. 创建一个新的库项目:

    cargo new my_macros --lib
    cd my_macros
  2. Cargo.toml中添加proc-macro属性和必要的依赖项:

    [lib]
    proc-macro = true
    
    [dependencies]
    syn = "1.0"
    quote = "1.0"
  3. src/lib.rs中定义一个简单的过程宏:

    extern crate proc_macro;
    
    use proc_macro::TokenStream;
    use quote::quote;
    use syn;
    
    #[proc_macro]
    pub fn hello_macro(input: TokenStream) -> TokenStream {
       // 解析输入TokenStream到一个Rust AST结构
       let input = syn::parse_macro_input!(input as syn::Ident);
    
       // 生成Rust代码
       let expanded = quote! {
           impl HelloMacro for #input {
               fn hello_macro() {
                   println!("Hello, Macro! My name is {}", stringify!(#input));
               }
           }
       };
    
       // 将生成的代码转换回TokenStream
       TokenStream::from(expanded)
    }

    这个宏会为指定的类型实现一个HelloMacro trait,该trait有一个hello_macro方法,打印出类型的名称。

  4. 现在,创建一个新的二进制项目来使用这个宏:

    cargo new use_macro
    cd use_macro
  5. use_macro项目的Cargo.toml中添加对my_macros库的依赖:

    [dependencies]
    my_macros = { path = "../my_macros" }
  6. use_macro项目的src/main.rs中使用这个宏:

    use my_macros::hello_macro;
    
    trait HelloMacro {
       fn hello_macro();
    }
    
    #[hello_macro]
    struct Pancakes;
    
    fn main() {
       Pancakes::hello_macro();
    }
  7. 运行use_macro项目:

    cargo run

    这将打印出:"Hello, Macro! My name is Pancakes"。

这个例子展示了过程宏的基本用法:定义一个宏,它在编译时读取并操作Rust代码,然后在另一个项目中使用这个宏。过程宏是Rust中非常强大的工具,可以用于各种复杂的代码生成场景。