《Rust编程实战》系列第50篇
上一篇文章中,我们学习了Rust的
Associated Type关联类型,知道Trait不仅可以定义方法,还可以定义由具体实现决定的内部类型。本篇继续深入Rust泛型体系,学习另一项非常实用的能力:
arduino
const Generics
也就是"const泛型"。
普通泛型解决的是:
类型可以变化。
例如:
r
struct Container<T> {
value: T,
}
这里T可以是:
rust
i32
String
User
Product
而const泛型解决的是:
某个编译期常量也可以作为泛型参数。
例如数组长度:
rust
struct Buffer<const N: usize> {
data: [u8; N],
}
现在:
swift
Buffer<8>
Buffer<16>
Buffer<1024>
都是根据不同常量参数生成的具体类型。
const泛型非常适合:
-
固定长度数组
-
矩阵
-
固定容量Buffer
-
向量
-
坐标
-
密码块
-
网络数据包
-
嵌入式开发
本文将介绍:
-
什么是const泛型
-
const N: usize -
泛型数组长度
-
[T;N] -
const泛型函数
-
const泛型Struct
-
const泛型impl
-
类型泛型与const泛型组合
-
多个const参数
-
const泛型与运行时参数区别
-
Matrix矩阵案例
-
Buffer缓冲区案例
-
const泛型常见错误
-
实际开发最佳实践
什么是const泛型
普通泛型参数声明:
r
<T>
表示:
r
T是一个类型参数
const泛型声明:
rust
<const N: usize>
表示:
N是一个编译期常量参数
例如:
rust
fn print_array<const N: usize>(
values: [i32; N],
) {
println!("长度:{}", N);
println!("{:?}", values);
}
调用:
scss
fn main() {
print_array([1, 2, 3]);
print_array([1, 2, 3, 4, 5]);
}
编译器会自动推断:
ini
第一次调用
N = 3
第二次调用
N = 5
同一个函数可以处理不同长度的数组。
为什么数组需要const泛型
Rust中的数组类型包含两个重要信息:
元素类型
数组长度
例如:
csharp
[i32; 3]
和:
csharp
[i32; 5]
不是同一个类型。
可以理解为:
css
[i32;3] != [i32;5]
以前如果想写一个函数处理多个数组长度,会比较麻烦。
例如:
javascript
fn print3(values: [i32; 3]) {}
fn print4(values: [i32; 4]) {}
fn print5(values: [i32; 5]) {}
const泛型可以统一成:
rust
fn print<const N: usize>(
values: [i32; N],
) {
}
这样任意长度数组都可以使用同一套逻辑。
const N:usize是什么意思
最常见形式:
rust
<const N: usize>
其中:
rust
const
表示这是常量泛型参数
N
参数名称
usize
参数类型
数组长度本身使用:
rust
usize
所以数组相关const泛型通常定义为:
rust
const N: usize
例如:
rust
struct ArrayHolder<const N: usize> {
values: [i32; N],
}
最简单的const泛型函数
例如计算数组长度:
rust
fn array_len<const N: usize>(
values: &[i32; N],
) -> usize {
N
}
fn main() {
let values = [10, 20, 30];
println!("{}", array_len(&values));
}
输出:
3
这里甚至不需要:
go
values.len()
因为数组长度已经包含在类型参数:
N
中。
使用N创建数组
const参数可以用于数组类型:
rust
fn zeros<const N: usize>() -> [i32; N] {
[0; N]
}
调用:
rust
fn main() {
let first: [i32; 3] = zeros();
let second: [i32; 10] = zeros();
println!("{:?}", first);
println!("{:?}", second);
}
输出:
csharp
[0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
调用位置的类型信息可以帮助Rust推断N。
显式指定const参数
也可以手动指定:
ini
let values = zeros::<5>();
这里:
makefile
::<5>
与类型泛型中的:
ruby
::<i32>
类似。
完整:
rust
fn main() {
let values = zeros::<5>();
println!("{:?}", values);
}
const泛型Struct
const泛型不仅可以用于函数,也可以用于Struct。
例如:
rust
#[derive(Debug)]
struct Buffer<const N: usize> {
data: [u8; N],
}
创建:
arduino
fn main() {
let buffer = Buffer::<8> {
data: [0; 8],
};
println!("{:?}", buffer);
}
这里:
swift
Buffer<8>
是一个具体类型。
不同N产生不同具体类型
例如:
ini
let first = Buffer::<8> {
data: [0; 8],
};
let second = Buffer::<16> {
data: [0; 16],
};
它们分别是:
swift
Buffer<8>
Buffer<16>
这两个不是同一个具体类型。
因此下面不能简单互相赋值:
sql
let first: Buffer<8> = second;
因为:
swift
Buffer<8> != Buffer<16>
const泛型参数是类型的一部分。
const泛型impl
为const泛型Struct定义方法:
rust
struct Buffer<const N: usize> {
data: [u8; N],
}
impl<const N: usize> Buffer<N> {
fn new() -> Self {
Self {
data: [0; N],
}
}
fn len(&self) -> usize {
N
}
}
使用:
arduino
fn main() {
let buffer = Buffer::<32>::new();
println!("{}", buffer.len());
}
这里:
rust
impl<const N: usize> Buffer<N>
表示:
为任意N的Buffer实现这些方法。
修改Buffer内容
例如:
rust
impl<const N: usize> Buffer<N> {
fn set(
&mut self,
index: usize,
value: u8,
) {
if index < N {
self.data[index] = value;
}
}
fn get(
&self,
index: usize,
) -> Option<&u8> {
self.data.get(index)
}
}
使用:
csharp
fn main() {
let mut buffer =
Buffer::<8>::new();
buffer.set(0, 100);
println!("{:?}", buffer.get(0));
}
const参数:
N
可以直接参与方法逻辑。
类型泛型与const泛型组合
const泛型经常与普通类型泛型一起使用。
例如:
rust
#[derive(Debug)]
struct Array<T, const N: usize> {
data: [T; N],
}
这里:
r
T
决定数组元素类型
N
决定数组长度
例如:
ini
let numbers = Array {
data: [1, 2, 3],
};
可以推断:
ini
T = i32
N = 3
另一个:
javascript
let flags = Array {
data: [true, false, true, true],
};
则:
ini
T = bool
N = 4
类型泛型Struct的impl
例如:
rust
struct FixedArray<T, const N: usize> {
data: [T; N],
}
impl<T, const N: usize>
FixedArray<T, N>
{
fn len(&self) -> usize {
N
}
fn get(
&self,
index: usize,
) -> Option<&T> {
self.data.get(index)
}
}
这里同时声明:
rust
T
const N: usize
顺序非常清晰:
diff
类型参数
+
常量参数
生命周期、类型、const泛型可以组合
Rust泛型系统中可以同时出现:
rust
'a
T
const N: usize
例如:
rust
struct SliceView<
'a,
T,
const N: usize,
> {
data: &'a [T; N],
}
这里:
css
'a
描述引用生命周期
T
描述元素类型
N
描述数组长度
这三个维度分别解决不同问题。
const泛型函数处理任意类型数组
例如:
rust
fn first<T, const N: usize>(
values: &[T; N],
) -> Option<&T> {
values.first()
}
使用:
rust
fn main() {
let numbers = [10, 20, 30];
let names = [
String::from("Tom"),
String::from("Alice"),
];
println!("{:?}", first(&numbers));
println!("{:?}", first(&names));
}
现在函数既不限制元素类型,也不限制数组长度。
配合Trait Bound
如果函数需要打印数组:
rust
use std::fmt::Debug;
fn print_array<
T,
const N: usize,
>(
values: &[T; N],
)
where
T: Debug,
{
println!("{:?}", values);
}
这里:
makefile
T:Debug
解决打印能力。
N
解决数组长度。
这正体现了Rust类型系统各个机制的配合。
多个const泛型参数
一个类型可以拥有多个const参数。
例如矩阵:
rust
struct Matrix<
T,
const ROWS: usize,
const COLS: usize,
> {
data: [[T; COLS]; ROWS],
}
这里:
r
T
元素类型
ROWS
行数
COLS
列数
例如:
ini
let matrix = Matrix {
data: [
[1, 2, 3],
[4, 5, 6],
],
};
类型可以推断成:
rust
Matrix<i32,2,3>
Matrix矩阵案例
定义:
rust
#[derive(Debug)]
struct Matrix<
T,
const R: usize,
const C: usize,
> {
data: [[T; C]; R],
}
实现:
rust
impl<
T,
const R: usize,
const C: usize,
> Matrix<T, R, C> {
fn rows(&self) -> usize {
R
}
fn cols(&self) -> usize {
C
}
fn get(
&self,
row: usize,
col: usize,
) -> Option<&T> {
self.data
.get(row)?
.get(col)
}
}
使用:
csharp
fn main() {
let matrix = Matrix {
data: [
[1, 2],
[3, 4],
[5, 6],
],
};
println!(
"{} x {}",
matrix.rows(),
matrix.cols()
);
println!("{:?}", matrix.get(1, 0));
}
输出:
scss
3 x 2
Some(3)
矩阵尺寸在编译阶段已经成为类型信息。
为什么矩阵适合const泛型
如果用运行时字段:
rust
struct Matrix {
rows: usize,
cols: usize,
data: Vec<f64>,
}
矩阵大小只有运行时才能确定。
而const泛型:
rust
Matrix<f64, 3, 3>
可以让编译器直接知道:
这是3x3矩阵
这对:
-
数学库
-
图形计算
-
嵌入式系统
-
SIMD相关算法
非常有价值。
固定容量Buffer案例
定义:
rust
#[derive(Debug)]
struct FixedBuffer<const N: usize> {
data: [u8; N],
len: usize,
}
实现:
rust
impl<const N: usize>
FixedBuffer<N>
{
fn new() -> Self {
Self {
data: [0; N],
len: 0,
}
}
fn push(
&mut self,
value: u8,
) -> Result<(), &'static str> {
if self.len >= N {
return Err("Buffer已满");
}
self.data[self.len] = value;
self.len += 1;
Ok(())
}
fn len(&self) -> usize {
self.len
}
fn capacity(&self) -> usize {
N
}
}
使用:
arduino
fn main() {
let mut buffer =
FixedBuffer::<4>::new();
buffer.push(10).unwrap();
buffer.push(20).unwrap();
println!("{:?}", buffer);
}
它不会像Vec一样根据需要动态扩容,而是固定:
ini
capacity = 4
固定容量容器的价值
固定容量类型适合:
-
不希望堆分配
-
内存非常有限
-
最大容量提前确定
-
网络协议数据包
-
嵌入式设备
例如:
swift
FixedBuffer<64>
可以在编译阶段明确其底层数组大小。
const泛型与Vec的区别
Vec:
r
Vec<T>
长度可以在运行时变化:
ini
let mut values = vec![1, 2];
values.push(3);
数组:
csharp
[T; N]
长度固定,并且:
N
是类型的一部分。
因此:
r
Vec<T>
动态长度
[T;N]
固定长度
const泛型主要让固定长度类型拥有更强的复用能力。
const泛型与运行时参数区别
假设:
arduino
fn create_buffer(size: usize) {
}
这里size是运行时值。
而:
csharp
fn create_buffer<
const N: usize,
>() {
}
这里N属于编译期泛型参数。
重要区别:
arduino
运行时参数
调用函数时作为普通数据存在
const泛型
参与类型生成
例如:
swift
Buffer<8>
和:
swift
Buffer<16>
是不同类型。
但普通:
php
Buffer::new(8)
Buffer::new(16)
如果Buffer内部使用Vec,它们仍然可能是同一个类型。
const参数是不是普通变量
不是。
例如:
rust
fn example<const N: usize>() {
println!("{}", N);
}
可以读取N,但不能:
ini
N += 1;
因为它是编译期常量参数,而不是可变局部变量。
const泛型支持哪些类型
常见稳定const泛型参数主要使用整数、布尔、字符等适合作为结构常量的类型,其中数组长度最典型的是:
rust
usize
实际开发中最常见形式仍然是:
rust
const N: usize
对于复杂自定义类型作为const参数等更高级能力,需要结合具体Rust版本和稳定特性判断,初学阶段不必过早深入。
const表达式要注意稳定能力
可能会自然想到:
rust
fn merge<
const A: usize,
const B: usize,
>(
a: [u8; A],
b: [u8; B],
) -> [u8; A + B] {
}
这种涉及泛型常量表达式的能力比基础:
csharp
[T; N]
复杂得多,并不是所有表达式都能在所有稳定Rust环境下直接使用。
因此学习const泛型时,建议先掌握稳定、常见的:
csharp
[T; N]
struct Type<const N: usize>
再逐步接触更高级的泛型常量表达式。
const泛型与单态化
和类型泛型一样:
swift
Buffer<8>
Buffer<16>
在编译阶段都是不同具体实例。
编译器可以针对不同N生成对应代码。
这种机制仍然属于泛型单态化思想。
例如:
rust
fn length<const N: usize>(
value: &[u8; N],
) -> usize {
N
}
分别调用:
ini
length(&[0; 8]);
length(&[0; 16]);
编译器知道两个调用的N分别是多少。
const泛型性能特点
const泛型通常没有额外的运行时类型判断成本。
一些信息在编译阶段已经确定,例如:
数组长度
矩阵尺寸
Buffer容量
编译器可能因此进行更多优化。
但大量不同const实例同样可能增加:
编译时间
二进制代码体积
例如项目同时大量使用:
swift
Buffer<1>
Buffer<2>
Buffer<3>
...
Buffer<1000>
可能产生很多不同单态化实例。
所以const泛型同样不应该无节制使用。
实战:固定长度坐标
例如:
rust
#[derive(Debug)]
struct Vector<
T,
const N: usize,
> {
values: [T; N],
}
可以定义:
ini
type Vector2 = Vector<f64, 2>;
type Vector3 = Vector<f64, 3>;
使用:
ini
let point = Vector2 {
values: [10.0, 20.0],
};
let position = Vector3 {
values: [10.0, 20.0, 30.0],
};
这种类型在游戏、图形和数学计算中很实用。
实战:固定长度ID
某些系统的ID必须固定长度:
rust
#[derive(Debug)]
struct Id<const N: usize> {
bytes: [u8; N],
}
例如:
ini
type Id16 = Id<16>;
type Id32 = Id<32>;
这样长度错误的数据甚至无法构造成对应类型:
swift
Id<16>
必须包含16个字节
把约束放到类型系统中,可以减少运行时检查。
const泛型与类型别名
为了避免到处写:
rust
Matrix<f64, 4, 4>
可以定义:
ini
type Matrix4 = Matrix<f64, 4, 4>;
同样:
ini
type Buffer64 = Buffer<64>;
对于项目中经常使用的尺寸,这种方式可以提升可读性。
const泛型与Trait
Trait也可以包含const泛型参数。
例如:
rust
trait SerializeFixed<
const N: usize,
> {
fn serialize(
&self,
) -> [u8; N];
}
某个类型:
rust
struct Packet;
impl SerializeFixed<4> for Packet {
fn serialize(
&self,
) -> [u8; 4] {
[1, 2, 3, 4]
}
}
这里Trait能力本身和固定长度关联起来。
const泛型与关联常量区别
Trait中还可能看到:
rust
trait Config {
const SIZE: usize;
}
这叫:
vbnet
Associated Const
关联常量
它和const泛型不是一回事。
const泛型:
rust
struct Buffer<const N: usize>
表示:
N是类型参数的一部分。
关联常量:
rust
trait Config {
const SIZE: usize;
}
表示:
实现这个Trait的类型提供一个固定常量值。
两者都涉及编译期常量,但用途不同。
常见错误
认为不同长度数组是同一类型
错误思路:
rust
[i32;3]
和
[i32;4]
都是i32数组,所以一样
实际上它们是不同类型。
const泛型正是为了解决这种长度变化问题。
忘记在impl重新声明const参数
错误:
rust
impl Buffer<N> {
}
需要:
rust
impl<const N: usize> Buffer<N> {
}
把N当运行时可变变量
错误:
ini
N += 1;
const泛型参数不能修改。
能用Vec却强行const泛型
如果数据长度真正需要运行时动态变化:
r
Vec<T>
通常更加合适。
为业务数据引入大量不同N
如果容量不是类型语义的一部分,而只是配置参数,运行时字段可能更加灵活。
假设任何const表达式都能直接作为类型参数运算
基础const泛型比较稳定,但复杂泛型常量表达式需要注意Rust版本和语言支持情况。
const泛型最佳实践
固定长度真正有意义时使用
例如:
矩阵尺寸
数组长度
Buffer容量
协议块大小
非常适合const泛型。
动态长度优先Vec
用户上传文件、数据库结果、商品列表等数量运行时变化的数据,不应该为了泛型而强行固定长度。
类型和常量职责分开
例如:
javascript
Array<T, const N: usize>
让:
r
T负责元素类型
N负责元素数量
非常清晰。
常用尺寸使用类型别名
例如:
ini
type Matrix3 =
Matrix<f64, 3, 3>;
避免业务代码过长。
不要过早使用复杂const表达式
先掌握:
csharp
[T; N]
以及:
rust
struct Type<const N: usize>
已经能解决大量实际问题。
把编译期约束放进类型系统
如果长度错误本身就属于非法状态,const泛型可以帮助编译器提前发现问题。
本章小结
const泛型让Rust泛型系统不仅可以抽象"类型",还可以抽象"编译期常量"。
本文学习了:
-
使用
const N: usize声明const泛型参数 -
数组长度本身属于类型的一部分
-
[T;N]可以配合const泛型处理任意固定长度数组 -
函数、Struct和impl都可以使用const泛型
-
类型泛型和const泛型可以组合
-
生命周期、类型、const可以同时存在
-
一个类型可以拥有多个const参数
-
Matrix可以通过行列数实现编译期尺寸建模
-
FixedBuffer可以通过N定义固定容量
-
const泛型与运行时参数作用不同
-
const泛型同样会发生单态化
-
const泛型适合固定长度和编译期尺寸约束
-
动态长度场景通常应该使用Vec
可以记住:
普通泛型让"类型"成为参数,const泛型让"编译期常量"成为参数。
T回答"里面是什么类型",N回答"固定有多少个"。当长度、容量或尺寸本身就是类型语义的一部分时,const泛型可以让很多错误直接在编译阶段被发现。
下一篇预告
下一篇我们将进入Rust函数式编程中非常重要的一部分:
Rust 闭包 Closure详解:把行为像数据一样传递
内容包括:
-
什么是Closure
-
闭包基本语法
-
闭包与普通函数区别
-
参数类型自动推断
-
返回值自动推断
-
捕获外部变量
-
不可变借用捕获
-
可变借用捕获
-
Move捕获
-
move关键字 -
Fn、FnMut、FnOnce
-
闭包作为函数参数
-
闭包作为返回值
-
Iterator中的闭包
-
sort_by与闭包
-
Web回调、任务处理等实战案例