Комментарии 23
Это позволит имитировать семантику ссылок на объекты в Java.
Имхо, конечно, но это очень плохой совет. Мало того что объекты за Rc/Arc нельзя мутировать без Cell/RefCell, так ещё и попытки натянуть сборку мусора на язык без оной плохо кончаются
Используйте структуры с функциями вместо классов
Весьма возможно , что этот совет хорош не только лишь для раста, но и для джавы.
используйте для конвертации ошибок:
.map_err(|e| e.to_string())?;
можно начать с простого типа Result<_, String>
Кстати - return ; не нужен:
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err("Division by zero".to_string())
} else {
Ok(a / b) // return ; не нужен
}
}
А также, очень часто используемая конструкция:
lazy_static! { pub static ref CACHE: Arc<RwLock<HashMap<...>>>
Убрал из статьи return - я тоже когда на Scala пишу его не использую ну и на Rust тоже. Просто по привычке для Java кодеров его оставил. В целом согласен для красоты надо убрать пусть ощущают новации в синтаксическом сахаре.
lazy_static! { pub static ref CACHE: Arc<RwLock<HashMap<...>>>
На счет этого я добавлю тоже в статью уже завтра.
Но я использую такой вариант:
use once_cell::sync::Lazy;
static HANDLERS: Lazy<Mutex<HashMap<String, i32>>> = Lazy::new(|| { Mutex::new(HashMap::new())});
для обьявления глобально асинхроного синглтона
а потом
let mut handlers = HANDLERS.lock().await;
handlers.insert(disk.to_string(), handler);
С некоторых пор ещё есть https://doc.rust-lang.org/std/sync/struct.OnceLock.html
use std::sync::{Mutex, OnceLock};
use std::collections::HashMap;
pub static CACHE: OnceLock<Mutex<HashMap<String, i32>>> = OnceLock::new();
fn get_cache() -> &'static Mutex<HashMap<String, i32>> {
CACHE.get_or_init(Default::default)
}
fn main() {
let mut guard = get_cache().lock().unwrap();
guard.insert("321".to_string(), 123);
}
Всё же лучше использовать более "всеядный" `Result<T, Box<dyn Error>>` или Error из крейта anyhow. Он больше похож на джавовский RuntimeException.
Строка может быть преобразована в `Box<dyn std::error::Error>` и поэтому можно писать `Err("Division by zero".into())`.
Вместо конструктора реализуют функцию new. Для асинхронного кода вернуть лучше Arc<MyClass>, а для синхронного - Rc<MyClass>
А может все-таки лучше возврашать обычный инстанс структуры, чтобы вызывающий код сам решал, нужен Rc, Arc или вообще ничего.
Да, когда ты опытный Rust-кодер, то это верное решение. А когда тебе неопытному быстро надо написать компилирующийся код, то лучше так. А потом убрать лишние Arc<....>
Когда код рабочий это легко изменить. Исправил в объявлении и реализации new и компилятор подсказал где надо в вызовах new это подправить.
А когда тебе неопытному быстро надо написать компилирующийся код, ...
поэтому и "lazy_static!"
кстати:
let _ = HANDLERS.lock().await.insert(disk.to_string(), handler);
лок освободит в той же строке
Не спорю, но чем цепочка вызовов функций длиннее, тем тяжелей человеку читать код. Хотя она у вас не сильно длинная, но в целом идею вы поняли)
Eсли не использовать async await, то будет .unwrap(), и все получится длиннее, и не так красиво. Кстати .unwrap() лучше избегать с самого начала.
let mut handlers = HANDLERS.lock().await; // лок на создается для переменной handlers
в случае, если функция больше чем в одну строку, то лок на handlers используется или до конца блока { }, или до конца функции.
И это эксклюзивный лок, а не rwlock.
Ооо, с такими советами раст быстро загнётся, одобряю!
не плохо бы сказать что Mutex в Rust не являются реентерабельными, в Java же по умолчаю synchronized/Lock/etc... на оборот
В Java ошибки обрабатываются с помощью исключений. В Rust исключений нет. Вместо них используется тип Result<T, E>
На самом деле в Java никто не запрещает вместо исключений использовать return-based подход к ошибкам. Например, с помощью Either из vavr.io, или можно самим написать класс-обёртку для возвращаемых значений.
В Rust есть два типа строк - &str и String. Первый представляет собой ссылку на строку, второй - владеющую строку.
Если это туториал, то, думаю, стоит упомянуть, что &'_ str
представляет собой по сути структуру с указателем на начало и длиной строки где-то в памяти (будь то стэк или куча).
А вот String
содержит Vec<u8>
и не только владеет строкой, но и всегда держит ее в куче.
Однако на практике проще начинать с String. Этот тип проще использовать, он ведет себя схожим с привычной строкой в Java. А &str требует следить за владением и ссылками
К чему это? Время жизни используется везде и не стоит искать поводы его обходить стороной. Функции на самом деле не нужен владеющий тип - println!()
этого не требует, а сама функция только деаллоцирует пришедшую извне строку - сложно.
В данном примере можно было бы спокойно использовать примитивный тип str
.
Но есть решение лучше. Задайте себе вопрос, что нам необходимо сделать с типом, или без каких его характеристик мы это сделать не сможем?
Единственное, что нам необходимо - нечто, что можно отобразить. За эту характеристику отвечает трейт Display
. Вот как измениться функция:
fn print_greeting<T: Display>(name: T) {
// Сейчас можно писать так
println!("Hello, {name}!");
}
fn main() {
let name = "John".to_string();
print_greeting(name.as_str()); // Или &name - это &str
print_greeting("Sam"); // это тоже &str
print_greeting(name); // Это просто String
// Обычно потом от строк отходят в пользу более сложных структур
// В вашем варианте придется везде писать .to_string()
// Здесь же достаточно, чтобы структура реализовала Display
print_greeting(person); // или &person
}
Второй совет тоже плохой:
Вы упоминули Debug
, но не остальные трейты. Даже Clone
забыли, без которого не получится скопировать сложные структуры (о Copy
и других даже не заикаюсь).
Третий совет еще хуже чем первый:
Когда вы используете владеемые типы вы ЯВНО управляете памятью, потому что вместе с выходом из функции начнутся ненужные удаления объектов. Идея простая - смотрите на то, как вы используете объекты. Ничего страшного в лайфтаймах нет, а с одним аргуметом нет никакого смысла избегать их (где запутаться хоть можно?).
Четвертый совет мимо кассы:
Если читатель не захочет использовать структуры, откуда он возьмет классы? В Rust их нет.
Нормальный совет мог бы звучать так: используйте композицию объектов - аналог наследования, и создавайте собственные трейты - аналог интерфейсов. Сразу отмечу, что второй более гибкий и простой на практике.
Вместо конструктора реализуют функцию new.
На самом деле это только соглашение, вполне можно использовать условный create
. Самым близким аналогом будет трейт Default
.
use std::sync::Arc;
// Классов в Rust нет
struct Struct {
pub field: i32
}
impl Default for Struct {
fn default() -> Self {
Self { field: 42 }
}
}
fn main() {
let data: Arc<Struct> = Arc::default();
println!("Field equals to '{}'", data.field);
}
Пятый совет тоже неудачный:
Поскольку Result
- тип с дженериками, разные Result
плохо между собой уживаются и требуется постоянно маппить ошибку. Используйте крейтыanyhow
и thiserror
для данных целей (смотрите документацию - оно того стоит).
// Добавьте крейт anyhow к существующему проекту:
// cargo add anyhow --features backtrace
// По умолчанию без бэктрейса
// Скрываем наш Result
use anyhow::Result;
// Используем derive macro Error из thiserror
// При желании, имя можно поменять
use thiserror::Error;
#[derive(Error, Debug)]
#[error("Unable to divide by zero denominator")]
pub struct DivisionByZeroError;
fn divide(num: i32, den: i32) -> Result<i32> {
// Для деления с проверкой лучше ее и использовать
num.checked_div(den).ok_or(DivisionByZeroError.into())
}
// Наиболее желательная сигнатура, anyhow Error можно использовать здесь
fn main() -> Result<()> {
println!("{}", divide(1, 0)?);
Ok(())
}
Blocking waiting for file lock on package cache
Compiling playground-rust v0.1.0 (...)
Finished dev [unoptimized + debuginfo] target(s) in 1.26s
Running `target\debug\playground-rust.exe`
Error: Unable to divide by zero denominator
// Если включена фича backtrace в anyhow
Stack backtrace:
В качестве типов для ошибок лучше всего использовать что-то, что реализует трейт Error
, иначе придется маппить ошибки. Два эти крейта делают все за вас.
Использование ? вместо unwrap() для эскалации ошибок
Для чего - для чего unwrap
используется? Для "эскалации" ошибки? Мы точно про один Rust говорим?
И опять же, можно переписать на нормальный код с anyhow
.
Далее идут два нормальных совета, но есть примечания:
В Java для объявления ошибок используется Exception. В Rust такого нет.
" Да ладно?! " (c) Якубович
А зачем тогда нужен трейт std::error::Error
, который используется в thiserror
и anyhow
, и который, к тому же, можно вернуть в виде Box<dyn std::error::Error>
из функции main
(тем самым позволяя пробросить ошибку откуда угодно на самый верх)?
В Java, когда вы хотите, чтобы переменная была доступна в нескольких потоках для чтения вы просто передаете ссылку.
// Не показывай этот код друзьям
struct SomeStruct {
pub thread_unsafe: String
}
unsafe impl Send for SomeStruct {}
unsafe impl Sync for SomeStruct {}
// SomeStruct можно использовать без Arc
Отличие Java от Rust здесь в том, что Rust говорит все типы нельзя безопасно передавать Send
или использовать из разных потоков Sync
, если не будет сказано обратное. Оба эти трейта - маркеры, поэтому их не стоит реализовывать не убедившись, что интерфейс типа не является потокобезопасным.
Я не так хорошо знаком с Java, но думаю там все +\- также: можно передать объект в другой поток и получить одновременный доступ к одному элементу
Кстати лучше сразу использовать futures::lock::Mutex, а не std::sync::Mutex, так как второй спроектирован для не асинхронного кода
Не пробовал, вместо этого советую сразу async_std
- там есть многие асинхронные реализации совместимые с синхронным std
(по крайней мере, когда я писал ВКР, проблем не ощутил).
Используйте RwLock
, когда необходимо получить множественное чтение (одним "мутиксом" сыт не будешь).
Еще одним интересным аспектом перехода с Java на Rust является использование трейтов (traits). В Rust нельзя объявить асинхронные функции внутри трейтов напрямую.
Можно. Для справки сейчас ведется работа над стабилизацией асинхронных трейтов. А данный макрос просто делает то, что можно уже сейчас с использованием сопли на конце: ... -> Pin<Box<dyn core::future::Future<Output = ()> + Send + '_>> {...}
Собственно, async
- это сахар, который не стабилизирован для трейтов.
Что там с выводами?
При переходе с Java на Rust лучше ориентироваться на то, что ДЕЛАЕТ тип, для чего он НУЖЕН в данной функции и отталкиваться от его функциональность. Именно ЭТО позволит быстрее написать рабочую версию кода.
При разработке ориентируетесь на трейты, а структуры воспринимайте как конкретизацию их набора.
Для обработки ошибок действительно используется
Result
, но ничего не запрещает заткнуть всеOption
(они тоже поддерживают?
). Лучше всего использоватьResult
изanyhow
и приправить щепоткойthiserror
.Все крейты упрощают разработку, равно как и для другого языка и его пакетов. Что более важно, ориентируйтесь на МАКРОСЫ. Пишите их, используйте их. И будет вам быстрая разработка.
По поводу пункта 4. Лучшим примерами будут: serde
(и связанные библиотеки serde_yaml
), thiserror
, clap
(3 версия, питоновский click
). И наверное есть много других - эти первые на ум пришли.
Раз уж я так много написал, думаю, имею право высказаться насчет статьи.
Это просто ужас. Надеюсь, ПЕРВЫЙ блин коммом. А дальнейшие статьи будут значительно лучше
P.S. Комментарий писался с перерывами суммарно около 2 часов. Я хотел уточнить некоторые вещи, но решил упростить комментарий и сконцентрироваться на самой статье. Спасибо за внимание
public String readFile(String filename) throws IOException {
File file = new File(filename);
if (!file.exists()) {
throw new IOException("File does not exist");
}
FileInputStream inputStream = new FileInputStream(file);
byte[] bytes = new byte[(int) file.length()];
inputStream.read(bytes);
inputStream.close();
return new String(bytes);
}
Похоже, что данный код свалится в компайл-тайме из-за неизвестного размера массива.
Если повнимательнее приглядеться к типичному Java-программисту, то со значительной долей вероятности это окажется Spring-программист :) Поэтому задача должна формулироваться так: как легко перейти со Spring .... куда?
Как легко перейти с Java на Rust: Особенности и советы