@mkpankov, считаю правильным не согласиться с последним утверждением.
Очень многое Rust позволяет сделать безопасно, если это делать не совсем так, как это делается в других языках. Здесь правильный подход делать это безопасно и этому мы учим тех, кто знакомится с Rust. Жить не по принципу C(++) “нет, компилятор, брешешь, unsafe
даю, безопасно”, а писать безопасный код, даже если он чуть длиннее. С этим, я надеюсь, согласны все.
Иногда встречаются задачи, которые, очевидно, можно выполнить безопасно, но компилятор не даёт этого сделать, потому что он чего-то не знает. Наглядный пример мы видим перед собой: компилятор не знает, что все вызовы next()
с гарантией вернут непересекающиеся ссылки. В этот момент возникает сильный соблазн найти пример, как такое где-нибудь обошли, скопировать его вместе с unsafe
, не задумываясь особо, и объявить задачу выполненной. И именно в этот момент игнорируется другое радикальное преимущество Rust - возможность построения безопасных абстракций. Если это уже сделали, то это можно скопировать… стоп, а нельзя этим воспользоваться?
Вернёмся к нашему примеру. Проблема в том, что компилятор не в курсе того, что все ссылки берутся на разные элементы. Эта задача уже решена в стандартной библиотеке для случая [T]
, так зачем писать свой unsafe
, когда он уже написан? Давайте попробуем просто им воспользоваться. Итак, у нас было (и не работало) следующее
pub struct UnitIterMut<'a, P, V> {
unit: &'a mut Unit<P, V>,
index: usize,
}
impl<'a, V: 'a> Iterator for UnitIterMut<'a, Joint, V> {
type Item = (Joint, &'a mut V);
fn next(&mut self) -> Option<Self::Item> {
self.index += 1;
match self.index {
1 => Some((Joint::J0, &mut self.unit.0[0])),
2 => Some((Joint::J1, &mut self.unit.0[1])),
3 => Some((Joint::J2, &mut self.unit.0[2])),
4 => Some((Joint::J3, &mut self.unit.0[3])),
5 => Some((Joint::J4, &mut self.unit.0[4])),
6 => Some((Joint::J5, &mut self.unit.0[5])),
7 => Some((Joint::J6, &mut self.unit.0[6])),
8 => Some((Joint::J7, &mut self.unit.0[7])),
_ => None,
}
}
}
А стало после переписывания (полная версия на playground)
pub struct UnitIterMut<'a, V> {
iter: iter::Enumerate<slice::IterMut<'a, V>>,
}
impl<'a, V: 'a> Iterator for UnitIterMut<'a, V> {
type Item = (Joint, &'a mut V);
fn next(&mut self) -> Option<Self::Item> {
let (index, reference) = self.iter.next()?;
let joint = Joint::from_usize(index)?;
Some((joint, reference))
}
}
Здесь метод Joint::from_usize
просто переводит из индекса в константу (и вообще сгенерирован при помощи #[derive(FromPrimitive)]
из num-derive
). Из кода ушло повторение однотипных строк и он теперь работает без unsafe
- просто потому, что мы используем многократно протестированный кусочек unsafe
-кода, живущий прямо в стандартной библиотеке.
Правило “не пиши unsafe
нигде, кроме FFI” для новичков возникает не только потому, что они плохо знают, что именно делать можно, а что нельзя ни в коем случае. Другая немаловажная причина заключается в том, что, если посмотреть вокруг внимательно, зачастую можно найти код, который сделает всё небезопасное за тебя. Мы учимся как сообщество не писать свой unsafe, а искать и использовать существующие примитивы. Так меньше кода, который нужно пристально анализировать на возможность падений или чинить в том случае, если он всё-таки оказывается опасным.
Да, есть ещё и третья важная причина. Бывает порой такое, что ты пытаешься применить какую-то похожую библиотечную абстракцию, а она не встаёт. Не сходится что-то. Смотришь внимательнее и понимаешь, что в твоих условиях так сделать нельзя, потому что упустил какое-то важное условие, и в твоих условиях такой подход был бы чреват падением. При копировании unsafe-кода к себе и доработке напильником пропустить такое куда проще, а искать ошибку с ментальным блоком “это скопировано из надёжного места и, стало быть, тоже надёжно” куда как весело…
Потому я за то, чтобы отвечать спрашивающим в духе “не пиши unsafe
нигде, кроме FFI, а лучше и в FFI тоже не пиши, если возможно”. Потому что умение находить и применять подходящие безопасные абстракции тоже очень важно, и чтобы ему учиться, их нужно искать и применять, а не копипастить. Потому что пусть лучше не убивают лайфтайм с помощью transmute
, когда знают, что это доживёт до конца программы, а честно пишут Box::leak
. Потому что Rust - это язык про то, как собирать безопасные и эффективные абстракции и пользоваться ими.