Перейти к содержимому

Продвинутые обобщения в Rust: const generics и PhantomData

На странице Обобщения обобщённый параметр всегда был типом: Pair<T> мог хранить i32 или &str. На этой странице — три инструмента, которые развивают ту же идею дальше:

  • Константные обобщения (const generics) позволяют сделать обобщённым параметром число, как 3 в [i32; 3].
  • const fn позволяет компилятору выполнить твою функцию прямо во время сборки программы, так что ответ готов ещё до её запуска.
  • PhantomData позволяет типу нести метку (например, «это ID пользователя» или «эта длина в футах»), не храня никаких лишних данных.

Представь коробку для яиц. Коробка на 6 яиц и коробка на 12 — разные товары, хотя обе — «коробки для яиц». Размер — часть того, чем они являются. Константные обобщения позволяют сказать это на Rust. Не торопись: каждый раздел самостоятельный, и к последним можно вернуться, когда они понадобятся.

Константные обобщения: число как обобщённый параметр

Заголовок раздела «Константные обобщения: число как обобщённый параметр»

В Rust длина массива — часть его типа. [u32; 3] и [u32; 4] различаются так же, как i32 и String. Функция, которая принимает [u32; 3], не примет четыре элемента:

error[E0308]: mismatched types
--> src/main.rs:7:26
|
7 | println!("{}", total(basket));
| ----- ^^^^^^ expected an array with a size of 3, found one with a size of 4
| |
| arguments to this function are incorrect

Компилятор ожидал массив из 3 элементов, а получил массив из 4. Это исправляет константное обобщение. Ты пишешь const N: usize в угловых скобках, и функция работает с массивом любой длины:

fn summary<const N: usize>(prices: [u32; N]) -> String {
let mut sum = 0;
for price in prices {
sum += price;
}
// Внутри функции N — просто обычное число
format!("{N} шт. на сумму {sum}")
}
fn main() {
let small_basket = [4, 10, 2];
let big_basket = [3, 8, 1, 12, 6];
println!("{}", summary(small_basket));
println!("{}", summary(big_basket));
}

Вывод:

3 шт. на сумму 16
5 шт. на сумму 30
▶ Попробуй сам

<const N: usize> читается как «для любого числа N типа usize». Rust выясняет N по массиву, который ты передаёшь, точно так же, как выясняет T для обычных обобщений. И, как с обычными обобщениями, компилятор делает отдельную копию summary для каждой длины, которую ты используешь, так что во время работы это ничего не стоит.

Если честно, для такой маленькой функции срез (prices: &[u32]) подошёл бы не хуже и был бы проще. См. Срезы. Константные обобщения по-настоящему окупаются, когда длина должна быть частью типа: храниться в структуре без выделения памяти в куче или использоваться в возвращаемом типе. Об этом дальше.

Структуры с константными обобщениями: сетка мест

Заголовок раздела «Структуры с константными обобщениями: сетка мест»

Представь схему зала в кинотеатре: несколько рядов, в каждом одинаковое число мест. Вот матрица, у которой и число строк R, и число столбцов C — часть её типа:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
impl<const R: usize, const C: usize> Matrix<R, C> {
fn new(cells: [[i32; C]; R]) -> Self {
Matrix { cells }
}
fn size(&self) -> String {
format!("{R}x{C}")
}
// Строки становятся столбцами: матрица R x C превращается в матрицу C x R
fn transpose(&self) -> Matrix<C, R> {
let mut cells = [[0; R]; C];
for row in 0..R {
for col in 0..C {
cells[col][row] = self.cells[row][col];
}
}
Matrix { cells }
}
}
fn main() {
let seats = Matrix::new([[1, 2, 3], [4, 5, 6]]);
let flipped = seats.transpose();
println!("{} {:?}", seats.size(), seats.cells);
println!("{} {:?}", flipped.size(), flipped.cells);
}

Вывод:

2x3 [[1, 2, 3], [4, 5, 6]]
3x2 [[1, 4], [2, 5], [3, 6]]
▶ Попробуй сам

Посмотри на возвращаемый тип transpose: Matrix<C, R>. Компилятор знает, что перевёрнутая матрица 2x3 становится матрицей 3x2. Если бы внутри была ошибка — скажем, ты собрал бы матрицу не той формы, — код бы не скомпилировался. Вся сетка живёт внутри структуры, без Vec и без кучи. Строка impl<const R: usize, const C: usize> работает как impl<T>: она говорит «эти методы есть для любого размера».

Практический пример: кольцевой буфер фиксированного размера

Заголовок раздела «Практический пример: кольцевой буфер фиксированного размера»

Видеорегистратор в машине хранит только последние несколько минут видео. Когда память заполнена, новая запись затирает самую старую. Это кольцевой буфер (ring buffer), и он идеально подходит для константных обобщений: ёмкость фиксирована, поэтому элементы можно хранить в массиве.

Этот буфер обобщён и по типу T, и по размеру N, поэтому строка impl выглядит как impl<T, const N: usize>:

struct RingBuffer<T, const N: usize> {
items: [T; N],
next: usize, // куда пойдёт следующий push
len: usize, // сколько элементов хранится (никогда не больше N)
}
impl<T: Copy + Default, const N: usize> RingBuffer<T, N> {
fn new() -> Self {
RingBuffer { items: [T::default(); N], next: 0, len: 0 }
}
fn push(&mut self, item: T) {
self.items[self.next] = item; // когда буфер полон, затирает самый старый элемент
self.next = (self.next + 1) % N;
if self.len < N {
self.len += 1;
}
}
// Хранимые элементы, от самого старого
fn to_vec(&self) -> Vec<T> {
let start = (self.next + N - self.len) % N;
(0..self.len).map(|i| self.items[(start + i) % N]).collect()
}
}
fn main() {
// Храним только 3 последних показания температуры
let mut recent: RingBuffer<i32, 3> = RingBuffer::new();
for temp in [18, 19, 21, 24, 22] {
recent.push(temp);
println!("после {temp}: {:?}", recent.to_vec());
}
}

Вывод:

после 18: [18]
после 19: [18, 19]
после 21: [18, 19, 21]
после 24: [19, 21, 24]
после 22: [21, 24, 22]
▶ Попробуй сам

[T::default(); N] заполняет массив N копиями значения по умолчанию (0 для i32). Чтобы так повторить значение, нужен T: Copy, поэтому impl требует Copy + Default. RingBuffer<i32, 3> и RingBuffer<i32, 60> — разные типы, так что буфер на 3 места никогда не попадёт туда, где ждут буфер на 60. Если тебе нужна растущая версия из стандартной библиотеки, см. VecDeque и BinaryHeap.

Значения по умолчанию, фигурные скобки и сегодняшние ограничения

Заголовок раздела «Значения по умолчанию, фигурные скобки и сегодняшние ограничения»

У константного параметра может быть значение по умолчанию: const N: usize = 4. Ещё можно передать именованную константу или выражение в фигурных скобках:

struct Buffer<const N: usize = 4> {
data: [u8; N],
}
impl<const N: usize> Buffer<N> {
fn new() -> Self {
Buffer { data: [0; N] }
}
}
const PAGE: usize = 8;
fn main() {
let small: Buffer = Buffer::new(); // без числа: по умолчанию 4
let page = Buffer::<PAGE>::new(); // именованная константа
let double = Buffer::<{ PAGE * 2 }>::new(); // выражению нужны { }
println!("{} {} {}", small.data.len(), page.data.len(), double.data.len());
}

Вывод:

4 8 16
▶ Попробуй сам

Значение по умолчанию срабатывает, только когда ты пишешь тип без числа, как в let small: Buffer. Просто let small = Buffer::new(); не скомпилируется: error[E0284]: type annotations needed for Buffer<_> — нужна аннотация типа, потому что Rust не выводит значение по умолчанию из выражения.

Константные обобщения в стабильном Rust полезны, но намеренно ограничены. Лучше знать границы заранее, чем наткнуться на них:

  • Допустимые типы: только целые числа (usize, u8, i32, …), bool и char. Попытка написать const NAME: &'static str даст ошибку «&'static str is forbidden as the type of a const generic parameter».
  • Нельзя считать с обобщённым параметром в типе. [f32; N * 2] внутри обобщённой структуры или сигнатуры — ошибка («generic parameters may not be used in const operations»). Просто N можно, и N * 2 в теле функции тоже можно — там N обычное значение. Возможность, которая разрешила бы N * 2 в типах (generic_const_exprs), пока есть только в nightly.
  • Проверка значения. Ограничение вроде «N должно быть больше 0» не написать. Ближе всего к этому — встроенный блок const в функции: const { assert!(N > 0, "need at least one slot") };. Он срабатывает, когда ты собираешь программу с плохим N (cargo check может его не заметить).

Некоторые магазины печатают прайс-лист один раз, а не высчитывают каждую цену на кассе. const fn — это функция, которую компилятору разрешено выполнить во время сборки программы, так что ответ «напечатан» в программе заранее.

С элементами const ты уже встречался на странице Переменные. Их значение должно быть известно во время компиляции, поэтому обычно там можно использовать только литералы и простую арифметику. С const fn там можно вызывать и свои функции:

const fn square(n: usize) -> usize {
n * n
}
const BOARD_SIDE: usize = 4;
const CELLS: usize = square(BOARD_SIDE); // считает компилятор
fn main() {
// Длина массива должна быть известна при компиляции, и CELLS известна
let board = [0u8; CELLS];
println!("на доске {BOARD_SIDE}x{BOARD_SIDE} клеток: {}", board.len());
println!("square(12) во время работы: {}", square(12));
}

Вывод:

на доске 4x4 клеток: 16
square(12) во время работы: 144
▶ Попробуй сам

const fn остаётся обычной функцией. square(12) в main — обычный вызов во время работы. Выполняться при компиляции она обязана только в константном контексте: в элементе const или static, в длине массива, в константном обобщённом аргументе или в блоке const { ... }.

Классическое применение — таблица поиска (lookup table): посчитать список значений один раз при компиляции, а во время работы просто читать его. Здесь компилятор строит таблицу «с какого дня года начинается каждый месяц» для невисокосного года:

const DAYS_IN_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const fn days_before_each_month() -> [u32; 12] {
let mut table = [0; 12];
let mut month = 1;
while month < 12 {
table[month] = table[month - 1] + DAYS_IN_MONTH[month - 1];
month += 1;
}
table
}
// Строится компилятором один раз. Во время работы это просто 12 чисел.
const DAYS_BEFORE: [u32; 12] = days_before_each_month();
fn day_of_year(month: usize, day: u32) -> u32 {
DAYS_BEFORE[month - 1] + day
}
fn main() {
println!("{:?}", DAYS_BEFORE);
println!("1 марта — это день {}", day_of_year(3, 1));
println!("25 декабря — это день {}", day_of_year(12, 25));
}

Вывод:

[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
1 марта — это день 60
25 декабря — это день 359
▶ Попробуй сам

Обрати внимание на цикл while вместо for. Это не вопрос стиля. Вот что можно использовать внутри const fn сегодня:

  • let и let mut, арифметику, сравнения, if, match, loop и while
  • массивы, кортежи, структуры и перечисления, а также индексацию вроде table[month]
  • вызовы других const fn (многие стандартные функции константные, например u32::pow или slice.len())
  • ссылки &mut и вычисления с плавающей точкой (стабильны с Rust 1.83 и 1.82)
  • panic! и assert!, которые при срабатывании во время компиляции становятся ошибками компиляции

А вот что пока нельзя: циклы for (им нужен трейт Iterator, а методы трейтов в стабильном Rust не константные), вызовы обычных функций вроде String::from или println! и всё, что выделяет память в куче, например добавление в Vec.

const или static? И то и другое — глобальные значения, которые не меняются, но хранятся они по-разному:

const static
Где живёт копируется в каждое место, где используется одно фиксированное место в памяти
Адрес может каждый раз быть разным всегда один и тот же, живёт всю программу ('static)
Может быть изменяемым никогда static mut существует, но требует unsafe
Для чего числа, маленькие таблицы, настройки большая таблица, которую нужно хранить в одном экземпляре, или глобальное значение, на которое нужна ссылка

Если сомневаешься, бери const. Если нужно глобальное значение, которое можно менять, вместо static mut используй static с атомарным типом или Mutex внутри. См. Общее состояние.

Вот ошибка, которая постоянно случается в реальных системах. ID пользователей и ID заказов — просто числа, поэтому ничто не мешает передать ID пользователя в cancel_order. Хотелось бы, чтобы тип нёс метку: Id<User> против Id<Order>. Первая попытка может выглядеть так:

struct Id<T> {
value: u32,
}
error[E0392]: type parameter `T` is never used
--> src/main.rs:1:11
|
1 | struct Id<T> {
| ^ unused type parameter
|
= help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`

Rust не принимает параметр типа, который не использует ни одно поле. Исправление — то, что предлагает компилятор. std::marker::PhantomData<T> — это поле, которое говорит «эта структура связана с T», но ничего не хранит. Оно занимает ноль байтов:

use std::marker::PhantomData;
struct User;
struct Order;
struct Id<T> {
value: u32,
_marker: PhantomData<T>, // ноль байтов, просто метка
}
impl<T> Id<T> {
fn new(value: u32) -> Self {
Id { value, _marker: PhantomData }
}
}
fn find_user(id: &Id<User>) -> &'static str {
if id.value == 7 { "Амара" } else { "неизвестно" }
}
fn cancel_order(id: &Id<Order>) {
println!("отменяем заказ #{}", id.value);
}
fn main() {
let user_id: Id<User> = Id::new(7);
let order_id: Id<Order> = Id::new(7);
println!("пользователь #{} — {}", user_id.value, find_user(&user_id));
cancel_order(&order_id);
println!("размер Id в байтах: {}", std::mem::size_of::<Id<User>>());
}

Вывод:

пользователь #7 — Амара
отменяем заказ #7
размер Id в байтах: 4
▶ Попробуй сам

Оба ID хранят число 7, и оба занимают 4 байта — ровно как обычный u32. Но теперь добавь cancel_order(&user_id); последней строкой в main:

error[E0308]: mismatched types
--> src/main.rs:32:18
|
32 | cancel_order(&user_id);
| ------------ ^^^^^^^^ expected `&Id<Order>`, found `&Id<User>`
| |
| arguments to this function are incorrect

Компилятор ожидал &Id<Order>, а получил &Id<User>, — путаница поймана ещё до запуска программы. PhantomData — это и тип, и значение, поэтому при создании структуры ты пишешь _marker: PhantomData. Подчёркивание в начале имени — просто распространённая привычка, означающая «это поле никто не читает».

В 1999 году NASA потеряло аппарат Mars Climate Orbiter, потому что одна программа выдавала числа в имперских единицах, а другая ждала метрические. И то и другое было «просто числами». С PhantomData единица измерения становится частью типа, и один обобщённый Length<U> покрывает любую единицу:

use std::marker::PhantomData;
use std::ops::Add;
struct Meters;
struct Feet;
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
}
// Одна реализация для всех единиц, но обе стороны должны быть в одной единице
impl<U> Add for Length<U> {
type Output = Length<U>;
fn add(self, other: Length<U>) -> Length<U> {
Length::new(self.value + other.value)
}
}
impl Length<Feet> {
fn to_meters(self) -> Length<Meters> {
Length::new(self.value * 0.3048)
}
}
fn main() {
let run: Length<Meters> = Length::new(400.0) + Length::new(250.0);
let climb: Length<Feet> = Length::new(1000.0);
let climb_m = climb.to_meters();
println!("подъём: {:.1} м", climb_m.value);
let total = run + climb_m;
println!("всего: {:.1} м", total.value);
}

Вывод:

подъём: 304.8 м
всего: 954.8 м
▶ Попробуй сам

Напиши run + climb (забыв про to_meters()), и Rust откажет — он ожидал Length<Meters>, а нашёл Length<Feet>:

error[E0308]: mismatched types
--> src/main.rs:39:23
|
39 | let total = run + climb;
| ^^^^^ expected `Length<Meters>`, found `Length<Feet>`

На странице Продвинутые трейты Meters и Centimeters были двумя отдельными структурами, у каждой своя реализация Add. Здесь одна impl<U> Add for Length<U> обслуживает все единицы, которые тебе когда-либо понадобится складывать.

Typestate: дверь, которую нельзя запереть открытой

Заголовок раздела «Typestate: дверь, которую нельзя запереть открытой»

Тем же приёмом можно отслеживать состояние чего-либо (паттерн typestate — «состояние в типе»). Дверь может быть открыта, закрыта или заперта. Закрытую дверь можно запереть, а запирать открытую бессмысленно. Если метка состояния есть в типе, недопустимых действий просто не существует:

use std::marker::PhantomData;
struct Open;
struct Closed;
struct Locked;
struct Door<State> {
room: String,
_state: PhantomData<State>,
}
// Доступно в любом состоянии
impl<State> Door<State> {
fn room(&self) -> &str {
&self.room
}
// Та же дверь, новая метка состояния
fn into_state<Next>(self, action: &str) -> Door<Next> {
println!("{}: {action}", self.room);
Door { room: self.room, _state: PhantomData }
}
}
impl Door<Closed> {
fn new(room: &str) -> Self {
Door { room: room.to_string(), _state: PhantomData }
}
fn open(self) -> Door<Open> {
self.into_state("открыта")
}
fn lock(self) -> Door<Locked> {
self.into_state("заперта")
}
}
impl Door<Open> {
fn close(self) -> Door<Closed> {
self.into_state("закрыта")
}
}
impl Door<Locked> {
fn unlock(self) -> Door<Closed> {
self.into_state("отперта")
}
}
fn main() {
let door = Door::new("серверная").open().close().lock();
println!("{}: заперта на ночь", door.room());
let door = door.unlock().open();
println!("{}: снова открыта", door.room());
}

Вывод:

серверная: открыта
серверная: закрыта
серверная: заперта
серверная: заперта на ночь
серверная: отперта
серверная: открыта
серверная: снова открыта
▶ Попробуй сам

Теперь попробуй запереть открытую дверь, Door::new("серверная").open().lock():

error[E0599]: no method named `lock` found for struct `Door<Open>` in the current scope
--> src/main.rs:50:46
|
50 | let door = Door::new("серверная").open().lock();
| ^^^^ method not found in `Door<Open>`
|
= note: the method was found for `Door<Closed>`

Компилятор не находит метод lock у Door<Open> и подсказывает, что такой метод есть у Door<Closed>. Каждый метод принимает self по значению, поэтому старая дверь «расходуется», и пользоваться «закрытой» дверью после того, как ты её открыл, нельзя. На странице Паттерны проектирования показана та же идея typestate, где каждое состояние ещё и хранит свои данные. PhantomData — облегчённый вариант для случаев, когда состояния — чистые метки.

Методы для одного размера и ассоциированные константы

Заголовок раздела «Методы для одного размера и ассоциированные константы»

На странице Обобщения ты видел impl Pair<f64>: методы только для одного конкретного типа. С константными обобщениями это тоже работает. Некоторые методы имеют смысл только для определённых размеров:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
// Только квадратные матрицы: строк и столбцов одинаково, N
impl<const N: usize> Matrix<N, N> {
fn trace(&self) -> i32 {
let mut sum = 0;
for i in 0..N {
sum += self.cells[i][i];
}
sum
}
}
// Только матрицы 2x2
impl Matrix<2, 2> {
fn determinant(&self) -> i32 {
let [[a, b], [c, d]] = self.cells;
a * d - b * c
}
}
fn main() {
let small = Matrix { cells: [[3, 8], [4, 6]] };
let big = Matrix { cells: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] };
println!("малая: след {}, определитель {}", small.trace(), small.determinant());
println!("большая: след {}", big.trace());
}

Вывод:

малая: след 9, определитель -14
большая: след 15
▶ Попробуй сам

big.determinant() не скомпилируется, как и trace() у матрицы 2x3: ты получишь ошибки вроде error[E0599]: no method named trace found for struct Matrix<2, 3> с примечанием the method was found for Matrix<N, N> — метод найден только для квадратных матриц. Правило «след есть только у квадратных матриц» теперь записано в типах.

Ассоциированная константа (associated const) — это константа, которая принадлежит типу или трейту, как метод. В трейте каждая реализация выбирает своё значение, а обобщённый код может прочитать его как U::NAME. Дадим единицам измерения из примера выше обозначение и размер:

use std::fmt;
use std::marker::PhantomData;
trait Unit {
const SYMBOL: &'static str;
const IN_METERS: f64; // сколько метров в одной такой единице
}
struct Meters;
struct Feet;
struct Miles;
impl Unit for Meters {
const SYMBOL: &'static str = "м";
const IN_METERS: f64 = 1.0;
}
impl Unit for Feet {
const SYMBOL: &'static str = "фт";
const IN_METERS: f64 = 0.3048;
}
impl Unit for Miles {
const SYMBOL: &'static str = "миль";
const IN_METERS: f64 = 1609.344;
}
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U: Unit> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
// Работает между любыми двумя единицами благодаря их константам
fn convert<To: Unit>(&self) -> Length<To> {
Length::new(self.value * U::IN_METERS / To::IN_METERS)
}
}
impl<U: Unit> fmt::Display for Length<U> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.2} {}", self.value, U::SYMBOL)
}
}
fn main() {
let marathon: Length<Miles> = Length::new(26.2);
let everest: Length<Feet> = Length::new(29032.0);
println!("{} = {}", marathon, marathon.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Miles>());
}

Вывод:

26.20 миль = 42164.81 м
29032.00 фт = 8848.95 м
29032.00 фт = 5.50 миль
▶ Попробуй сам

U::IN_METERS находится во время компиляции, поэтому во время работы не нужно ничего искать в таблице. Чтобы добавить новую единицу, скажем Kilometers, хватит одного маленького блока impl Unit, и convert с Display сразу заработают и для неё. Ассоциированные константы бывают и у собственного (inherent) impl, например impl<T, const N: usize> RingBuffer<T, N> { const CAPACITY: usize = N; }, а читаются они как RingBuffer::<i32, 3>::CAPACITY.

Если нужна одна реализация трейта сразу для всех типов, подходящих под условие, — это обобщённая реализация (blanket impl), о ней рассказано на странице Продвинутые трейты.

Эти возможности мощные, и поэтому соблазнительные. Несколько честных советов:

  • Часто хватает среза или Vec. Если функция только читает элементы, &[T] принимает любую длину, и такой код легче читать. Если длина известна только во время работы (файл, ввод пользователя), тебе всё равно нужен Vec.
  • Большие массивы живут на стеке. RingBuffer<u8, 10_000_000> — это десять мегабайт внутри структуры, и стек может переполниться. Положи его в Box (см. Умные указатели) или используй Vec.
  • Каждый новый размер или тип — новая копия кода. Обобщение с 20 разными значениями N соберётся в 20 версий. Это стоит времени компиляции и размера программы. См. Производительность.
  • Typestate добавляет типы и удлиняет сообщения об ошибках. Он отлично подходит для API, которым будут пользоваться многие (строитель, соединение, процесс оплаты). Для состояния, которое меняется по данным во время работы, например статуса, загруженного из базы данных, используй перечисление и match.

Хорошее правило: начинай с простого и бери эти инструменты, когда появляется настоящая ошибка или настоящая цена, от которой они защитят.

Арифметика с константным параметром в типе. Кажется естественным, но стабильный Rust пока этого не позволяет:

struct Pairs<const N: usize> {
values: [f32; N * 2],
}
error: generic parameters may not be used in const operations
--> src/main.rs:2:19
|
2 | values: [f32; N * 2],
| ^ cannot perform const operation using `N`
|
= help: const parameters may only be used as standalone arguments here, i.e. `N`

Компилятор говорит, что обобщённые параметры нельзя использовать в константных операциях: здесь N можно писать только само по себе. Варианты исправления: используй [[f32; 2]; N] (массив пар), добавь второй параметр или возьми Vec, если размер не обязан быть частью типа.

Забытые фигурные скобки вокруг выражения. Buffer::<2 + 2>::new() даёт error: expressions must be enclosed in braces to be used as const generic arguments (выражение в константном аргументе нужно заключить в фигурные скобки), и строка help показывает исправление: Buffer::<{ 2 + 2 }>::new(). Простому числу или имени const скобки не нужны.

Вызов неконстантного кода в константном контексте. const не может вызвать обычную функцию, а const fn не может использовать цикл for:

const GREETING: String = String::from("hello");
error[E0015]: cannot call non-const associated function `<String as From<&str>>::from` in constants
--> src/main.rs:1:26
|
1 | const GREETING: String = String::from("hello");
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: calls in constants are limited to constant functions, tuple structs and tuple variants

Компилятор сообщает, что в константах можно вызывать только константные функции. Для текста используй const GREETING: &str = "hello";. Внутри const fn замени for циклом while с индексом, как в таблице поиска выше.

#[derive(Clone, Copy)] у структуры с PhantomData. Ты ожидаешь, что Id<User> будет Copy, ведь внутри только u32. Но derive добавляет ограничение T: Copy, а User не Copy, поэтому ID перемещается:

error[E0382]: use of moved value: `id`
--> src/main.rs:16:10
|
14 | let id: Id<User> = Id { value: 1, _marker: PhantomData };
| -- move occurs because `id` has type `Id<User>`, which does not implement the `Copy` trait
15 | show(id);
| -- value moved here
16 | show(id);
| ^^ value used here after move
|
note: if all bounds were met, you could clone the value
--> src/main.rs:4:1
|
3 | #[derive(Clone, Copy)]
| ----- derived `Clone` adds implicit bounds on type parameters

Компилятор сообщает об использовании перемещённого значения и подсказывает, что выведенный Clone добавляет неявные ограничения на параметры типа. Самое простое исправление — вывести Clone, Copy и для типов-меток (#[derive(Clone, Copy)] struct User;). Или написать реализации вручную — им ограничение на T не нужно:

impl<T> Clone for Id<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Id<T> {}

Напиши fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N], которая копирует очки в массив ровно из N элементов. Недостающие места заполни 0, а лишние очки отбрось. Вызови её как pad_scores::<5>(&[90, 85]) и как let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);. Подсказка: начни с [0; N], затем пройди циклом по 0..N и используй scores.get(i).

Показать решение
fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N] {
let mut out = [0; N];
for i in 0..N {
if let Some(&score) = scores.get(i) {
out[i] = score;
}
}
out
}
fn main() {
let five = pad_scores::<5>(&[90, 85]);
let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);
println!("{:?}", five);
println!("{:?}", top3);
}

Вывод:

[90, 85, 0, 0, 0]
[70, 95, 88]
▶ Попробуй сам

Здесь N не может взяться из аргумента (у среза в типе нет длины), поэтому он берётся из турбофиша ::<5> или из типа, который ты запрашиваешь, — [u32; 3].

Напиши две const fn: minutes(n: u64) -> u64 и hours(n: u64) -> u64, которые возвращают число секунд. С их помощью объяви const SESSION_TIMEOUT: u64 = hours(1) + minutes(30); и массив const RETRY_DELAYS: [u64; 3] из 1, 5 и 15 минут. Выведи оба. Подсказка: hours может вызывать minutes.

Показать решение
const fn minutes(n: u64) -> u64 {
n * 60
}
const fn hours(n: u64) -> u64 {
minutes(n * 60)
}
const SESSION_TIMEOUT: u64 = hours(1) + minutes(30);
const RETRY_DELAYS: [u64; 3] = [minutes(1), minutes(5), minutes(15)];
fn main() {
println!("тайм-аут сессии: {SESSION_TIMEOUT} секунд");
println!("паузы перед повтором: {:?}", RETRY_DELAYS);
}

Вывод:

тайм-аут сессии: 5400 секунд
паузы перед повтором: [60, 300, 900]
▶ Попробуй сам

Компилятор вычисляет 5400 и таблицу пауз во время сборки, а hours(1) + minutes(30) читается гораздо лучше, чем голое 5400. Если убрать const у minutes, элементы const перестанут компилироваться с ошибкой E0015.

Создай struct Euro; и struct Dollar; и структуру Money<C> с полем cents: i64 и полем PhantomData<C>. Дай ей new(cents) и plus(self, other: Money<C>) -> Money<C>. Сложи обед и кофе в евро и выведи сумму. Подсказка: раз у other тот же C, сложить Money<Euro> и Money<Dollar> не получится.

Показать решение
use std::marker::PhantomData;
struct Euro;
struct Dollar;
struct Money<C> {
cents: i64,
_currency: PhantomData<C>,
}
impl<C> Money<C> {
fn new(cents: i64) -> Self {
Money { cents, _currency: PhantomData }
}
fn plus(self, other: Money<C>) -> Money<C> {
Money::new(self.cents + other.cents)
}
}
fn main() {
let lunch: Money<Euro> = Money::new(1250);
let coffee: Money<Euro> = Money::new(320);
let ticket: Money<Dollar> = Money::new(4999);
let total = lunch.plus(coffee);
println!("итого: {}.{:02} EUR", total.cents / 100, total.cents % 100);
println!("билет: {}.{:02} USD", ticket.cents / 100, ticket.cents % 100);
}

Вывод:

итого: 15.70 EUR
билет: 49.99 USD
▶ Попробуй сам

Попробуй lunch.plus(ticket): получишь ошибку expected Money<Euro>, found Money<Dollar> — ожидались евро, а пришли доллары. А хранение суммы в центах целым числом, а не в f64, ещё и избавляет от ошибок округления с деньгами.

  • Константные обобщения (const N: usize) делают число частью типа. Массивы разной длины — разные типы, а impl<T, const N: usize> пишет методы сразу для любого размера.
  • В стабильном Rust константные параметры могут быть целыми числами, bool или char. Выражениям в аргументах нужны фигурные скобки ({ PAGE * 2 }), а арифметика с обобщённым N внутри типа пока запрещена.
  • const fn может выполняться при компиляции в константных контекстах (const, static, длины массивов). Используй while вместо for и вызывай только другие const fn.
  • PhantomData<T> — поле нулевого размера, которое связывает структуру с типом, который она не хранит. Используй его для типизированных ID, единиц измерения и typestate.
  • impl Matrix<2, 2> и impl<const N: usize> Matrix<N, N> дают методы только некоторым размерам. Ассоциированные константы вроде U::IN_METERS дают каждому типу своё значение, известное при компиляции.
  • Эти инструменты стоят времени компиляции и читаемости. Используй их, когда они защищают от настоящей ошибки.

Проверь себя

1. Почему [u32; 4] нельзя передать в функцию, которая принимает [u32; 3]?

2. Что PhantomData<T> добавляет к структуре во время работы программы?

3. Что из этого НЕЛЬЗЯ использовать внутри const fn в стабильном Rust?

4. Что означает impl<const N: usize> Matrix<N, N>?

Результат: 0 / 4