Структура данных Rust

#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]]); where m[amp;[1, 1]] is the option enum None , он выдает ошибку, потому что он будет выполнять 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> это небезопасная территория. Это позволяет вам иметь достаточно большой кусок инициализированной памяти, чтобы вместить a T , а затем предположить, что он инициализирован небезопасно. Это может вызвать много проблем и обычно того не стоит, поскольку 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>