【跟小嘉学 Rust 编程】十九、高级特性
系列文章目录
【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念
【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据
【跟小嘉学 Rust 编程】六、枚举和模式匹配
【跟小嘉学 Rust 编程】七、使用包(Packages)、单元包(Crates)和模块(Module)来管理项目
【跟小嘉学 Rust 编程】八、常见的集合
【跟小嘉学 Rust 编程】九、错误处理(Error Handling)
【跟小嘉学 Rust 编程】十一、编写自动化测试
【跟小嘉学 Rust 编程】十二、构建一个命令行程序
【跟小嘉学 Rust 编程】十三、函数式语言特性:迭代器和闭包
【跟小嘉学 Rust 编程】十四、关于 Cargo 和 Crates.io
【跟小嘉学 Rust 编程】十五、智能指针(Smart Point)
【跟小嘉学 Rust 编程】十六、无畏并发(Fearless Concurrency)
【跟小嘉学 Rust 编程】十七、面向对象语言特性
【跟小嘉学 Rust 编程】十八、模式匹配(Patterns and Matching)
【跟小嘉学 Rust 编程】十九、高级特性
文章目录
- 系列文章目录
- @[TOC](文章目录)
- 前言
- 一、 不安全 Rust
- 1.1、不安全 Rust
- 1.2、unsafe 关键字
- 1.2.1、解引用裸指针(Dereference a raw pointer)、
- 1.2.1.1、裸指针(raw pointer)
- 1.2.1.2、基于引用创建裸指针
- 1.2.1.3、内存地址创建裸指针
- 1.2.1.4、使用 `*` 解引用
- 1.2.1.5、基于指针指针创建裸指针
- 1.2.1.6、总结
- 1.2.2、调用不安全的函数或方法
- 1.2.2.1、调用不安全的函数或方法
- 1.2.2.2、用安全抽象包裹 unsafe 代码
- 1.2.2.3、FFI(Foreign Function Interface)
- 1.2.2.4、ABI(Application Binary Interface)
- 1.2.2.5、其他语言调用 Rust 函数
- 1.2.3、访问或修改可变静态变量
- 1.2.4、实现不安全的Trait
- 1.2.5、访问联合体的字段
- 1.2.6、一些实用库
- 二、高级 Trait
- 2.1、在 Trait定义中使用关联类型来指定占位类型
- 2.2、关联类型与泛型区别
- 2.3、默认泛型参数和运算符重载(operator overloading)
- 2.4、默认泛型参数的主要场景
- 2.5、完全限定语法(Fully Qualified Syntax)
- 2.6、使用 supertrait 要求 trait 附带其他 trait 的功能
- 2.7、使用 newtype 模式在外部类型上实现外部 trait
- 三、高级类型
- 3.1、newtype模式
- 3.2、使用类型别名创建类型同义词
- 3.3、Never type
- 3.3、动态大小 和 Size Trait
- 3.3.1、动态大小
- 3.3.2、Sized Trait
- 3.3.4、?Sized Trait 约束
- 四、高级函数和闭包
- 4.1、函数指针(function pointer)
- 4.2、返回闭包
- 五、宏(macro)
- 5.1、宏
- 5.2、宏和函数的区别
- 5.3、macro_rules! 声明宏
- 5.4、用过程宏(procedural macros)为属性标记生成代码
- 5.4.1、自定义 derive 宏
- 5.4.2、属性宏
- 5.4.3、函数宏
- 总结
文章目录
- 系列文章目录
- @[TOC](文章目录)
- 前言
- 一、 不安全 Rust
- 1.1、不安全 Rust
- 1.2、unsafe 关键字
- 1.2.1、解引用裸指针(Dereference a raw pointer)、
- 1.2.1.1、裸指针(raw pointer)
- 1.2.1.2、基于引用创建裸指针
- 1.2.1.3、内存地址创建裸指针
- 1.2.1.4、使用 `*` 解引用
- 1.2.1.5、基于指针指针创建裸指针
- 1.2.1.6、总结
- 1.2.2、调用不安全的函数或方法
- 1.2.2.1、调用不安全的函数或方法
- 1.2.2.2、用安全抽象包裹 unsafe 代码
- 1.2.2.3、FFI(Foreign Function Interface)
- 1.2.2.4、ABI(Application Binary Interface)
- 1.2.2.5、其他语言调用 Rust 函数
- 1.2.3、访问或修改可变静态变量
- 1.2.4、实现不安全的Trait
- 1.2.5、访问联合体的字段
- 1.2.6、一些实用库
- 二、高级 Trait
- 2.1、在 Trait定义中使用关联类型来指定占位类型
- 2.2、关联类型与泛型区别
- 2.3、默认泛型参数和运算符重载(operator overloading)
- 2.4、默认泛型参数的主要场景
- 2.5、完全限定语法(Fully Qualified Syntax)
- 2.6、使用 supertrait 要求 trait 附带其他 trait 的功能
- 2.7、使用 newtype 模式在外部类型上实现外部 trait
- 三、高级类型
- 3.1、newtype模式
- 3.2、使用类型别名创建类型同义词
- 3.3、Never type
- 3.3、动态大小 和 Size Trait
- 3.3.1、动态大小
- 3.3.2、Sized Trait
- 3.3.4、?Sized Trait 约束
- 四、高级函数和闭包
- 4.1、函数指针(function pointer)
- 4.2、返回闭包
- 五、宏(macro)
- 5.1、宏
- 5.2、宏和函数的区别
- 5.3、macro_rules! 声明宏
- 5.4、用过程宏(procedural macros)为属性标记生成代码
- 5.4.1、自定义 derive 宏
- 5.4.2、属性宏
- 5.4.3、函数宏
- 总结
前言
到目前为止,我们已经学习了 Rust 之中最常用的部分,本章节讲解如下特性
- 不安全 Rust:如何选择退出 Rust 的某些保证,并负责手动维护这些保证;
- 高级特征:关联类型、默认类型参数、完全限定语法、超特征以及特征相关的newtype模式
- 高级类型:更多关于newtype模式、类型别名、never类型和动态大小类型的内容高级函数和闭包:函数指针和返回闭包
- 宏:定义在编译时定义更多代码的方法
主要教材参考 《The Rust Programming Language》
一、 不安全 Rust
1.1、不安全 Rust
到目前为止,我们讨论的所有代码在编译时强制执行了 Rust 的内存安全保证,然而 Rust 内部隐藏着另一种语言,它不强制执行这些内存安全保证,它被称为不安全 Rust 和常规 Rust 一样工作,但赋予我们额外的能力。
不安全 Rust 之所以存在,是因为静态分析本质是保守的。当编译器试图确定代码是否支持这些保证时候,拒绝一些有效的程序比接受一些无效的程序要好。虽然代码可能没有问题,但如果 Rust 编译器没有足够的信息来确定,它将拒绝代码。在这些情况下,您可以使用不安全代码告诉编译器:“相信我,我知道我在做什么”。但是请注意,使用不安全的 Rust 的风险由您自己承担,如果不正确地使用不安全的代码,可能会由于内存不安全而出现问题,例如空指针解引用。
Rust 具有不安全另一面的原因是底层计算机硬件本质上是不安全的。如果 Rust 不允许你做不安全的操作,你就不能完成某些任务。 Rust 需要允许您进行低级系统编程,例如直接与操作系统交互,甚至编写自己的操作系统。处理低级系统编程是该语言的目标之一,让我们来探索一下不安全 Rust 可以做什么 以及如何做。
1.2、unsafe 关键字
要切换到不安全 Rust 使用 unsafe 关键字,然后启动一个包含不安全代码的新块,你可以在不安全的 Rust 中执行五个在安全 Rust 中无法执行的操作,我们称之为不安全的超能力。
- 解引用裸指针
- 调用不安全的函数或方法
- 访问或修改可变静态变量
- 实现不安全的Trait
- 访问联合体的字段
1.2.1、解引用裸指针(Dereference a raw pointer)、
1.2.1.1、裸指针(raw pointer)
裸指针(raw pointer,又称原生指针)在功能上跟引用类型,同时也需要显式地注明可变性。但是又和引用有所不同,裸指针形式如: * const T
和 *mut T
,分别代表了不可变和可变。
*
操作符,可以用于解引用,但是裸指针 * const T
中,*
只是类型名称的一部分,并没有解引用的含义。
至此我们已经学过三种类似指针的概念:引用、 智能指针、裸指针。与前两者不同,裸指针可以绕过 Rust 的借用规则,可以同时拥有一个数据的可变、不可变指针,甚至可以拥有多个可变的指针,并不能保证指向合法的内存,可以是null,没有实现任何自动回收的(drop)
总之裸指针和 C指针非常像,它需要以牺牲安全性为前提,但是我们获得了更好的性能,也可以跟其他语言和硬件打交道。
1.2.1.2、基于引用创建裸指针
范例:基于引用创建裸指针
let mut num = 5;let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
as 关键字可以用于强制类型转换,我们这里将引用 &mut / & mut num
强制转换为 * const i32 / * mut i32
在这段代码里面并没有 unsafe 身影,因为创建裸指针是安全的行为,而解引用裸指针才是不安全的行为。
范例:解引用裸指针
fn main() {let mut num = 5;let r1 = &num as *const i32;unsafe {println!("r1 is: {}", *r1);}
}
1.2.1.3、内存地址创建裸指针
我们基于引用创建裸指针,这种行为是很安全的,但是接下来的方式就不安全了。
let address = 0x012345usize;
let r = address as *const i32;
这里是基于一个内存地址来创建裸指针,这种行为相当危险,试图使用任意的内存地址往往是一种未定义的行为(undefined behavior),因为该内存地址有可能存在值,也有可能没有,就算有值,也大概率不是你需要的值。
同时编译器也有可能会优化这段代码,会造成没有任何内存访问发送,甚至程序还可能会发生段错误(segmentation fault)。总之,你几乎没有好的理由像上面这样实现代码,虽然它是可行的。
如果真的要使用内存地址,也是类似下面的用法啊,先取地址,再使用,而不是凭空捏造一个地址。
use std::{slice::from_raw_parts, str::from_utf8_unchecked};// 获取字符串的内存地址和长度
fn get_memory_location() -> (usize, usize) {let string = "Hello World!";let pointer = string.as_ptr() as usize;let length = string.len();(pointer, length)
}// 在指定的内存地址读取字符串
fn get_str_at_location(pointer: usize, length: usize) -> &'static str {unsafe { from_utf8_unchecked(from_raw_parts(pointer as *const u8, length)) }
}fn main() {let (pointer, length) = get_memory_location();let message = get_str_at_location(pointer, length);println!("The {} bytes at 0x{:X} stored: {}",length, pointer, message);// 如果大家想知道为何处理裸指针需要 `unsafe`,可以试着反注释以下代码// let message = get_str_at_location(1000, 10);
}
1.2.1.4、使用 *
解引用
let a = 1;
let b: *const i32 = &a as *const i32;
let c: *const i32 = &a;
unsafe {println!("{}", *c);
}
使用 * 可以对裸指针进行解引用,由于该指针的内存安全性并没有任何保证,因此我们需要使用 unsafe 来包裹解引用的逻辑(切记,unsafe 语句块的范围一定要尽可能的小,具体原因在上一章节有讲)。
以上代码另一个值得注意的点就是:除了使用 as 来显式的转换,我们还使用了隐式的转换方式 let c: *const i32 = &a;
。在实际使用中,我们建议使用 as 来转换,因为这种显式的方式更有助于提醒用户:你在使用的指针是裸指针,需要小心。
1.2.1.5、基于指针指针创建裸指针
let a: Box<i32> = Box::new(10);
// 需要先解引用a
let b: *const i32 = &*a;
// 使用 into_raw 来创建
let c: *const i32 = Box::into_raw(a);
1.2.1.6、总结
使用裸指针可以让我们创建两个可变指针指向同一个数据,如果使用安全的 Rust 是无法做到这一点,违背了借用规则,编译器会阻止。因此裸指针可以绕过借用规则,由此带来的数据竞争问题,需要大家自己处理。
重要用途就是跟 C 语言的代码进行交互(FFI),在讲解 FFI 之前,先看看调用 unsafe 函数和方法。
1.2.2、调用不安全的函数或方法
1.2.2.1、调用不安全的函数或方法
unsafe 函数从外表上来看跟普通函数并无区别,唯一区别就是需要使用 unsafe fn
来进行定义,这种定义行为告诉调用者:当调用此函数时候,你需要注意它的相关需求,因为 Rust 无法担保调用者在使用该函数时能满足它所需要的一切需求。
强制调用者加上 unsafe 语句块,就可以让他清晰认识到正在调用一个不安全的函数,需要小心看看文档,看看函数有哪些特别的要求需要被满足。
unsafe fn dangerous() {}
fn main() {dangerous();
}
如果试图这样调用,编译器就会报错
error[E0133]: call to unsafe function is unsafe and requires unsafe function or block--> src/main.rs:3:5|
3 | dangerous();| ^^^^^^^^^^^ call to unsafe function
范例:修改
unsafe fn dangerous() {}
fn main() {unsafe{dangerous();}
}
使用 unsafe 声明的函数时候,一定要看看相关文档,确定自己没有遗漏什么。
1.2.2.2、用安全抽象包裹 unsafe 代码
一个函数包含了 unsafe 不代表我们需要将整个函数定义 unsafe fn。事实上,在标准库中有大量的安全函数,他们内部都包含了 unsafe 函数。
对于 Rust 的借用检查器来说,它无法理解我哦们分别借用了同一个切片的两个不同部分,但是事实上,这种行为是没有问题,毕竟两个借用没有任何重叠之处。
use std::slice;fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {let len = slice.len();let ptr = slice.as_mut_ptr();assert!(mid <= len);unsafe {(slice::from_raw_parts_mut(ptr, mid),slice::from_raw_parts_mut(ptr.add(mid), len - mid),)}
}fn main() {let mut v = vec![1, 2, 3, 4, 5, 6];let r = &mut v[..];let (a, b) = split_at_mut(r, 3);assert_eq!(a, &mut [1, 2, 3]);assert_eq!(b, &mut [4, 5, 6]);
}
相比安全事项,这段代码没有那么好理解,我们甚至需要像C语言那样,通过指针地址的偏移去控制数组的分割。
1.2.2.3、FFI(Foreign Function Interface)
FFI(Foreign Function Interface) 可以用来与其他语言进行交互,但是并不是所有语言都这么称呼,例如 Java 称之为 JNI(Java Native Interface)。
FFI 之所以存在是由于现实中很多代码库都是由不同语言编写的,如果我们需要使用某个库,但是它是由其它语言编写的,那么往往只有两个选择:
- 对该库进行重写或移植
- 使用 FFI
前者相当不错,但是在很多时候,并没有那么多时间去重写,因此 FFI 就成了最佳选择。回到 Rust 语言上,由于这门语言依然很年轻,一些生态是缺失的,我们在写一些不是那么大众的项目时,可能会同时遇到没有相应的 Rust 库可用的尴尬境况,此时通过 FFI 去调用 C 语言的库就成了相当棒的选择。
还有在将 C/C++ 的代码重构为 Rust 时,先将相关代码引入到 Rust 项目中,然后逐步重构,也是不错的(为什么用不错来形容?因为重构一个有一定规模的 C/C++ 项目远没有想象中美好,因此最好的选择还是对于新项目使用 Rust 实现,老项目。。就让它先运行着吧)。
当然,除了 FFI 还有一个办法可以解决跨语言调用的问题,那就是将其作为一个独立的服务,然后使用网络调用的方式去访问,HTTP,gRPC 都可以。
言归正传,之前我们提到 unsafe 的另一个重要目的就是对 FFI 提供支持,它的全称是 Foreign Function Interface,顾名思义,通过 FFI , 我们的 Rust 代码可以跟其它语言的外部代码进行交互。
范例:
extern "C" {fn abs(input: i32) -> i32;
}fn main() {unsafe {println!("Absolute value of -3 according to C: {}", abs(-3));}
}
C 语言的代码定义在了 extern 代码块中, 而 extern 必须使用 unsafe 才能进行进行调用,原因在于其它语言的代码并不会强制执行 Rust 的规则,因此 Rust 无法对这些代码进行检查,最终还是要靠开发者自己来保证代码的正确性和程序的安全性。
1.2.2.4、ABI(Application Binary Interface)
ABI 定义了如何在汇编层面来调用该函数,在所有的ABI中,C语言是最常见的。
1.2.2.5、其他语言调用 Rust 函数
在 Rust 中调用其它语言的函数是让 Rust 利用其他语言的生态,那反过来可以吗?其他语言可以利用 Rust 的生态不?答案是肯定的。
我们可以使用 extern 来创建一个接口,其它语言可以通过该接口来调用相关的 Rust 函数。但是此处的语法与之前有所不同,之前用的是语句块,而这里是在函数定义时加上 extern 关键字,当然,别忘了指定相应的 ABI:
范例:
#[no_mangle]
pub extern "C" fn call_from_c() {println!("Just called a Rust function from C!");
}
上述代码可以编译称一个共享库,然后链接到C语言。
#[no_mangle]
注解告诉编译器:不要乱改函数的名称。
Mangling 的定义是:当 Rust 因为编译需要去修改函数的名称,例如为了让名称包含更多的信息,这样其它的编译部分就能从该名称获取相应的信息,这种修改会导致函数名变得相当不可读。
因此,为了让 Rust 函数能顺利被其它语言调用,我们必须要禁止掉该功能
1.2.3、访问或修改可变静态变量
我们在全局变量章节中讲解过,这里不讲述了
1.2.4、实现不安全的Trait
unsafe 的trait 确实不多见,如果大家还记得话,我们在之前的 Send 和 Sync 章节过实现过 unsafe 特征 Send。
范例:unsafe trait 声明很简单
unsafe trait Foo {// 方法列表
}unsafe impl Foo for i32 {// 实现相应的方法
}fn main() {}
1.2.5、访问联合体的字段
union 是用于 C 代码进行交互。访问 union 的字段是不安全的,因为 Rust 无法保证存在在 union 实例中的数据类型。
#[repr(C)]
union MyUnion {f1: u32,f2: f32,
}
从上述代码可以看出,union 的使用方式跟结构体确实很相似,但是前者的所有字段都共享同一个存储空间,意味着往 union 的某个字段写入值,会导致其它字段的值会被覆盖。
关于 Union 可以查看 https://doc.rust-lang.org/reference/items/unions.html。
1.2.6、一些实用库
1、rust-bindgen 和 cbindgen
对于 FFI 的调用来说,保证接口的正确性是非常重要的,这两个库可以帮我们自动生成相应的接口,其中 rust-bindgen 用于在 Rust 中访问 C 代码,而 cbindgen则反之。
2、cxx
如果需要跟 C++ 代码交互,非常推荐使用 cxx,它提供了双向的调用,最大的优点就是安全:是的,你无需通过 unsafe 来使用它!
3、Miri
miri 可以生成 Rust 的中间层表示 MIR,对于编译器来说,我们的 Rust 代码首先会被编译为 MIR ,然后再提交给 LLVM 进行处理。
可以通过 rustup component add miri 来安装它,并通过 cargo miri 来使用,同时还可以使用 cargo miri test 来运行测试代码。
miri 可以帮助我们检查常见的未定义行为(UB = Undefined Behavior),以下列出了一部分:
- 内存越界检查和内存释放后再使用(use-after-free)
- 使用未初始化的数据
- 数据竞争
- 内存对齐问题
但是需要注意的是,它只能帮助识别被执行代码路径的风险,那些未被执行到的代码是没办法被识别的。
4、Clippy
官方的 clippy 检查器提供了有限的 unsafe 支持,虽然不多,但是至少有一定帮助。例如 missing_safety_docs 检查可以帮助我们检查哪些 unsafe 函数遗漏了文档。
需要注意的是: Rust 编译器并不会默认开启所有检查,大家可以调用 rustc -W help 来看看最新的信息。
5、prusti
prusti 需要大家自己来构建一个证明,然后通过它证明代码中的不变量是正确被使用的,当你在安全代码中使用不安全的不变量时,就会非常有用。
6、模糊测试(fuzz testing)
cargo install cargo-fuzz
二、高级 Trait
2.1、在 Trait定义中使用关联类型来指定占位类型
关联类型(associated type) 是 Trait 中 的类型占位符,它可以用于 Trait 的方法签名中,可以定义包含某些类型的 Trait ,而实现前无需要知道这些类型是什么
pub trait Iterator {type Item;fn next(&mut self) -> Option<Self::Item>;
}
2.2、关联类型与泛型区别
泛型 | 关联类型 |
---|---|
每次实现 Trait 时标注类型 | 无需标注类型 |
可以为一个类型多次实现某个 trait(不同的泛型参数) | 无法为单个类型多次实现某个 Trait |
2.3、默认泛型参数和运算符重载(operator overloading)
可以在使用泛型参数时为泛型指定一个默认的具体类型,语法 <PlaceholderType=ConcreteType>
这种技术常用于运算符重载, Rust 不允许创建自己的运算符以及重载任意的运算符,但是可以通过实现 std::ops 中列出的那些 Trait 来重载一部分相应的运算符。
use std::ops::Add;#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {x: i32,y: i32,
}impl Add for Point {type Output = Point;fn add(self, other: Point) -> Point {Point {x: self.x + other.x,y: self.y + other.y,}}
}fn main() {assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },Point { x: 3, y: 3 });
}
Add Trait 声明如下
trait Add<Rhs=Self> {type Output;fn add(self, rhs: Rhs) -> Self::Output;
}
2.4、默认泛型参数的主要场景
- 扩展一个类型不破坏现有代码
- 允许在大部分用户都不需要的特定场景下进行自定义
2.5、完全限定语法(Fully Qualified Syntax)
示例代码:
trait Pilot {fn fly(&self);
}trait Wizard {fn fly(&self);
}struct Human;impl Pilot for Human {fn fly(&self) {println!("This is your captain speaking.");}
}impl Wizard for Human {fn fly(&self) {println!("Up!");}
}impl Human {fn fly(&self) {println!("*waving arms furiously*");}
}fn main() {let person = Human;person.fly();
}
默认调用 Human 的fly方法,我们如果要调用 Trait 的实现方法可以使用下列调用方式
fn main() {let person = Human;Pilot::fly(&person);Wizard::fly(&person);person.fly();
}
如果我们要调用的是关联函数。
trait Animal {fn baby_name() -> String;
}struct Dog;impl Dog {fn baby_name() -> String {String::from("Spot")}
}impl Animal for Dog {fn baby_name() -> String {String::from("puppy")}
}fn main() {println!("A baby dog is called a {}", Dog::baby_name());
}
此时如果我们想要调用 Animal 的 baby_name 语法,则需要使用 完全限定语法。
fn main() {println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
完全限定语法形式如下
<Type as Trait>::function(receiver_if_method, next_arg, ...);
2.6、使用 supertrait 要求 trait 附带其他 trait 的功能
有需要在一个 trait 使用其他 trait。
use std::fmt;trait OutlinePrint: fmt::Display {fn outline_print(&self) {let output = self.to_string();let len = output.len();println!("{}", "*".repeat(len + 4));println!("*{}*", " ".repeat(len + 2));println!("* {} *", output);println!("*{}*", " ".repeat(len + 2));println!("{}", "*".repeat(len + 4));}
}
2.7、使用 newtype 模式在外部类型上实现外部 trait
孤儿规则:只有当 trait 或 类型定义在本地包,才能为该类型实现这个trait。
可以通过 newtype 模式来绕过这一规则,利用 tuple struct 创建一个新的类型。
use std::fmt;struct Wrapper(Vec<String>);impl fmt::Display for Wrapper {fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {write!(f, "[{}]", self.0.join(", "))}
}fn main() {let w = Wrapper(vec![String::from("hello"), String::from("world")]);println!("w = {}", w);
}
三、高级类型
3.1、newtype模式
- 用来静态的保证各种值之间不会混淆并表明值的单位;
- 为类型的某些细节提供抽象能力
- 通过轻量级的封装来隐藏内部实现细节
3.2、使用类型别名创建类型同义词
使用type 关键字创建类型别名,类型别名并不是独立的类型。主要用途是减少代码字符重复
type Kilometers = i32;let x: i32 = 5;let y: Kilometers = 5;println!("x + y = {}", x + y);
3.3、Never type
有一个名为 !
的特殊类型,它没有任何值,行话叫做空类型(empty type),它在不返回的函数中充当返回类型
不返回值的函数也被称做发散函数(diverging function)
fn bar() -> ! {// --snip--
}
这种代码会报错。
loop循环 和 continue 的返回类型就是 !
3.3、动态大小 和 Size Trait
3.3.1、动态大小
Rust 需要在编译时确定为一个特定的类型值分配多少空间。动态大小的类型(Dynamically Sized Types, DST)概念,编写代码时使用只有在运行时才能确定大小的值。
Rust 使用动态大小类型的通用方式:附带一些元数据来存储动态信息的大小,使用动态类型大小总会把它的值放在某种指针后面。
3.3.2、Sized Trait
为了处理动态大小的类型, Rust 提供了一个 Sized Trait 来确定一个类型的大小在编译时是否已知
- 编译时可以计算出大小的类型会自动实现这一个 trait
- Rust 还会为每个泛型函数隐式添加 Sized 约束
3.3.4、?Sized Trait 约束
使用 ?Sized 表示 泛型 可能是 Sized 也可能不是Sized,参数必须是引用。
四、高级函数和闭包
4.1、函数指针(function pointer)
可以将函数传递给其他函数,函数在传递过程中会被强制转换 fn 类型,fn 类型就是函数指针。
fn add_one(x: i32) -> i32 {x + 1
}fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {f(arg) + f(arg)
}fn main() {let answer = do_twice(add_one, 5);println!("The answer is: {}", answer);
}
函数指针是一个类型,不是一个 trait,全部实现了三种闭包 trait。
4.2、返回闭包
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {Box::new(|x| x + 1)
}
五、宏(macro)
5.1、宏
宏在 Rust 里面指的是一组相关特性的集合称谓
- 使用 macro_rules! 构建声明宏(declarative macro)
- 三种过程宏
- 自定义 #[derive] 宏,用于 struct 或 enum,可以为其制定随 derive 属性添加的代码
- 类似属性的宏(Attribute-like macro),在任意条目上添加自定义属性
- 类似函数的宏(Function-like macro),看起来像函数调用,对其指定为参数的 token 进行操作
5.2、宏和函数的区别
- 宏是通过一种代码生成另一种代码,Rust 的函数签名是固定的,而宏可以拥有可变数量的参数。
- 编译器会在解释代码进行展开宏
- 函数可以在任何位置定义和使用
5.3、macro_rules! 声明宏
#[macro_export]
macro_rules! vec {( $( $x:expr ),* ) => {{let mut temp_vec = Vec::new();$(temp_vec.push($x);)*temp_vec}};
}
#[macro_export]
注释将宏进行了导出,这样其他包可以将宏导入到当前座女与中,然后才能使用。标准库 vec!宏已经通过 std::prelude 自动引入。
对于 macro_rules! 存在一些问题,Rust 计划在未来使用新的生命宏来替换它。
5.4、用过程宏(procedural macros)为属性标记生成代码
从形式上来看,过程宏跟函数较为相像,但过程宏是使用源代码作为输入参数,基于代码进行一系列操作后,再输出一段全新的代码。注意,过程宏中的 derive 宏输出的代码并不会替换之前的代码,这一点与声明宏有很大的不同!
有三种类型:自定义 derive、属性宏、函数宏。
5.4.1、自定义 derive 宏
extern crate proc_macro;use proc_macro::TokenStream;
use quote::quote;
use syn;
use syn::DeriveInput;#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {// 基于 input 构建 AST 语法树let ast:DeriveInput = syn::parse(input).unwrap();// 构建特征实现代码impl_hello_macro(&ast)
}fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {let name = &ast.ident;let gen = quote! {impl HelloMacro for #name {fn hello_macro() {println!("Hello, Macro! My name is {}!", stringify!(#name));}}};gen.into()
}
5.4.2、属性宏
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
5.4.3、函数宏
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
总结
以上就是今天要讲的内容
相关文章:

【跟小嘉学 Rust 编程】十九、高级特性
系列文章目录 【跟小嘉学 Rust 编程】一、Rust 编程基础 【跟小嘉学 Rust 编程】二、Rust 包管理工具使用 【跟小嘉学 Rust 编程】三、Rust 的基本程序概念 【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念 【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据 【跟小嘉学…...

pandas由入门到精通-数据清洗-缺失值处理
pandas-02-数据清洗&预处理 A.缺失值处理1. Pandas缺失值判断2. 缺失值过滤2.1 Series.dropna()2.2 DataFrame.dropna()3. 缺失值填充3.1 值填充3.2 向前/向后填充文中用S代指Series,用Df代指DataFrame 数据清洗是处理大型复杂情况数据必不可少的步骤,这里总结一些数据清…...

Redis 教程 - 主从复制
Redis 教程 - 主从复制 Redis 支持主从复制(Master-Slave Replication),通过主从复制可以将一个 Redis 服务器(主节点)的数据复制到其他 Redis 服务器(从节点),以实现数据的冗余备份…...

[递归] 子集 全排列和组合问题
1.1 子集I 思路可以简单概括为 二叉树,每一次分叉要么选择一个元素,要么选择空,总共有n次,因此到n1进行保存结果,返回。像这样: #include <cstdio> #include <vector> #include <algorithm&…...

ELK安装、部署、调试(四)KAFKA消息队列的安装和部署
1.简介 Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者在网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因素。 这些数据通常是由于吞吐量的要求而通…...

半导体晶片机器视觉测量及MARK点视觉定位
半导体晶片机器视觉测量及MARK点视觉定位 客户的需求: 检测内容: SMT行业晶片位置角度与PCB板Mark点位置的测试测量 检测要求: 精度0.04mm,移动速度100mm/s 视觉可行性分析: 对样品进行了光学实验,并进行图像处理,…...

ranger无法同步用户问题解决
1.首先就是定位日志,日志目录 cd /var/log/ranger/usersync 定位到问题报错如下: LdapDeltaUserGroupBuilder.getUsers() failed with exception:java.naming.AuthticationExceptiom :[LDAP:error code 49 - Invalid Credentials]:remaing name ‘ouPeople,dc*.dccom’ 解决办法…...

使用通信顺序进程(CSP)模型的 Go 语言通道
在并发编程中,许多编程语言采用共享内存/状态模型。然而,Go 通过实现 通信顺序进程(CSP)模型来区别于众多。在CSP中,程序由不共享状态的并行进程组成;相反,它们通过通道进行通信和同步操作。因此…...

VPN网关
阿里云VPN网关(VPN Gateway,简称VPN)是一款基于Internet,通过加密通道将企业数据中心、办公网或终端与专有网络(VPC) 安全可靠连接起来的服务。 VPN网关提供IPsec-VPN和SSL-VPN两种。 网络连接方式应用场景IPsec-VPN支持在企业本地数据中心、企业办公网…...

产品展示视频制作的要点
制作产品展示视频时通过精心策划的视频剧本和拍摄手法,可以准确地呈现活动的目的、主题和特点,让观众更好地理解和认同活动的意义。深圳产品活动视频制作公司老友记小编还为您整理了以下一些重要的制作要点: 1.明确目标受众:了解你…...

appium+python自动化测试
获取APP的包名 1、aapt即Android Asset Packaging Tool,在SDK的build-tools目录下。该工具可以查看apk包名和launcherActivity 2、在android-sdk里面双击SDK-manager,下载buidl-tools 3、勾选build-tools,随便选一个版本,我这里选的是24的版…...

【AI辅助办公】PDF转PPT,移除水印
PDF转PPT 将PDF上传链接即可转换成PPT。 https://www.camscanner.com/pdftoppthttps://www.camscanner.com/pdftoppt移除水印 第一步:打开视图-宏 第二步:输入宏名(可以是人以文字…...

ssm农业视频实时发布管理系统源码
ssm农业视频实时发布管理系统源码108 开发工具:idea 数据库mysql5.7 数据库链接工具:navcat,小海豚等 技术:ssm package com.controller;import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; impo…...

【100天精通python】Day48:python Web开发_WSGI接口与使用
目录 1 WSGI接口 1.1 CGI 简介 1.2 WSGI 简介 1.3 定义 WSGI 接口 1.3.1 应用程序(Application) 1.3.2 服务器(Server) 1.4 WSGI 接口的使用示例 1.5 WSGI接口的优势 1 WSGI接口 上一节实现了静态服务器,但是当…...

Understanding Lockup Cells
工具会分析扫描链和EDT逻辑之间的控制时序元素的时钟的时序关系,当必须要同步时钟并保持数据完整性时插入边沿触发寄存器(lockup cells)。 可以使用report_edt_lockup_cells命令来展示工具已经插入的lockup cells的详细报告。 Lockup Cell Insertion 工具会分析控制时序元…...

javaCV实现java图片ocr提取文字效果
引入依赖: <dependency><groupId>org.bytedeco</groupId><artifactId>javacv-platform</artifactId><version>1.5.5</version></dependency> 引入中文语言训练数据集:chi_sim GitHub - tesseract-ocr…...

七牛云OSS存储
前言: 七牛云的存储项目的附件,需要开发一套七牛云的工具类,可以使用该工具类进行七牛云服务器进行文件的上传与下载操作; 七牛云的文档学习: 相关的依赖项的配置: <dependency><groupId>com.amazonaws</groupId><artifactId>aws-java-sdk-s3…...

11.物联网lwip,网卡原理
一。LWIP协议栈内存管理 1.LWIP内存管理方案 (1)堆heap 1.灰色为已使用内存 2.黑色为未使用内存 3.紫色为使用后内存 按照某种算法,把数据放在内存块中 (2)池pool 设置内存池,设置成大小相同的内存块。 2…...

视频监控/视频汇聚/视频云存储EasyCVR平台接入华为ivs3800平台提示400报错,该如何解决?
开源EasyDarwin视频监控TSINGSEE青犀视频平台EasyCVR能在复杂的网络环境中,将分散的各类视频资源进行统一汇聚、整合、集中管理,在视频监控播放上,视频云存储/安防监控汇聚平台可支持1、4、9、16个画面窗口播放,可同时播放多路视频…...

WordPress主题Zing V2.2.1/模块化WordPress响应式通用企业商城主题
WordPress主题Zing V2.2.1,模块化WordPress响应式通用企业商城主题。 功能介绍 百度熊掌号文章实时推送、原创保护 多设备支持自适应布局,支持电脑、Pad、手机以及各种浏览器 SEO优化首页、文章、页面、分类均支持自定义标题、关键字和描述 速度优化…...

【无需公网IP】在树莓派上搭建Web站点
目录 1.概述 2.使用 Raspberry Pi Imager 安装 Raspberry Pi OS 3.设置 Apache Web 服务器 3.1测试 web 站点 3.2安装静态样例站点 3.3将web站点发布到公网 3.4安装 Cpolar 3.5cpolar进行token认证 3.6生成cpolar随机域名网址 3.7生成cpolar二级子域名 3.8将参数保存…...

出差在外,远程访问企业局域网象过河ERP系统「内网穿透」
文章目录 概述1.查看象过河服务端端口2.内网穿透3. 异地公网连接4. 固定公网地址4.1 保留一个固定TCP地址4.2 配置固定TCP地址 5. 使用固定地址连接 概述 ERP系统对于企业来说重要性不言而喻,不管是财务、生产、销售还是采购,都需要用到ERP系统来协助。…...

Vue2-replace属性、编程式路由导航、缓存路由组件、两个新的生命周期钩子、路由守卫、路由器工作模式
🥔:如果事与愿违,那一定是上天另有安排 更多Vue知识请点击——Vue.js VUE2-Day13 router-link的replace属性编程式路由导航1、什么是编程式路由导航2、如何编码3、使用案例示例说明 缓存路由组件两个新的生命周期钩子路由守卫1、路由元信息2、…...

C语言:指针的运算
一、指针 或 - 整数 指针 或 - 整数表示指针跳过几个字节(具体跳过几个字节由指针类型决定) 本文不做具体讲解,详解跳转链接: 《C语言:指针类型的意义》 二、指针 - 指针 前提条件:指针类型相同并且指向同…...

设计模式的使用——模板方法模式+动态代理模式
一、需求介绍 现有自己写的的一套审批流程逻辑,由于代码重构,需要把以前的很多业务加上审批的功能,再执行完审批与原有业务之后,生成一个任务,然后再统一处理一个任务(本来是通过数据库作业去处理的&#x…...

C++学习记录——삼십 智能指针
文章目录 1、为什么需要智能指针?2、内存泄漏3、智能指针的使用及原理1、RAII思想2、拷贝问题1、unique_ptr2、shared_ptr1、多线程2、循环引用3、定制删除器 1、为什么需要智能指针? 看一个场景 int div() {int a, b;cin >> a >> b;if (b…...

插件式架构 与 ReSharper、Visual Studio的故事
文章首发地址 ReSharper和Visual Studio的故事 ReSharper是一款由JetBrains公司开发的Visual Studio插件,它主要用于提高Visual Studio的开发效率和改善代码质量。ReSharper在早期的版本中被称为"Omea Code",它最初是JetBrains一个研究项目的…...

Python UDP编程
前面我们讲了 TCP 编程,我们知道 TCP 可以建立可靠连接,并且通信双方都可以以流的形式发送数据。本文我们再来介绍另一个常用的协议--UDP。相对TCP,UDP则是面向无连接的协议。 UDP 协议 我们来看 UDP 的定义: UDP 协议ÿ…...

结构体(个人学习笔记黑马学习)
1、结构体的定义和使用 #include <iostream> using namespace std; #include <string>struct Student {string name;int age;int score; }s3;int main() {//1、struct Student s1;s1.name "张三";s1.age 18;s1.score 100;cout << "姓名&a…...

小白带你学习linux的PXE装机
目录 目录 一、PXE是什么? 二、PXE的组件: 1、vsftpd/httpd/nfs 2、tftp 3、dhcp 三、配置dhcp 1、关闭防火墙与selinux和配置本地yum源 2、安装dhcp服务 3、配置dhcp配置文件 四、配置vsftpd 五、配置tftp 1、安装tftp-server 2、启动tft…...