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

в 4:47, , рубрики: 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 - действительно красивый язык.
Но иногда я эту красоту просто не понимаю.

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

Автор: mallo_c

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js