Pull to refresh

Comments 23

Это позволит имитировать семантику ссылок на объекты в Java.

Имхо, конечно, но это очень плохой совет. Мало того что объекты за Rc/Arc нельзя мутировать без Cell/RefCell, так ещё и попытки натянуть сборку мусора на язык без оной плохо кончаются

Там в статье говориться про Mutex и про то, что это решение временное, что бы быстро написать рабочий код. И тут сборку мусора никто не натягивает. В Rust ее просто нет )

Используйте структуры с функциями вместо классов

Весьма возможно , что этот совет хорош не только лишь для раста, но и для джавы.

Таки статья дает советы как делать в Rust, а не в Java )

используйте для конвертации ошибок:

.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 - это сахар, который не стабилизирован для трейтов.

Что там с выводами?

  1. При переходе с Java на Rust лучше ориентироваться на то, что ДЕЛАЕТ тип, для чего он НУЖЕН в данной функции и отталкиваться от его функциональность. Именно ЭТО позволит быстрее написать рабочую версию кода.

  2. При разработке ориентируетесь на трейты, а структуры воспринимайте как конкретизацию их набора.

  3. Для обработки ошибок действительно используется Result, но ничего не запрещает заткнуть все Option (они тоже поддерживают ?). Лучше всего использовать Result из anyhow и приправить щепоткой thiserror.

  4. Все крейты упрощают разработку, равно как и для другого языка и его пакетов. Что более важно, ориентируйтесь на МАКРОСЫ. Пишите их, используйте их. И будет вам быстрая разработка.

По поводу пункта 4. Лучшим примерами будут: serde (и связанные библиотеки serde_yaml), thiserror, clap (3 версия, питоновский click). И наверное есть много других - эти первые на ум пришли.

Раз уж я так много написал, думаю, имею право высказаться насчет статьи.

Это просто ужас. Надеюсь, ПЕРВЫЙ блин коммом. А дальнейшие статьи будут значительно лучше

P.S. Комментарий писался с перерывами суммарно около 2 часов. Я хотел уточнить некоторые вещи, но решил упростить комментарий и сконцентрироваться на самой статье. Спасибо за внимание

Зачетная критика. Снимаю шляпу. Вы явно Rust-гуру.

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 .... куда?

Sign up to leave a comment.

Articles