#rust
#Ржавчина
Вопрос:
В настоящее время я изучаю Rust для развлечения. У меня есть некоторый опыт в C / C и другой опыт в других языках программирования, которые используют более сложные парадигмы, такие как дженерики.
Предыстория
Для моего первого проекта (после урока) я хотел создать N-мерную массивную (или матричную) структуру данных, чтобы практиковать разработку в Rust.
Вот что у меня есть на данный момент для моей матричной структуры и базовой заливки и новых инициализаций.
Простите за отсутствие проверки привязки и тестирования параметров
pub struct Matrix<'a, T> {
data: Vec<Option<T>>,
dimensions: amp;'a [usize],
}
impl<'a, T: Clone> Matrix<'a, T> {
pub fn fill(dimensions: amp;'a [usize], fill: T) -> Matrix<'a, T> {
let mut total = if dimensions.len() > 0 { 1 } else { 0 };
for dim in dimensions.iter() {
total *= dim;
}
Matrix {
data: vec![Some(fill); total],
dimensions: dimensions,
}
}
pub fn new(dimensions: amp;'a [usize]) -> Matrix<'a, T> {
...
Matrix {
data: vec![None; total],
dimensions: dimensions,
}
}
}
Я хотел иметь возможность создавать «пустой» N-мерный массив, используя новый fn. Я подумал, что использование опции enum будет лучшим способом для достижения этой цели, поскольку я могу заполнить N-мерное None
значение, и оно автоматически выделит пространство для этого T generic.
Итак, все сводится к возможности установить записи для этого. Я нашел черты IndexMut
и Index
, которые выглядели так, как будто я мог бы сделать что-то вроде m[amp;[2, 3]] = 23
. Поскольку логика похожа друг на друга IndexMut
, вот пример для Matrix
.
impl<'a, T> ops::IndexMut<amp;[usize]> for Matrix<'a, T> {
fn index_mut(amp;mut self, indices: amp;[usize]) -> amp;mut Self::Output {
match self.data[get_matrix_index(self.dimensions, indices)].as_mut() {
Some(x) => x,
None => {
NOT SURE WHAT TO DO HERE.
}
}
}
}
В идеале должно произойти то, что значение (если оно есть) будет изменено, т.е.
let mut mat = Matrix::fill(amp;[4, 4], 0)
mat[amp;[2, 3]] = 23
Это установило бы значение от 0 до 23 (что делает приведенный выше fn с помощью возврата amp;mut x
from Some(x)
). Но я также хочу None
установить значение, т.е.
let mut mat = Matrix::new(amp;[4, 4])
mat[amp;[2, 3]] = 23
Вопрос
Наконец, есть ли способ сделать m[amp;[2,3]] = 23
возможным то, что требуется структуре Vec для выделения памяти? Если нет, то что я должен изменить и как я могу по-прежнему иметь массив с «пустыми» местами. Открыт для любых предложений, поскольку я пытаюсь учиться. 🙂
Заключительные мысли
Благодаря моим исследованиям, структура Vec подразумевает, что я вижу, что тип T
типизирован и должен иметь размер. Это может быть полезно для выделения Vec с соответствующим размером через vec![pointer of T that is null but of size of T; total]
. Но я не уверен, как это сделать.
Комментарии:
1. Что у вас есть для этой
Index
черты? Что вы возвращаете, если значение в этом индексе не существует?2. Я спрашиваю, потому что реализация
IndexMut
по желанию возможна, но посколькуIndexMut
тип вывода должен соответствовать соответствующейIndex
черте, это будет иметь неприятные последствия дляIndex
реализации.3. Для
Index
признака у меня такая же проблема, если значениеOption<T>
перечисления равноNone
. Я не знаю, что вернуть. В настоящее время у меня просто паника, но я не уверен, что делать, что является идиоматичным для Rust и имеет функциональность для возврата «нулевого» указателя или чего-то, что имеет смысл для пользователя.4. В идеале это означало бы, что в таких ситуациях, как
println!("{}", m[amp;[1, 1]]);
wherem[amp;[1, 1]]
is the option enumNone
, он выдает ошибку, потому что он будет выполнять to_string для «нулевого» значения или чего-то подобного. Но для функциональности онIndex
должен возвращать значение, которое находится в этом месте в матрице.
Ответ №1:
Итак, есть несколько способов сделать это более похожим на идиоматический rust, но сначала давайте посмотрим, почему ветка none не имеет смысла.
Итак Output
, тип IndexMut
, который я собираюсь предположить amp;mut T
, таков: вы не показываете определение индекса, но я чувствую себя в безопасности в этом предположении. Тип amp;mut T
означает изменяемую ссылку на инициализированную T
, в отличие от указателей в C / C , где они могут указывать на инициализированную или неинициализированную память. Это означает, что вы должны вернуть инициализированное T
значение, которое ветвь none не может, потому что нет инициализированного значения. Это приводит к первому из наиболее идиоматичных способов.
Возвращает Option<T>
Самым простым способом было бы изменить Index::Output
, чтобы быть Option<T>
. Это лучше, потому что пользователь может решить, что делать, если раньше там не было значения, и оно близко к тому, что вы на самом деле храните. Затем вы также можете удалить панику в своем индексном методе и позволить вызывающей стороне выбирать, что делать, если значение отсутствует. На данный момент, я думаю, вы можете пойти немного дальше с улучшением структуры в следующем варианте.
Хранить T
непосредственно
Этот метод позволяет вызывающей стороне напрямую изменять сохраняемый тип, а не оборачивать его в параметр. Это хорошо очищает большую часть вашего индексного кода, поскольку вам просто нужно получить доступ к тому, что уже сохранено. Основная проблема теперь заключается в инициализации, как вы представляете неинициализированные значения? Вы были правы, что этот вариант — лучший способ сделать это 1, но теперь вызывающий может решить использовать эту необязательную возможность инициализации, сохранив Option
себя. Это означает, что мы всегда можем хранить инициализированные T
s без потери функциональности. Это только действительно меняет вашу новую функцию, чтобы вместо этого не заполнять None
значениями. Мое предложение здесь состоит в том, чтобы сделать привязку T: Default
для новой функции 2:
impl<'a, T: Default> Matrix<'a, T> {
pub fn new(dimensions: amp;'a [usize]) -> Matrix<'a, T> {
Matrix {
data: (0..total).into_iter().map(|_|Default::default()).collect(),
dimensions: dimensions,
}
}
}
Этот метод гораздо более распространен в мире rust и позволяет вызывающей стороне выбирать, разрешать ли неинициализированные значения. Option<T>
также реализует значение по умолчанию для всех T
и возвращает None
, поэтому функциональность очень похожа на то, что у вас есть в настоящее время.
Дополнительная информация
Поскольку вы новичок в rust, я могу сделать несколько замечаний о ловушках, в которые я попадал раньше. Для начала ваша структура содержит ссылку на измерения с жизненным циклом. Это означает, что ваши структуры не могут существовать дольше, чем объект измерения, который их создал. Это не вызвало у вас проблем, поскольку все, что вы передавали, — это статически созданные измерения, измерения, которые вводятся в код и хранятся в статической памяти. Это дает вашему объекту время жизни 'static
, но этого не произойдет, если вы используете динамические измерения.
Как еще вы можете сохранить эти измерения, чтобы у вашего объекта всегда было 'static
время жизни (такое же, как и без времени жизни)? Поскольку вам нужен N-мерный массив, о выделении стека не может быть и речи, поскольку стековые массивы должны быть детерминированными во время компиляции (иначе известный как const
в rust). Это означает, что вы должны использовать кучу. Это оставляет два реальных варианта Box<[usize]>
или Vec<usize>
. Box
это просто еще один способ сказать, что это находится в куче и добавляет Sized
к значениям, которые есть ?Sized
. Vec
немного более понятна и добавляет возможность изменения размера за счет небольших накладных расходов. Любой из них позволил бы вашему матричному объекту всегда иметь 'static
время жизни.
- 1. Другой способ представить это без
Option<T>
дискриминации —MaybeUninit<T>
это небезопасная территория. Это позволяет вам иметь достаточно большой кусок инициализированной памяти, чтобы вместить aT
, а затем предположить, что он инициализирован небезопасно. Это может вызвать много проблем и обычно того не стоит, посколькуOption
оно уже сильно оптимизировано в том смысле, что если он хранит тип с указателем, он использует магию компилятора для сохранения различий в том, является ли это значение нулевым указателем. - 2. Причина, по которой этот раздел не используется
vec![Default::default(); total]
, заключается в том, чтоT: Clone
для работы этого макроса требуется, чтобы первая часть вызывалась один раз и клонировалась до тех пор, пока не будет достаточно значений. Это дополнительное требование, которое нам не нужно, чтобы интерфейс был более плавным без него.
Комментарии:
1. Я ценю все предложения, и да, это ловушка, в которую я попал. Мне нравится идея
Box<[usize]>
, потому что я не хочу, чтобы матрица могла быть изменена после инициализации (возможно, я добавлю функцию для клонирования матрицы с новым размером). Что касается храненияT
в матрице, я разрываюсь междуOption<T>
иDefault::default()
из-за других функций (таких как tostring для распечатки матрицы). По умолчанию это привело бы к путанице между заполненной матрицейMatrix::fill([4,4], 0)
иMatrix::new([4,4])
тем, что они были бы одинаковыми. Так склоняясь кOption<T>