Допустим, вы решили изучить Rust

от автора

Поначалу всё будет хорошо. И вы будете изучать 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 

То есть, получается, моя программа:

  1. Сравнивает a и b через PartialOrd

  2. Понимает, что они равны

  3. Ещё раз сравнивает, но теперь уже через PartialEq

  4. Понимает, что они равны

  5. Ещё раз сравнивает Наконец-то выводит 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... 

Итак, программа:

  1. Сравнивает a и b через PartialOrd

  2. Понимает, что они равны

  3. Ещё раз сравнивает, но теперь уже через PartialEq

  4. Понимает, что они НЕ равны

  5. Сравнивает a и b через PartialOrd

  6. Понимает, что они равны

  7. Пишет 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 

А 100! немножко не влезает в 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 — действительно красивый язык.
Но иногда я эту красоту просто не понимаю.

Всем спасибо!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Почему у Rust маскот — краб? Жду только ваше мнение

22.22% Может больно ущипнуть2
11.11% Симпатичный язык и симпатичный краб1
66.67% Потому что краб связан с программированием точно так же, как и ржавчина6
0% Какая разница? Ну вот какая разница?0

Проголосовали 9 пользователей. Воздержались 3 пользователя.

ссылка на оригинал статьи https://habr.com/ru/articles/862102/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *