泛型类型(generics)允许编写与具体类型无关的代码。它们通过尖括号语法实现:
fn max<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } }
这里的T是一个受限于trait PartialOrd 的泛型类型。
泛型参数通过 <T> 声明,但可以通过冒号限制它们,例如 <T: Display> 。这是一种向编译器声明,只有实现所需trait的类型才能被使用。
在Rust中,泛型的调度主要有两种形式:
dyn Trait,则通过虚表(vtable)进行调用。对机器代码的影响: 使用带有trait bounds的泛型(不使用dyn Trait)会导致单态化:增加二进制文件的大小,但达到最高速度。使用dyn Trait节省二进制文件的大小,但性能会下降。
问题: 有一个函数
fn do_something<T: Debug>(value: &T)
编译器会为每个使用的类型在二进制代码中创建单独的 do_something 函数还是会使用通用实现?
典型错误答案: 会因trait bound而为所有类型使用一个函数。
正确答案: 编译器为每个类型创建该函数的单独副本(单态化),因为trait bound并不会使泛型函数通过vtable变得“通用”。只有在使用 dyn Trait(动态调度)时,才会实现通用性。
示例:
fn print_val<T: std::fmt::Debug>(val: T) { println!("{:?}", val); } // 每次用不同类型调用时都会生成自己的函数版本
故事
在一个使用大量泛型对象的项目中,发现生成的二进制文件比预期大得多。后来发现原因是在广泛使用没有限制的泛型函数时。加载十几种类型的调用导致可执行文件体积的指数增长(code bloat),这是在CI的发布版本中才发现的。
故事
一位开发人员接收了带有trait bound的泛型参数,以为这样的代码使用的是“动态”调度。这导致服务器的内存使用过高,以及由于代码不断增长和被处理器缓存而降低的性能。
故事
在一个库中,尝试将带有Self类型的泛型trait(例如,trait Clone)用作dyn Trait,这是Rust不支持的,这导致编译错误。需要显式地重写接口,否则泛型API在动态运行时将无法工作,接口必须在编译时更改。