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