Поначалу всё будет хорошо. И вы будете изучать 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 - действительно красивый язык.
Но иногда я эту красоту просто не понимаю.
Всем спасибо!
Автор: mallo_c