31_rust_高级特性

高级特性

  • 不安全rust
  • 高级Trait
  • 高级类型
  • 高级函数和闭包

不安全rust

隐藏这第二个语言,其未强制内存安全保证:Unsafe rust(不安全的rust);其和普通rust一样,但提供了额外的“超能力”。
unsafe rust存在的原因:

  • 静态分析是保守的,使用unsafe rust,表示开发者知道自己在做什么,并承担相应风险。
  • 计算机硬件本身就不安全,rust需要能够进行底层系统编程。

unsafe的超能力:
使用unsafe关键字来切换到unsafe rust,开启一个块,块内为unsafe代码。
unsafe rust里可执行四个动作(超能力):

  • 解引用原始指针
  • 调用unsafe函数或方法
  • 访问或修改可变的静态变量
  • 实现unsafe trait

注意:

  • unsafe并未关闭借用检查或停用其他安全检查,如果在里边使用引用,依然会被检查。所以即便在unsafe代码块中依然可获得一定的安全性。
  • 任何内存安全相关的错误必须保留在unsafe块里。
  • 尽可能隔离unsafe代码,最好将其封装在安全的抽象里,提供安全的API。
    很多标准库里也含有unsafe代码,但使用安全接口封装,防止不安全代码泄露到调用处。

解引用原始指针

原始指针分类:

  • 可变的:*mut T
  • 不可变的:*constT。意味着指针在解引用后不能直接对其进行赋值

注意:这里的*不是解引用符号,它是类型名的一部分。

与引用的区别点,原始指针:

  • 允许通过同时具有不可变和可变指针,或多个指向同一位置的可变指针,并忽略借用规则
  • 无法保证能指向合理的内存
  • 允许为 null
  • 不实现任何自动清理
  • 放弃保证的安全,换取更好的性能/与其它语言或硬件接口的能力
fn main() {
  let mut n = 5;
  let r1 = &n as *const i32; // 不可变原始指针
  let r2 = &mut n as *mut i32; // 可变原始指针
  // 可在非安全代码块外创建原始指针,但只能在不安全代码块内进行解引用
  // 在非安全代码块内解引用这两个必定有效的指针
  unsafe {
    println!("r1 {}", *r1);
    println!("r2 {}", *r2);
  }
  // 创建一个无法确定有效性的原始指针
  let addr = 0x0123456usize; // 一个内存地址,可能有数据也可能无
  let r = addr as *const i32; // 创建一个该地址的原始指针,编译不会报错
  unsafe {
    println!("r {}", *r); // 解引用不确定指针,编译不报错,需开发者自己保证安全性
  }
}
/* 运行输出:
r1 5
r2 5
thread 'main' panicked at 'misaligned pointer dereference: address must be a multiple of 0x4 but is 0x123456', src\main.rs:15:7
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread caused non-unwinding panic. aborting.
*/

用原始指针的作用:与c语言进行接口,构建借用检查器无法理解的安全抽象。

调用unsafe函数或方法

unsafe函数或方法,在定义前加上unsafe关键字。

  • 调用前需开发者确认满足要求或条件(通常由提供方提供文档说明),因为rust无法对这些条件进行验证。
  • 需要在unsafe块里进行调用。
unsafe fn danger() {}
fn main() {
    unsafe {
        danger(); //正确调用
    }
    danger(); // 编译报错 call to unsafe function is unsafe and requires unsafe function or block
}

创建unsafe代码的安全抽象

函数包含unsafe代码并不意味着需要将整个函数标记为unsafe,将unsafe代码包裹在安全函数中是一个常见的抽象。

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    let r = &mut v[..]; // 建立一个完整切片
    // split_at_mut函数是标准库的函数,的作用是根据传入的数字切分vector,
    // 分割成两个切片,分别是1 2 3, 4 5两组
    let (a, b) = r.split_at_mut(3);
    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5]);
}

split_at_mut是库函数,使用了不安全函数,为了理解,这里进行手动定义如下:

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    assert!(mid <= len);
    (&mut slice[..mid], &mut slice[mid..]) // 试图返回两个切片,以mid作为切分点
    // 但这里会报无法对切片进行两次借用的错误,因为这是rust语言的规则
    // 但实际情况是可以的切分借用的,因为这里借用的是前后两个无交叉部分,切分后无关联,但rust规则限定,无法识别这种情况
}

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    let r = &mut v[..]; // 建立一个完整切片
    // split_at_mut函数是标准库的函数,的作用是根据传入的数字切分vector,
    // 分割成两个切片,分别是1 2 3, 4 5两组
    let (a, b) = r.split_at_mut(3);
    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5]);
}

编译报错:cannot borrow *slice as mutable more than once at a time,所以这里需要使用unsafe代码块来实现,进行安全抽象,代码如下:

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr(); //返回原始指针,类型是*mut i32,指向slice
    assert!(mid <= len);
    unsafe {
        (slice::from_raw_parts_mut(ptr, mid), // 创建切片
        slice::from_raw_parts_mut(ptr.add(mid), len - mid))
    } //由于使用了原始指针和偏移量,代码块不安全,调用需要在unsafe块里完成
    // 虽然split_at_mut调用了不安全代码,但自身未标记为不安全函数,它就是不安全代码的安全抽象
}

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    let r = &mut v[..]; // 建立一个完整切片
    // split_at_mut函数是标准库的函数,的作用是根据传入的数字切分vector,
    // 分割成两个切片,分别是1 2 3, 4 5两组
    let (a, b) = r.split_at_mut(3);
    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5]);
}

如果在main中不是调用安全抽象函数,而是直接调用非安全函数:

fn main() {
    let addr = 0x0123456usize;
    let r = addr as *const i32;
    let slice: &[i32] = unsafe {
        slice::from_raw_parts_mut(r, 50);
    };
    // 如果这种方式调用,编译不会报错,但运行时可能崩溃,因为无法保障切片是有效的
}

使用extern函数调用外部代码

extern关键字:简化创建和使用外部函数接口(FFI)的过程
外部函数接口(FFI,Foreign Function Interface):允许一种编程语言定义函数,并让其它编程语言能调用这些函数。

extern "C" {
  fn abs(in: i32) -> i32;
}
fn main() {
  unsafe {
    println!("call c abs:{}", abs(-3));
  }
}

应用二进制接口(ABI,Application Binary Interface):定义函数在汇编层的调用方式。
“C” ABI是最常见的ABI,它遵循C语言的ABI。

从其它语言调用rust函数

可使用extern创建接口,其它语言通过它们可调用rust的函数。

  • 在fn前添加extern关键字,并指定ABI
  • 还需添加#[no_mangle]注解(这是一个编译阶段),避免rust在编译时改变它的名称
#[no_mangle]
pub extern "C" fn call_func() {
  println!("func in rust");
}

且无需使用unsafe。

访问或修改一个可变静态变量

rust支持全局变量,但因所有权机制可能产生某些问题,比如数据竞争。
在rust里,全局变量叫静态(static)变量。声明时必须指明类型。其生命周期能被编译器推断出,只存储'static的引用。

static HELLO: &str = "hello";
fn main() {
  println!("{}", HELLO);
}

静态变量

  • 静态变量与常量类似
  • 命名:SCREAMIN_SNAKE_CASE,全大写+下划线分割
  • 必须标注类型
  • 静态变量只能存储'static生命周期的引用,无需显示标注
  • 访问不可变静态变量是安全的

常量和不可变静态变量的区别

  • 静态变量有固定的内存地址,使用它的值总会访问同样的数据
  • 常量允许使用它们的时候对数据进行复制
  • 静态变量可以是可变的,访问和修改静态可变变量是不安全的(unsafe)
static mut CNT: u32 = 0;

fn add(inc: i32) {
    unsafe {//访问和修改静态是unsafe的,所以要放在unsafe块中
        CNT += inc;
    }
}

fn main() {
    add(5);
    unsafe {//如果多线程的情况可能出现数据竞争
        println!("{}", CNT);
    }
    // 多线程的情况下,很难保障数据竞争不发生,所以是不安全的
}

实现不安全(unsafe)trait

当某个trait中存在至少一个方法拥有编译器无法校验的不安全因素时,就称之为trait是不安全的。
声明unsafe trait:在定义前加unsafe关键字,该trait只能在unsafe代码块中实现。

unsafe trait Ts {
  // methods go here
}
unsafe impl Ts for i32 {
  // method implementations go here
}

何时使用 unsafe 代码

  • 编译器无法保证内存安全,保证 unsafe 代码正确并不简单
  • 有充足理由使用 unsafe 代码时,就可以这样做
  • 通过显式标记 unsafe,可以在出现问题时轻松的定位

高级trait

在 Trait 定义中使用关联类型来指定占位类型
关联类型(associated type)是 Trait 中的类型占位符,它可以用于 Trait 的方法签名中: 可以定义出包含某些类型的 Trait,而在实现前无需知道这些类型是什么。

pub trait Iter {
    type Item; // Item是关联类型,类型占位符
    // 在迭代的过程中,用item来替代具体的值,与泛型有点类似
    fn next(&mut self) -> Option<Self::Item>;
}
fn main() {
}

关联类型与泛型的区别

  • 泛型
    每次实现Trait 时标注类型;可以为一个类型多次实现某个 Trait(不同的泛型参数)。
  • 关联类型
    无需标注类型, 无法为单个类型多次实现某个Trait。

默认泛型参数和运算符重载

  • 可以在使用泛型参数时为泛型指定一个默认的具体类型
  • 语法:<PlaceholderType=ConcreteType>
  • 这种技术常用于运算符重载(operator overloading)
  • Rust不允许创建自己的运算符及重载任意的运算符
  • 但可以通过实现 std::ops 中列出的那些 trait 来重载一部分相应的运算符
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Pt {
    x: i32,
    y: i32,
}
impl Add for Pt {
    type Output = Pt;
    fn add(self, other: Pt) -> Pt {
        Pt { x: self.x + other.x,
             y: self.y + other.y,
           }
    }
}
fn main() {
    assert_eq!(Pt {x: 1, y: 1} + Pt {x: 2, y: 3}, Pt{x: 3, y: 4});
}

默认泛型参数被指定类型的例子

use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Mm(u32);
#[derive(Debug, PartialEq)]
struct M(u32);

impl Add<M> for Mm {
    type Output = Mm;
    fn add(self, other: M) -> Mm {
        Mm (self.0 + (other.0 * 1000))
    }
}
fn main() {
    assert_eq!(Mm(1) + M(1), Mm(1001));
}

默认泛型参数的主要应用场景:

  • 扩展一个类型而不破坏现有代码
  • 允许在大部分用户都不需要的特定场景下进行自定义

完全限定语法(Fully Qualified Syntax)

如何调用同名方法

trait P1 {
    fn func(&self);
}
trait P2 {
    fn func(&self);
}
struct H;
impl P1 for H {
    fn func(&self) {
        println!("p1 func");
    }
}
impl P2 for H {
    fn func(&self) {
        println!("p2 func");
    }
}
impl H {
    fn func(&self) {
        println!("H's func");
    }
}
fn main() {
    let h = H;
    h.func();// 默认是自身的func
    P1::func(&h);//通过传入的h区分是哪个实现,类似于多态
    P2::func(&h);
}
/*
H's func
p1 func
p2 func
*/

而对于没有参数的场景,则需要采用完全限定语法。
完全限定语法(Fully Qualified Syntax)
完全限定语法格式:<Type as Trait>::function(receiver_if_method, netx_arg, ...);

  • 可在任何调用函数或方法的地方使用
  • 允许忽略从其它上下文能推导出来的部分
  • 当rust无法区分期望调用具体哪个函数实现时才使用这种方式
trait A {
    fn func() -> String;
}
struct D;
impl D {
    fn func() -> String {
        String::from("from D")
    }
}
impl A for D {
    fn func() -> String {
        String::from("from A for D")
    }
}
fn main() {
    println!("{}", D::func());
    println!("{}", <D as A>::func());
}
/*
from D
from A for D
*/

使用supertrait要求trait附带其它trait功能

使用supertrait要求trait附带其它trait的功能,就相当于一个trait继承于另外一个trait。
需要一个trait中使用其它trait的功能,需要被依赖的trait也被实现,被间接依赖的trait就是当前trait的supertrait。

use std::fmt;

trait O: fmt::Display {//要求实现display trait
    fn o_print(&self) {
        let o = self.to_string(); //要求实现to_string方法
        let len = o.len();
        println!("{}, {}", len, o);
    }
}
struct P {
    x: i32,
    y: i32,
}
impl O for P {} // 因为有默认实现,则可不用再实现o_print
// 但依然会报错,因为O trait要求实现display trait
impl fmt::Display for P {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{} {}]", self.x, self.y)
    }
}
fn main() {
    let p1 = P {x: 3, y: 6,};
    p1.o_print();
}
/*输出
5, [3 6]
*/

使用 newtype 模式在外部类型上实现外部 trait

孤儿规则:只有当 trait 或类型定义在本地包时,才能为该类型实现这个trait

可以通过 newfype 模式来绕过这一规则,利用 tuple struct(元组结构体)创建一个新的类型的。

use std::fmt;
// 如果想对vec实现display trait,但这两个都实现在外部包中,无法像自定义struct那样实现
// 就可以将vec放在一个struct里,对该struct进行实现
struct Tv(Vec<String>); // 创建一个本地tuple struct,包含Vec

impl fmt::Display for Tv {// 为本地struct实现display trait
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", ")) //由于是tuple struct,通过self.0获取vec
    }
}
fn main() {
    let w = Tv(vec![String::from("a"), String::from("b"), String::from("c"),]);
    println!("{}", w);
}
/*
[a, b, c]
*/

高级类型

使用newtype模式实现类型安全和抽象

newtype 模式可以:

  • 用来静态的保证各种值之间不会混淆并表明值的单位
  • 为类型的某些细节提供抽象能力
  • 通过轻量级的封装来隐藏内部实现细节

使用类型别名创建类型同义词

Rust提供了类型别名的功能:

  • 为现有类型生产另外的名称(同义词)
  • 但并不是一个独立的类型
  • 使用 type 关键字实现该功能
  • 主要用途:减少代码字符重复
type MyI32T = i32; //为i32定义别名
fn main() {
    let x: i32 = 5;
    let y: MyI32T = 6;
    println!("{}", x + y);
}

针对类型很长的场景:

type BTy = Box<dyn Fn() + Send + 'static>;
fn func1(f: BTy) {}
fn func2() -> BTy {
    Box::new(|| println!("hi"))
}
fn main() {
    let f: BTy = Box::new(|| println!("f"));
}

在标准库里的场景

use std::io::Error;
use std::fmt;
// pub trait Write {
//     fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
//     fn flish(&mut self) -> Result<usize, Error>;
//     fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
// }
// 在std::io下的大部分函数都会返回Result<usize, Error>类型,这个Result<T, E>中T不同,E都是Error
// 所以在std::io下定义了一个如下别名,下边表达式声明在std::io里
// type Result<T> = Result<T, std::io::Error> // 实际不能再重复声明
// 这样定义后,就只剩T对外呈现了,E写死为std::io::Error了
// 上面trait就可改成如下格式
type Result<T> = std::io::Result<T>;
pub trait Write2 {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flish(&mut self) -> Result<usize>;
    fn write_all(&mut self, buf: &[u8]) -> Result<()>;
}
fn main() {}

Never类型

有一个名为!的特殊类型:

  • 它没有任何值,行话称为空类型(empty type)
  • 倾向于叫它 never 类型,因为它在不返回的函数中充当返回类型
  • 不返回值的函数也被称作发散函数(diverging function)
 fn func() -> ! {
// 这里什么都不返回和返回!类型是不同的,这段代码编译报错:
//  expected `!`, found `()`
// 如果返回值为! 则表示该函数永远不会返回值,但实际无法创建出来这样的never值
 }
 fn main() {}

continue表达式

fn main() {
    let g = "";
    loop {
        let g: u32 = match g.trim().parse() {
            Ok(n) => n,
            Err(_) => continue, // 这里返回值是continue,continue类型就是never类型,这里将被强制转成u32类型
            // never类型会被强制转成其它任意类型
        }
    }
}

panic!的返回值也是nerver类型

impl<T> Option<T> {
    pub fn unwrap(self) -> T {
        match self {
            Some(v) => v, // panic!返回值!,因为其会中断程序,不会返回值
            None => panic!("called Option::unwrap() on a None value"),
            // 所以在None的时候,不会为unwrap返回一个值,所以定义是合理的
        }
    }
}

loop循环

fn main() {
    loop {// loop表达式永远不会结束,所以返回类型是nerver类型
        print!("forever");
    }
}

动态大小和Sized Trait

Rust需要在编译时确定为一个特定类型的值分配多少空间。

动态大小的类型(Dynamically Sized Types,DST)的概念:

  • 编写代码时使用只有在运行时才能确定大小的值
  • str是动态大小的类型(注意不是 &str):只有运行时才能确定字符串的长度

下列代码无法正常工作:

let s1: str = "Hello there!";
let s2: str = "How's it going2";

使用&str解决:因为字符串切片存放的是str的地址和长度。

Rust 使用动态大小类型的通用方式:

  • 附带一些额外的元数据来存储动态信息的大小
  • 使用动态大小类型时总会把它的值放在某种指针后边

另外一种动态大小的类型:trait

  • 每个 trait 都是一个动态大小的类型,可以通过名称对其进行引用
  • 为了将 trait 用作 trait 对象, 必须将它放置在某种指针之后,
    例如放在 &dyn Trait 或 Box<dyn Trait>(Rc<dyn Trait>)之后

Sized trait

为了处理动态大小的类型,Rust 提供了一个 Sized trait 来确定一个类型的大小在编译时是否已知:

  • 编译时可计算出大小的类型会自动实现这一 trait
  • Rust 还会为每一个泛型函数隐式的添加 Sized 约束
fn ge<T>(t: T) {}
//会被隐式转成如下
fn ge<T: Sized>(t: T) {}

?Sized trait约束

默认情况下,泛型函数只能被用于编译时已经知道大小的类型,可以通过特殊语法解除这一限制
该语法只能用在sized语法上,不能用于其他。

fn ge<T>(t: T) {}
//会被隐式转成如下
fn ge<T: Sized>(t: T) {}
//?Sized表示T可能为sized也可能不是sized
fn ge<T: ?Sized>(t: &T) {} //这里变成了&T,因为T可能运行时才能确定大小,只能为指针

高级函数和闭包

函数指针

可将函数传递给其它函数,函数在传递过程中会被强制转换成fn类型,fn类型就是“函数指针(function pointer)”。

fn func1(x: i32) -> i32 {
    x + 1
}
fn func2(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}
fn main() {
    let a = func2(func1, 5);
    println!("{}", a);
}

函数指针与闭包的不同

  • fn是一个类型,不是一个trait,可直接指定fn为参数类型,不用声明一个Fn trait为约束的泛型参数。
  • 函数指针实现了全部3种闭包trait(Fn, FnMut, FnOnce),总是可把函数指针用作参数传递给一个链接闭包的函数,所以倾向于搭配闭包trait的泛型编写函数,可同时接收闭包和普通函数。
  • 某些情况下,只接收fn而不接收闭包,与外部不支持闭包的代码交互,如c函数。
fn main() {
    let l1 = vec![1, 2, 3];
    // 传入闭包
    let l_str: Vec<String> = l1.iter().map(|i| i.to_string()).collect();

    let l1 = vec![1, 2, 3];
    // 传入函数指针
    let l_str: Vec<String> = l1.iter().map(ToString::to_string).collect();
}

另一个枚举构造的例子:

fn main() {
    enum St {
        V(u32),
        Ss,
    }
    let v = St::V(5);// 这个构造器被实现成了函数,所以下边map可直接传入构造器函数指针
    let l: Vec<St> = (0u32..20).map(St::V).collect();
}

返回闭包

闭包使用trait进行表达,无法在函数中直接返回一个闭包,可将一个实现了该trait的具体类型作为返回值。

// fn re_closure() -> Fn(i32) -> i32 {
//     |x| x + 1 //试图返回一闭包,但报错trait objects must include the `dyn` keyword
// }
fn re_closure2() -> Box<dyn Fn(i32) -> i32> { //正确写法
    Box::new(|x| x + 1)
}
fn main() {}

宏macro

宏在 Rust 里指的是一组相关特性的集合称谓,主要有:

  • 使用 macro_rules!构建的声明宏(declarative macro)
  • 3种过程宏
    • 自定义#[derive] 宏,用于 struct 或 enum,可以为其指定随 derive 属性添加的代码
    • 类似属性的宏,在任何条目上添加自定义属性的
    • 类似函数的宏,看起来像函数调用,对其指定为参数的 foken 进行操作

函数与宏的差别

  • 本质上,宏是用来编写可以生成其它代码的代码(元编程,metaprogramming)
  • 函数在定义签名时,必须声明参数的个数和类型,宏可处理可变的参数
  • 编译器会在解释代码前展开宏
  • 宏的定义比函数复杂得多,难以阅读、理解、维护
  • 在某个文件调用宏时,必须提前定义宏或将宏引入当前作用域
  • 函数可以在任何位置定义并在任何位置使用的

macro_rules!声明宏(可能弃用)

rust中最常见的宏形式:声明宏,类似于match模式匹配,需要使用macro_rules!。

let v: Vec<u32> = vec![1, 2, 3];
// 上面的vec!宏,其简化的定义如下:
#[macro_export] //标注,表示宏所在的包只有被引入作用域后才可被使用,缺少这个标注则不能被引入作用域
macro_rules! vec { // 使用macro_rules!声明了一个名字为vec的宏
//以下内容有点像match的模式匹配,这里只有一个分支,匹配的是rust的代码结构
/*这是一个模式,$x:expr表示任意表达式expr,被命名为$x。
后面的,号表示紧跟表达式后可能是,号作为分隔符,再后面的*号表示*号前的语句“( $x:expr ),”可能被匹配0次或多次。
比如vec![1, 2, 3]表示$x会匹配1 2 3三次
*/
( $( $x:expr ),* ) => {
    {
        let mut temp_vec = Vec::new();
        $(
            temp_vec.push($x);
        )*
        temp_vec
    }
};
}
//匹配后大概如下:
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec

macro_rules!关键字有些奇怪的技术细节,rust团队正在推第二种声明宏的方式,修复了当前宏声明可能的极端情况,后会将本宏声明的方式标注为弃用。

基于属性来生成代码的过程宏

这种形式更像函数(某种形式的过程)一些,接收并操作输入的 Rust 代码,生成另外一些 Rust 代码作为结果。
三种过程宏:

  • 自定义派生
  • 属性宏
  • 函数宏

创建过程宏时:一宏定义必须单独放在它们自己的包中,并使用特殊的包类型。

use proc_macro;
#[some_attribute] //指定过程宏的占位符
// 定义过程宏的函数,参数和返回值都是TokenStream(定义于proc_macro包中)
pub fn func(input: TokenStream) -> TokenStream {}

自定义 derive 宏

类似属性的宏

  • 属性宏与自定义 derive 宏类似
  • 允许创建新的属性
  • 但不是为 derive 属性生成代码

属性宏更加灵活:

  • derive 只能用于 struct 和 enum
  • 属性宏可以用于任意条目,例如函数

类似函数的宏

函数宏定义类似于函数调用的宏,但比普通函数更加灵活
函数宏可以接收 TokenStream 作为参数
与另外两种过程宏一样,在定义中使用 Rust 代码来操作 TokenStream

posted @ 2023-11-30 00:49  00lab  阅读(64)  评论(0编辑  收藏  举报