мономорфизация на этапе компиляцииcompile-time monomorphization
Обобщённые функции и типы (fn largest<T: PartialOrd>, struct Pair<T>) позволяют написать код один раз и использовать его для любого типа, удовлетворяющего требуемым trait bounds. Компилятор разрешает дженерики через мономорфизацию: для каждого конкретного типа генерируется отдельная, полностью конкретная скомпилированная версия — никакого рантайм-параметра типа, никакого boxing, никакой vtable, если явно не выбран dyn Trait.Generic functions and types (fn largest<T: PartialOrd>, struct Pair<T>) let you write code once and use it for any type satisfying the required trait bounds. The compiler resolves generics via monomorphization: for every distinct concrete type, a separate, fully concrete compiled version is generated — no runtime type parameter, no boxing, no vtable, unless dyn Trait is separately opted into.
Дженерик — параметр типа в сигнатуре функции или определении типа, ограниченный (опционально) trait bounds: fn largest<T: PartialOrd>(list: &[T]) -> &T работает для любого T, реализующего PartialOrd. Структуры тоже обобщаются: struct Pair<T> { x: T, y: T }. Компилятор не хранит "параметр типа" во время выполнения — вместо этого на этапе компиляции для каждого фактически использованного T генерируется отдельная конкретная версия кода.A generic is a type parameter in a function signature or type definition, optionally bounded by trait bounds: fn largest<T: PartialOrd>(list: &[T]) -> &T works for any T implementing PartialOrd. Structs are generic too: struct Pair<T> { x: T, y: T }. The compiler doesn't keep a "type parameter" around at runtime — instead, at compile time, a separate concrete version of the code is generated for each T actually used.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { largest = item; }
}
largest
}
// largest::<i32> и largest::<f64> — две независимые скомпилированные функции,// largest::<i32> and largest::<f64> — two independent compiled functions,
// без общего рантайм-пути и без единого параметра типа во время выполнения// with no shared runtime path and no type parameter at runtime at all
fn largest_i32(list: &[i32]) -> &i32 { /* ... *//* ... */ }
fn largest_f64(list: &[f64]) -> &f64 { /* ... *//* ... */ }
Мономорфизация архитектурно идентична шаблонам C++ — та же философия "нулевой цены абстракции", но с важным улучшением в проверке ограничений:Monomorphization is architecturally identical to C++ templates — the same "zero-cost abstraction" philosophy, but with an important improvement in bound checking:
Пишете обобщённый код один раз, платите ровно ноль рантайм-накладных расходов по сравнению с вручную написанной версией для каждого типа — цена переносится на этап компиляции и на размер бинарника (по копии на инстанциацию). Ближайший современный C++-аналог — концепты (см. concepts-language.html).You write generic code once and pay exactly zero runtime overhead compared to a hand-written version per type — the cost shifts to compile time and binary size (one copy per instantiation). The closest modern C++ analogue is concepts (see concepts-language.html).
fn largest<T: PartialOrd>(...) проверяется один раз, для общего случая — если тело использует операцию, которую T по объявленным границам не поддерживает, ошибка возникает сразу и ясно, а не только позже, в конкретной точке вызова с неограниченным параметром шаблона (классическая проблема нечитаемых сообщений об ошибках дошаблонных концептов в C++).fn largest<T: PartialOrd>(...) is checked once, for the general case — if the body uses an operation T isn't bounded to support, the error surfaces immediately and clearly, rather than only later, confusingly, at a specific call site with an unconstrained template parameter (the classic pre-concepts C++ template error-message problem).
Плата за нулевую рантайм-цену — время компиляции и размер бинарника: каждая новая инстанциация — новая копия кода. Этот компромисс явный и осознанный, а не скрытый побочный эффект.The price for zero runtime cost is compile time and binary size: every new instantiation is a new copy of the code. This tradeoff is explicit and deliberate, not a hidden side effect.
Обобщённый метод может быть доступен только при дополнительных ограничениях сверх собственных границ структуры — это распространённый идиоматичный для Rust паттерн: структура обобщена по T без ограничений, но конкретный метод существует только когда T реализует нужные трейты.A generic method can be made available only under additional bounds beyond the struct's own — this is a common Rust-idiomatic pattern: the struct is generic over T with no bounds, but a specific method exists only when T implements the needed traits.
struct Pair<T> { x: T, y: T }
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self { Self { x, y } } // доступно для любого T// available for any T
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) { // доступен только когда T: Display + PartialOrd// only exists when T: Display + PartialOrd
if self.x >= self.y {
println!("largest is x = {}", self.x);
} else {
println!("largest is y = {}", self.y);
}
}
}
На этапе кодогенерации largest::<i32> и largest::<f64> становятся двумя независимыми скомпилированными функциями, не разделяющими ни единого рантайм-пути выполнения — компилятор буквально копирует и специализирует тело функции для каждого конкретного T.At codegen, largest::<i32> and largest::<f64> become two independent compiled functions sharing no runtime execution path whatsoever — the compiler literally copies and specializes the function body for each concrete T.
Одна функция работает и для Vec<i32>, и для Vec<char> — компилятор генерирует по отдельной специализированной версии на каждый вызываемый тип.One function works for both Vec<i32> and Vec<char> — the compiler generates a separate specialized version per type actually called.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { largest = item; }
}
largest
}
let numbers = vec![34, 50, 25, 100, 65];
println!("{}", largest(&numbers)); // largest::<i32>// largest::<i32>
let chars = vec!['y', 'm', 'a', 'q'];
println!("{}", largest(&chars)); // largest::<char> — отдельная копия// largest::<char> — a separate copy
Обобщённый тип не обязан предоставлять все методы для любого T — cmp_display существует только когда T реализует и Display, и PartialOrd.A generic type doesn't have to expose every method for every possible T — cmp_display only exists when T implements both Display and PartialOrd.
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("largest is x = {}", self.x);
} else {
println!("largest is y = {}", self.y);
}
}
}
// Pair<SomeTypeWithoutDisplay> компилируется, но не имеет cmp_display вовсе// Pair<SomeTypeWithoutDisplay> compiles, but has no cmp_display at all
Когда границ становится много, where-клауза читается заметно яснее, чем нагромождение ограничений прямо в списке параметров типа.Once bounds pile up, a where clause reads noticeably clearer than stacking constraints directly in the type-parameter list.
fn process<T, U>(t: T, u: U) -> String
where
T: Display,
U: Clone + Debug,
{
format!("{} / {:?}", t, u.clone())
}
Большая обобщённая функция, вызванная с 10 разными конкретными типами, — 10 скомпилированных копий. Та же логика за dyn Trait (см. trait-objects.html) — ровно одна скомпилированная копия ценой диспетчеризации во время выполнения; в крупной кодовой базе такой переход измеримо уменьшает бинарник.A large generic function called with 10 different concrete types produces 10 compiled copies. The same logic behind dyn Trait (see trait-objects.html) produces exactly one compiled copy, at the cost of runtime dispatch — in a large codebase, switching measurably shrinks the binary.
// render::<Circle>, render::<Square>, ... render::<Triangle> — 10 отдельных копий// render::<Circle>, render::<Square>, ... render::<Triangle> — 10 separate copies
fn render<T: Shape>(shape: &T) { /* большая функция *//* a large function */ }
// render_dyn — одна копия, вызывается со всеми 10 типами через один и тот же адрес// render_dyn — one copy, called with all 10 types through the same address
fn render_dyn(shape: &dyn Shape) { /* та же большая функция *//* the same large function */ }
Крупная обобщённая функция, используемая с множеством конкретных типов, ощутимо увеличивает размер бинарника и время сборки ("code bloat") — та же проблема, что хорошо известна в шаблоно-тяжёлых кодовых базах C++. Именно поэтому стандартная библиотека и некоторые крейты выносят необобщённую "trampoline"-функцию, к которой делегирует тонкая обобщённая обёртка, — чтобы мономорфизировался только тонкий слой, а основная логика компилировалась один раз.A large generic function used with many concrete types meaningfully increases binary size and compile time ("code bloat") — the same well-known tradeoff in template-heavy C++ codebases. This is precisely why the standard library and some crates provide a non-generic "trampoline" inner function that a thin generic public wrapper delegates to, keeping only the thin layer monomorphized while the bulk of the logic compiles once.
fn f<T>(x: T) без trait bounds не может сравнить, вывести на печать или произвести арифметику над x — у компилятора нет никаких гарантированных возможностей для полностью неограниченного T. Это реальный источник раннего замешательства у тех, кто ждёт от дженериков поведения duck typing из динамических языков.fn f<T>(x: T) with no trait bounds cannot compare, print, or do arithmetic on x — the compiler has zero guaranteed capabilities for a fully unconstrained T. This is a real point of early confusion for people expecting generics to behave like duck-typed generics in dynamically-typed languages.
Типовые параметры дженериков стираются из поверхности API с точки зрения рантайм-интроспекции — узнать "какой конкретно тип здесь T" во время выполнения из самого обобщённого кода нельзя, встроенной рефлексии нет. Ближайшая аналогия — стирание типов (type erasure) в Java, хоть и по другим внутренним причинам.Generic type parameters are erased from the API surface in terms of runtime introspection — there's no way to ask "what concrete type is T here" at runtime from the generic code itself, no built-in reflection. The closest analogy is Java's type erasure, though for different underlying reasons.