#8 Black Hat Rust. Несколько вещей, которым я научился на этом пути.
Здравствуйте, дорогие друзья.
Если бы мне пришлось обобщить свой опыт работы с Rust в одном предложении, это было бы: производительность языка высокого уровня при скорости языка низкого уровня. Вот несколько советов, усвоенных на собственном горьком опыте, которыми я делюсь с Вами, чтобы сделать Ваше путешествие по Rust максимально приятным.
Изучение Rust иногда может быть крайне неприятным: предстоит изучить множество новых концепций, а компилятор безжалостен. Но это для Вашего же блага.
Мне потребовался почти 1 год полноценного программирования в Rust, чтобы стать опытным, и больше не нужно читать документацию через каждые 5 строк кода. Это долгое путешествие, но оно того стоит.
Старайтесь избегать аннотаций
Время жизни, безусловно, одна из самых страшных вещей для новичков, приходящих в Rust. Подобно async, это вирусные и цветные функции и структура, которые не только затрудняют чтение Вашего кода, но и затрудняют его использование.
1 2 3 4 5 6 |
// Haha is a struct to wrap a monad generator to provide a facade for any kind of generic iterator. struct Haha<'y, 'o, L, O> where for<'oO> L: FnOnce(&'oO O) -> &'o O, O: Trait<L, 'o, L>, O::Item : Clone + Debug + 'static { x: L, } |
Да, конечно, пожалуйста, не возражайте, что кому-нибудь когда-нибудь придется прочитать и понять Ваш код. Но аннотаций на время жизни можно избежать, и, на мой взгляд, их следует избегать. Итак, вот моя стратегия, позволяющая избежать превращения кода Rust в какое-то чудовище, к которому никто никогда не захочет прикасаться и которое медленно умрет от пренебрежения.
Зачем вообще нужны lifetime аннотации?
Lifetime аннотации нужны для того, чтобы сообщить компилятору, что мы манипулируем какой-то долгоживущей ссылкой, и позволить ему утверждать, что мы не собираемся накручивать себя.
Lifetime Elision
Самый простой и элементарный трюк заключается в том, чтобы опустить аннотацию о времени жизни.
1 2 3 |
fn do_something(x: &u64) { println!("{}", x); } |
В большинстве случаев легко исключить lifetime входных данных, но имейте в виду, что для того, чтобы опустить аннотации о lifetime выходных данных, Вы должны следовать этим 3 правилам:
• Каждое пропущенное lifetime в аргументах функции становится отдельным параметром lifetime.
• Если существует ровно одно входное lifetime, пропущенное или нет, это lifetime присваивается всем пропущенным временам жизни в возвращаемых значениях этой функции.
• Если существует несколько входных значений lifetime, но одним из них является &self или &mut self, lifetime self присваивается всем исключенным выходным значениям lifetime. В противном случае исключение выходного значения является ошибкой.
1 2 3 4 5 6 7 8 9 |
fn do_something(x: &u64)-> &u64 { println!("{}", x); x } // is equivalent to fn do_something_else<'a>(x: &'a u64)-> &'a u64 { println!("{}", x); x } |
Умные указатели
Теперь не все так просто, как в Hello World, и Вам может понадобиться какая-то долговременная ссылка, которую Вы можете использовать в нескольких местах Вашей кодовой базы (например, подключение к базе данных или HTTP-клиент с внутренним пулом подключений).
Решением для долгоживущих, совместно используемых (или нет), изменяемых (или не очень) ссылок, является использование интеллектуальных указателей. Единственным недостатком является то, что интеллектуальные указатели в Rust немного многословны (но все же намного менее уродливы, чем пожизненные аннотации).
1 2 3 4 5 6 7 8 9 |
use std::rc::Rc; fn main() { let pointer = Rc::new(1); { let second_pointer = pointer.clone(); // or Rc::clone(&pointer) println!("{}", *second_pointer); } println!("{}", *pointer); } |
Rc
Для получения изменяемого общего указателя, Вы можете использовать шаблон внутренней изменяемости:
1 2 3 4 5 6 7 8 9 10 |
use std::cell::{RefCell, RefMut}; use std::rc::Rc; fn main() { let shared_string = Rc::new(RefCell::new("Hello".to_string())); { let mut hello_world: RefMut<String> = shared_string.borrow_mut(); hello_world.push_str(" World"); } println!("{}", shared_string.take()); } |
Arc
К сожалению, Rc<RefCell<T>> не может использоваться в потоках или в асинхронном контексте. Здесь в игру вступает Arc, который реализует отправку и синхронизацию и, таким образом, безопасен для совместного использования в потоках.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
use std::sync::{Arc, Mutex}; use std::{thread, time}; fn main() { let pointer = Arc::new(5); let second_pointer = pointer.clone(); // or Arc::clone(&pointer) thread::spawn(move || { println!("{}", *second_pointer); // 5 }); thread::sleep(time::Duration::from_secs(1)); println!("{}", *pointer); // 5 } For mutable shared variables, you can use Arc<Mutex<T>> : use std::sync::{Arc, Mutex}; use std::{thread, time}; fn main() { let pointer = Arc::new(Mutex::new(5)); let second_pointer = pointer.clone(); // or Arc::clone(&pointer) thread::spawn(move || { let mut mutable_pointer = second_pointer.lock().unwrap(); *mutable_pointer = 1; }); thread::sleep(time::Duration::from_secs(1)); let one = pointer.lock().unwrap(); println!("{}", one); // 1 } |
Интеллектуальные указатели особенно полезны при встраивании в структуры:
1 2 3 4 5 6 |
struct MyService { db: Arc<DB>, mailer: Arc<dyn drivers::Mailer>, storage: Arc<dyn drivers::Storage>, other_service: Arc<other::Service>, } |
Когда использовать аннотации lifetimes
На мой взгляд, аннотации lifetimes никогда не должны появляться ни в одном общедоступном API. Их можно использовать, если Вам нужна абсолютная производительность, И минимальное использование ресурсов, И Вы занимаетесь встроенной разработкой, но Вы должны скрывать их в своем коде, и они никогда не должны появляться в общедоступном API.
На этом все. Всем хорошего дня!