Поначалу всё будет хорошо. И вы будете изучать Rust, и думать, какие хорошие люди его написали. В нём есть автоопределение типов, безопасные указатели aka ссылки, столько синтаксического сахара, что любой Kotlin позавидует, и плюс ко всему этому ещё и кроссплатформенность и no-std режим, если вы вдруг решите запрограммировать кофеварку.
А потом одной чёрной-чёрной ночью вы обнаружите там…
Interior Mutability
Переменные, которые вы объявите через let, нельзя взять и поменять, а те, что объявлены через let mut, — можно:
fn main() { let a = 5; let mut b = 7; // a = 11; // не компилируется b = 9; println!("{a} {b}"); }
Если вы в функцию передаёте ссылку, созданную через &, то, на что указывает ссылка, менять нельзя, а если ссылка создана через &mut, то можно:
fn main() { let mut a=7; let mut b=5; println!("Было: a={a}, b={b}"); swap_two_ints_wrong(&mut a, &mut b); println!("swap_two_ints_wrong: a={a}, b={b}"); // swap_two_ints(&a, &b); // не компилируется swap_two_ints(&mut a, &mut b); println!("swap_two_ints: a={a}, b={b}"); let c=1; let d=8; // swap_two_ints(&mut c, &mut d); // не компилируется } fn swap_two_ints_wrong(a: &isize, b: &isize) { let temp = *a; // *a = *b; // не компилируется // *b = temp; // не компилируется } fn swap_two_ints(a: &mut isize, b: &mut isize) { let temp = *a; *a = *b; *b = temp; }
Круто, то есть я могу контролировать, может ли моя переменная измениться и где? Определё…
use std::cell::RefCell; fn main() { let /*mut*/ r = RefCell::new(7); println!("Было: {}", r.borrow()); nasty_function(&r); println!("Стало: {}", r.borrow()); } fn nasty_function(r: &/*mut*/ RefCell<isize>) { let rc = RefCell::new(17); r.swap(&rc); }
Ещё раз: мы только что поменяли то, что лежит внутри переменной r, при этом в коде нет ни одного mut!
Опа, я нашёл баг!!! А вот и нет! Мне даже официальный туториал говорит, что так можно:
A consequence of the borrowing rules is that when you have an immutable value, you can’t borrow it mutably.
…
However, there are situations in which it would be useful for a value to mutate itself in its methods…
Чего? Вот так и пишут.
but appear immutable to other code.
Похоже «не писать pub» больше не вариант, надо что-нибудь ещё изобрести!
PartialOrd, PartialEq
Предположим, у вас есть структура:
struct NamedNumber(i64, char);
И вам нужно понять, какая из переменных с типом NamedNumber больше:
fn main() { let a = NamedNumber(10, 'A'); let b = NamedNumber(12, 'B'); if a > b { println!("a is more than b") } else if a == b { println!("a is equal to b") } else if a<b { println!("a is less than b") } else { println!("it is impossible...") } }
Вам Rust говорит, что нужно, чтобы для этого объекта я определил PartialOrd, иначе их не сравнить:
impl PartialOrd for NamedNumber { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { if self.0 > other.0 { Some(Ordering::Greater) } else if self.0 == other.0 { Some(Ordering::Equal) // запомните это место, особенно слово Equal } else { Some(Ordering::Less) } } } // всё остальное как раньше...
Вроде всё логично, мы объясняем Rust, как понять, что первая больше второй, первая меньше второй, или первая равна второй…нет, вы этого не определяли!
error[E0277]: can't compare `NamedNumber` with `NamedNumber` --> src/main.rs:25:16 | 25 | } else if a<b { | ^ no implementation for `NamedNumber == NamedNumber` | = help: the trait `PartialEq` is not implemented for `NamedNumber`
Хорошо, ты меня убедил! Сделаем так:
use std::cmp::Ordering; struct NamedNumber(i64, char); impl PartialOrd for NamedNumber { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { if self.0 > other.0 { Some(Ordering::Greater) } else if self.0 == other.0 { println!("Это сообщение всё равно никто не напечатает, я ведь уже в PartialEq проверил, что они равны"); Some(Ordering::Equal) } else { Some(Ordering::Less) } } } impl PartialEq for NamedNumber { fn eq(&self, other: &Self) -> bool { self.0 == other.0 } } fn main() { let a = NamedNumber(10, 'A'); let b = NamedNumber(10, 'B'); if a > b { println!("a is more than b") } else if a == b { println!("a is equal to b") } else if a<b { println!("a is less than b") } else { println!("it is impossible...") } }
И выводит моя программа:
Это сообщение всё равно никто не напечатает, я ведь уже в PartialEq проверил, что они равны a is equal to b
Понял, а зачем я тогда этот PartialEq определял?
use std::cmp::Ordering; struct NamedNumber(i64, char); impl PartialOrd for NamedNumber { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { if self.0 > other.0 { Some(Ordering::Greater) } else if self.0 == other.0 { Some(Ordering::Equal) } else { Some(Ordering::Less) } } } impl PartialEq for NamedNumber { fn eq(&self, other: &Self) -> bool { println!("Это сообщение всё равно никто не напечатает, я ведь уже в PartialOrd проверил, что они равны"); // теперь эта строчка тут self.0 == other.0 } } fn main() { let a = NamedNumber(10, 'A'); let b = NamedNumber(10, 'B'); if a > b { println!("a is more than b") } else if a == b { println!("a is equal to b") } else if a<b { println!("a is less than b") } else { println!("it is impossible...") } }
И моя программа печатает…
Это сообщение всё равно никто не напечатает, я ведь уже в PartialOrd проверил, что они равны a is equal to b
То есть, получается, моя программа:
-
Сравнивает
aиbчерезPartialOrd -
Понимает, что они равны
-
Ещё раз сравнивает, но теперь уже через
PartialEq -
Понимает, что они равны
-
Ещё раз сравниваетНаконец-то выводитa is equal to b
А теперь подумаем, ЗАЧЕМ он это делает?
impl PartialOrd for NamedNumber { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { if self.0 > other.0 { Some(Ordering::Greater) } else if self.0 == other.0 { println!("Сравним через PartialOrd: {} и {}", self.1, other.1); Some(Ordering::Equal) } else { Some(Ordering::Less) } } } impl PartialEq for NamedNumber { fn eq(&self, other: &Self) -> bool { println!("Пробуем сравнить через PartialEq, чтоб наверняка: {} и {}", self.1, other.1); false } } // всё остальное как и раньше
Делайте ставки, что выведет программа!
Спорим, вы не угадали?
Сравним через PartialOrd: A и B Пробуем сравнить через PartialEq, чтоб наверняка: A и B Сравним через PartialOrd: A и B it is impossible...
Итак, программа:
-
Сравнивает
aиbчерезPartialOrd -
Понимает, что они равны
-
Ещё раз сравнивает, но теперь уже через
PartialEq -
Понимает, что они НЕ равны
-
Сравнивает
aиbчерезPartialOrd -
Понимает, что они равны
-
Пишет
it is impossible...
Несложные сообщения об ошибках
Если вы вдруг упустили mut, или .into(), или тип перепутали, или ещё что-нибудь, Rust вам об этом заботливо скажет, например:
error[E0308]: mismatched types --> src/main.rs:2:21 | 2 | let x: String = 181; | ------ ^^^- help: try using a conversion method: `.to_string()` | | | | | expected `String`, found integer | expected due to this For more information about this error, try `rustc --explain E0308`.
А потом вы проснулись. Например, вы в обработчике запроса захватили какую-то переменную, которая не реализует ни Send, ни Sync, ни 'static. Код:
use std::io; use axum::{routing::get, serve, Router}; use tokio::{net::TcpListener, sync::Mutex}; #[tokio::main] async fn main() -> io::Result<()> { let x = Mutex::new(0); let r = Router::new() .route("/", get(|| async move { let mut l = x.lock().await; *l += 1; format!("Вы посетили эту страницу {l} раз") })); serve(TcpListener::bind("0.0.0.0:8080").await?, r).await.unwrap(); Ok(()) }
И Rust мне выводит достаточно понятный и несложный для прочтения лог, в котором рассказывает, что именно пошло не так:
error[E0277]: the trait bound `tokio::sync::Mutex<i32>: Clone` is not satisfied in `{closure@src/main.rs:10:25: 10:27}` --> src/main.rs:10:25 | 10 | .route("/", get(|| async move { | --- ^- | | | | _____________________|___within this `{closure@src/main.rs:10:25: 10:27}` | | | | | required by a bound introduced by this call 11 | | let mut l = x.lock().await; 12 | | *l += 1; 13 | | format!("Вы посетили эту страницу {l} раз") 14 | | })); | |_________^ within `{closure@src/main.rs:10:25: 10:27}`, the trait `Clone` is not implemented for `tokio::sync::Mutex<i32>`, which is required by `{closure@src/main.rs:10:25: 10:27}: Handler<_, _>` | = help: the following other types implement trait `Handler<T, S>`: `Layered<L, H, T, S>` implements `Handler<T, S>` `MethodRouter<S>` implements `Handler<(), S>` note: required because it's used within this closure --> src/main.rs:10:25 | 10 | .route("/", get(|| async move { | ^^ = note: required for `{closure@src/main.rs:10:25: 10:27}` to implement `Handler<((),), ()>` note: required by a bound in `axum::routing::get` --> /home/mallo_c/.cargo/registry/src/index.crates.io-6f17d22bba15001f/axum-0.7.9/src/routing/method_routing.rs:439:1 | 439 | top_level_handler_fn!(get, GET); | ^^^^^^^^^^^^^^^^^^^^^^---^^^^^^ | | | | | required by a bound in this function | required by this bound in `get` = note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0277`.
Чего??? А что я не так сделал? Где я не так сделал? Ай-ай-ай, ну что за позор, ты же мысли читать умеешь, возьми да прочитай!
На самом деле надо этот x обернуть в какой-нибудь Arc, например так:
use std::{io, sync::Arc}; use axum::{routing::get, serve, Router}; use tokio::{net::TcpListener, sync::Mutex}; #[tokio::main] async fn main() -> io::Result<()> { let x = Arc::new(Mutex::new(0)); // меняем всё тут let r = Router::new() .route("/", get(|| async move { let mut l = x.lock().await; // это проблемная строчка *l += 1; format!("Вы посетили эту страницу {l} раз") })); serve(TcpListener::bind("0.0.0.0:8080").await?, r).await.unwrap(); Ok(()) }
И заметьте, ни на одну из отмеченных строчек компилятор мне не указал!
ZeroVer
У нас в Rust есть SemVer. Это значит, что первая цифра определяет что-то ну совсем важное, что совсем всё сломает, вторая цифра определяет, что-нибудь чуть менее важное, где можно чуть поменять код, и всё будет работать, третья цифра определяет что-то, где можно даже не менять код, и всё по прежнему будет работать. Причём, если первая цифра 0, это означает, что это unstable-релиз и разработчик может вот хоть прям щас сломать библиотеку, и это всё будет по SemVerу.
Смотрим:
axum — v0.7.6
tower — v0.5.1
sqlx — v0.8.1
Окей, с вебом понятно, может с чем-то базовым получше?
rand — v0.8.5
num — v0.4.3
hashbrown — v0.14.5
itertools — v0.13.0
И, наконец, мой любимец:
base64 — v0.22.1
Алгоритм кодирования в base64 же у нас каждые полгода меняется и всё никак не хочет стабилизироваться? :]
Removed vs Not yet released (Rust 1.82)
Однажды в Rust появился оператор ?, который позволяет просто взять и вернуть ошибку из функции.
Пусть вы делаете программу, которая читает файл и записывает в stdout только первые 5 байтов или меньше (что-то вроде head -c 5).
Сравните:
fn head_c_5(file_path: &str) -> io::Result<()> { let mut bytes = [0u8; 5]; let n = File::open(file_path)?.read(&mut bytes)?; io::stdout().write_all(&bytes[0..n]) }
vs
fn head_c_5(file_path: &str) -> io::Result<()> { let mut bytes = [0u8; 5]; let mut file = match File::open(file_path) { Ok(f) => f, Err(e) => return Err(e) }; let n = match file.read(&mut bytes) { Ok(n) => n, Err(e) => return Err(e) }; io::stdout().write_all(&bytes[0..n]) }
Классная идея с этим оператором! Тем более, что вдруг разработчики Rust решили: давайте-ка мы сделаем трейт std::ops::Try, чтобы не только для Result можно было ставить знак вопроса!
Сделали-то они сделали, но вот потом они подумали, что как-то плохо сделали и надо бы по другому. Делают-делают, делают-делают, а пока можно и оставить старый… Нет, оставили только новый! Давайте-ка его испытаем…
error[E0554]: `#![feature]` may not be used on the stable release channel
То есть нет мне ни старого способа, ни нового… По крайней мере, на stable-версии.
Тем временем Python две версии подряд вежливо предлагал разработчикам перейти на setuptools, при этом он не удалял свой distutils аж до 3.12! Нет, разработчики Rust так не умеют, они — люди решительные.
Методы на все случаи жизни
Например, у итераторов есть product, который должен считать произведение элементов:
fn main() { println!("{}", (1..100).product::<u128>()) }
Посмотрим, как это работает:
thread 'main' panicked at /rustc/a7399ba69d37b019677a9c47fe89ceb8dd82db2d/library/core/src/iter/traits/accum.rs:149:1: attempt to multiply with overflow
А немножко не влезает в u128!
Похоже, что разработчики, которые писали product, не учли, что иногда произведение элементов всё же вылезает за u<что-нибудь>.
Может, сделаем Iterator.product_mod и будем считать произведение по модулю? Не, лень.
Или вот sum():
fn main() { println!("{}", (1..100).sum::<u32>()) }
И чем их не устроил .fold(0, |a, b| a+b)?
В Rust можно так:
fn main() { println!("{}", (1..100).reduce(|a, b| a+b).unwrap_or(0)); }
А можно так:
fn main() { println!("{}", (1..100).fold(0, |a, b| a+b)); }
В Rust можно так:
fn main() { let iter = (1..100).map(|x| { println!("{x}"); x }); let v: Vec<u64> = iter.collect(); // Что-нибудь делаем с v }
А можно так:
fn main() { let iter = (1..100).inspect(|x| { println!("{x}"); }); let v: Vec<u64> = iter.collect(); // Что-нибудь делаем с v }
Я конечно понимаю, что каждый пишет, как хочет, но давайте не будем это доводить до абсурда!
Выводы
Я не сомневаюсь, Rust — действительно красивый язык.
Но иногда я эту красоту просто не понимаю.
Всем спасибо!
ссылка на оригинал статьи https://habr.com/ru/articles/862102/
Добавить комментарий