Продвинутые обобщения в 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 шт. на сумму 165 шт. на сумму 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 stris 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 fn: пусть считает компилятор»Некоторые магазины печатают прайс-лист один раз, а не высчитывают каждую цену на кассе. 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 клеток: 16square(12) во время работы: 144const 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 марта — это день 6025 декабря — это день 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 внутри. См. Общее состояние.
PhantomData: метка, которую не перепутать
Заголовок раздела «PhantomData: метка, которую не перепутать»Вот ошибка, которая постоянно случается в реальных системах. 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],}
// Только квадратные матрицы: строк и столбцов одинаково, Nimpl<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 }}
// Только матрицы 2x2impl 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большая: след 15big.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` trait15 | show(id); | -- value moved here16 | 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> {}Практика
Заголовок раздела «Практика»1. Дополняем список очков
Заголовок раздела «1. Дополняем список очков»Напиши 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].
2. Тайм-ауты при компиляции
Заголовок раздела «2. Тайм-ауты при компиляции»Напиши две 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.
3. Деньги, которые нельзя смешать
Заголовок раздела «3. Деньги, которые нельзя смешать»Создай 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
Что почитать
Заголовок раздела «Что почитать»- Const generics — The Rust Reference (на английском): точные правила для константных параметров — какие типы разрешены и когда нужны фигурные скобки.
- Const functions — The Rust Reference (на английском): что такое константные контексты и что может делать
const fn. PhantomData— документация стандартной библиотеки (на английском): официальное объяснение с примерами неиспользуемых параметров типа.- PhantomData-параметры — Rust на примерах (перевод на русский): короткие запускаемые примеры, включая разбор единиц измерения.
- Const generics MVP hits beta — Rust Blog (на английском): анонс, который объясняет, зачем появились константные обобщения и что умела их первая версия.