3个不稳定版本
0.2.0 | 2023年4月15日 |
---|---|
0.1.1 | 2023年4月15日 |
0.1.0 | 2023年4月15日 |
#153 在 无标准库
每月31次下载
25KB
247 行
增强切片和索引的实用工具
简介
static_slicing
库提供了一组编译时检查切片和索引的有用实用工具。
背景
不想阅读这些内容?这里有一个简短总结:在某些常见情况下,切片和索引可能会很奇怪,所以这个库使这些事情变得不那么奇怪。现在你可以跳转到安装部分。
我最初开发这个库是为了供伍斯特理工学院的团队在2023年MITRE嵌入式Capture the Flag比赛中使用。
在为嵌入式系统安装的固件开发过程中,发现了一个有趣的语言痛点:数组切片和索引的编译时推断不足。
例如,这个函数无法编译(亲自尝试)
fn test() {
let a = [0u8, 1u8, 2u8, 3u8];
let x = a[4]; // there are only 4 elements in `a`! no good...
}
幸运的是,编译器知道访问一个4元素数组的第4个索引永远不会成功,并发出一个unconditional_panic
警告,默认情况下该警告会被转换为错误。
然而,这个几乎完全相同的函数可以成功编译,并在运行时崩溃(再次,亲自尝试)
fn test() {
let a = &[0u8, 1u8, 2u8, 3u8];
let x = a[4]; // there are only 4 elements in `a`! no good...
}
编译器知道a
是一个指向一个4元素u8
数组的引用 - 换句话说,&[u8; 4]
- 但是已经无法指出任何潜在的问题。
编译器对切片的分析(或缺乏分析)也可能有问题。这个新用户可能会期望编译的函数实际上不能编译
fn test() {
let a = &[0u8, 1u8, 2u8, 3u8];
let x: &[u8; 2] = &a[1..3]; // *x should be [a[1], a[2]], which is a 2-element array.
}
编译器的解释
3 | let x: &[u8; 2] = &a[1..3];
| -------- ^^^^^^^^ expected array `[u8; 2]`, found slice `[u8]`
| |
| expected due to this
这也无法工作
fn test() {
let a = &[0u8, 1u8, 2u8, 3u8];
let x: &[u8; 2] = &a[1..3].into();
}
然而,这可以工作
fn test() {
let a = &[0u8, 1u8, 2u8, 3u8];
let x: &[u8; 2] = &a[1..3].try_into().unwrap();
}
我并不是特别喜欢在所有东西的末尾都使用 .try_into().unwrap()
,但我确实是超级喜欢解决像这样的奇怪问题——这就是为什么这个库存在的原因!
安装
要安装当前版本的 static_slicing
,请将以下内容添加到您的 Cargo.toml
文件的 dependencies
部分
static-slicing = "0.2.0"
可以通过禁用默认功能来启用 no_std
支持
static-slicing = { version = "0.2.0", default-features = false }
注意:此库需要 Rust 1.59 或更高版本。
它是如何工作的?
不想读这个?跳到示例部分。
这个库引入了两种新的索引类型,利用了 const generics 的力量
StaticIndex<INDEX>
用于获取/设置 单个 元素,以及;StaticRangeIndex<START, LENGTH>
用于获取/设置 范围 的元素。
对于固定大小的数组(即 [T; N]
,其中 T
是元素类型,N
是数组长度),库提供了 Index
和 IndexMut
的实现,这两个实现都接受静态索引类型。通过 const panic 支持(以及一点类型系统黑客技术),可以防止无效的索引操作编译!
要了解这是如何实现的,请查看 IsValidIndex
和 IsValidRangeIndex
的实现。
对于其他类型(切片、Vec
等),需要使用 SliceWrapper
。由于我在开发过程中遇到了 Rust 的孤儿规则的一些问题,因此 SliceWrapper
是一个不幸的权宜之计。无论如何,SliceWrapper
被设计得尽可能“正常”。它可以传递给接受切片引用的函数,本质上看起来就像它所封装的数据。
性能
包含 Criterion 基准测试,用于固定大小数组索引。在我的计算机上(12核心 AMD Ryzen 9 5900X),内置索引功能和本库提供的功能之间没有观察到显著的 负面 性能差异。也就是说,这不会使你的代码变慢 200000 倍——实际上,任何性能影响在最坏的情况下也应该是微不足道的。
以下是单个基准测试运行的结果
基准类型 | 编译时检查 | 运行时检查 |
---|---|---|
单个索引 | 428.87 ps | 428.07 ps |
范围索引 | 212.19 ps | 212.69 ps |
(在某些其他运行中,编译时检查的单个索引比运行时检查的单个索引略快。)
示例
以下是如何使用此库的一些示例。
示例 1:从一个固定大小数组获取元素和切片
use static_slicing::{StaticIndex, StaticRangeIndex};
fn main() {
let x = [513, 947, 386, 1234];
// get the element at index 3
let y = x[StaticIndex::<3>];
// get 2 elements starting from index 1
let z: &[i32; 2] = &x[StaticRangeIndex::<1, 2>];
// this also works:
let z: &[i32] = &x[StaticRangeIndex::<1, 2>];
// prints: y = 1234
println!("y = {}", y);
// prints: z = [947, 386]
println!("z = {:?}", z);
}
示例 2:使用静态索引进行修改
use static_slicing::{StaticIndex, StaticRangeIndex};
fn main() {
let mut x = [513, 947, 386, 1234];
assert_eq!(x[StaticIndex::<1>], 947);
x[StaticIndex::<1>] = 1337;
assert_eq!(x[StaticIndex::<1>], 1337);
assert_eq!(x, [513, 1337, 386, 1234]);
x[StaticRangeIndex::<2, 2>] = [7331, 4040];
assert_eq!(x[StaticRangeIndex::<2, 2>], [7331, 4040]);
assert_eq!(x, [513, 1337, 7331, 4040]);
}
示例 3:防止越界
use static_slicing::{StaticIndex, StaticRangeIndex};
fn main() {
// read Background to understand why
// `x` being an array reference is important!
let x = &[513, 947, 386, 1234];
// this block compiles...
{
let y = x[5];
let z: &[i32; 2] = &x[2..5].try_into().unwrap();
}
// ...but not this one!
{
let y = x[StaticIndex::<5>];
let z = x[StaticRangeIndex::<2, 3>];
}
}
示例 4:从 SliceWrapper
中读取
use static_slicing::{StaticIndex, StaticRangeIndex, SliceWrapper};
fn main() {
let x = SliceWrapper::new(&[513, 947, 386, 1234][..]);
{
let y = x[StaticIndex::<3>];
let z = x[StaticRangeIndex::<2, 2>];
// prints: y = 1234
println!("y = {}", y);
// prints: z = [386, 1234]
println!("z = {:?}", z);
}
{
// both of these would panic at runtime
// since they're performing invalid operations.
let _ = x[StaticIndex::<5>];
let _ = x[StaticRangeIndex::<2, 4>];
}
}
示例 5:向 SliceWrapper
中写入
use static_slicing::{StaticIndex, StaticRangeIndex, SliceWrapper};
fn main() {
// SliceWrappers are mutable under the following conditions:
// 1. the wrapper itself has been declared as mutable, AND;
// 2. the wrapper owns its wrapped data OR contains
// a mutable reference to the data.
let mut x = SliceWrapper::new(vec![513, 947, 386, 1234]);
assert_eq!(x[StaticIndex::<3>], 1234);
x[StaticIndex::<3>] = 1337;
assert_eq!(x[StaticIndex::<3>], 1337);
assert_eq!(x[StaticRangeIndex::<1, 2>], [947, 386]);
x[StaticRangeIndex::<1, 2>] = [5555, 6666];
assert_eq!(x[StaticRangeIndex::<1, 2>], [5555, 6666]);
}
局限性
有一些局限性需要注意
rust-analyzer
似乎无法显示由库使用const panic生成的错误。SliceWrapper
在任何情况下都无法进行编译时检查,即使提供了具有已知长度的数组引用。这可能会在特化稳定之前被阻塞。SliceWrapper
的存在本身就是一个限制(至少在我心中是这样),除非对孤儿规则和一致性执行进行重大更改,否则它不太可能消失。(基本问题是无法为Index(Mut)
同时实现[T; N]
和[T]
。)
否则,这应该是一个相当容易处理的库 - 它确实让我的队友和我处理事情容易了很多!