Морозное январское утро. Салаты доедены, а организм всё ещё не понимает, какой сейчас год. Душа просит начать новый pet проект, которому никогда не суждено увидеть свет. И тут я узнаю, что вместо pip стильно модно молодежно теперь использовать написанный на Rust uv, а ядро pydantic v2 переписали на rust. Стоп, а зачем питоновским инструментам Rust?
Если убрать маркетинг, ответ довольно приземлённый. Python отлично справляется, пока:
- ты быстро пишешь код
- тебе важнее скорость разработки, а не выполнения
- ошибки можно поймать после “ща-ща докатим в прод до дедлайн и тестами обмажем”
Но есть класс задач, где это перестаёт работать комфортно:
- высокая стоимость ошибки
- производительность
- надежность
И в этих местах внезапно оказывается, что удобство Python начинает стоить ощутимых ресурсов.
Привет, мир! и отличия синтаксиса Link to heading
print("hello, world!")
fn main() {
println!("Hello, World!");
}
Отличие:
- в Python это выглядит как команда, интерпритатор читает файл и выполняет. В Rust требуется явно объявить точку входа
main() - в Python блоки выделяются отступами, в Rust заключено в
{...} - в Rust у println есть
!- индикатор макроса. Помимо декларативных макросовvec!,panic!существуют еще процедурные, например[derive(Debug, Serialize)] - в Python строка заканчивается переводом строки, в Rust
; - в Python код интерпритируется, в Rust компилируется и запускается
rustc hello.rs
Переменные и мутабельность Link to heading
name = "Stas"
count = 0
for _ in range(3):
count += 1
print(f"Hello, {name}!")
print(f"Повторил {count} раза, понимаю как тяжело после нового года")
fn main() {
let name = "Stas";
let mut count = 0;
for _ in 0..3 {
count += 1;
println!("Hello, {}!", name);
}
println!("Повторил {} раза, понимаю как тяжело после нового года", count);
}
- в Python переменные объявляются без типов. Присвоил и поехали. В Rust используется
let, а тип либо указывается явно, либо выводится компилятором - в Python переменные изменяемы по умолчанию. В Rust нет. Нужно явно написать
mut. Тут первый культурный шок. Требуется подумать о том, будет ли эта переменная меняеться. - в Python есть несколько способ форматирования строк. В Rust используется
{}с опциональными позиционными{0}, {1}, именованными{name}аргументами или спецификаторами типа{:?},{:b},{:x},{:o}
Ownership и borrowing Link to heading
def greet(name):
print(f"Hello, {name}!")
name = "Stas"
greet(name)
greet(name)
На Rust это выглядело бы так:
fn greet(name: String) {
println!("Hello, {}!", name);
}
fn main() {
let name = String::from("Stas");
greet(name);
greet(name); // ошибка компилятора
}
Если бы не borrow checker, с которым придется изрядно БОРОТЬСЯ в процессе написания кода и его отладки:
rustc hello.rs
error[E0382]: use of moved value: `name`
--> hello.rs:9:11
|
6 | let name = String::from("Stas");
| ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
7 |
8 | greet(name);
| ---- value moved here
9 | greet(name); // ошибка
| ^^^^ value used here after move
|
note: consider changing this parameter type in function `greet` to borrow instead if owning the value isn't necessary
...
В Python нет необходимости думать кто владеет объектом и его использует. Ссылки, копии, память остаются под капотом и являются заботой сборщика мусора.
В Rust у каждого объекта есть владелец и правила того, кто и как его может использовать. Значение name было перемещено в функцию greet и чтобы не терять переменную следует использовать заимствование &:
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let name = String::from("Stas");
greet(&name);
greet(&name);
}
И тут появляется деталь, которая выглядит избыточной. Значение переменной через &str let name = "Stas"; изменилось на String let name = String::from("Stas");
&strты не владелец, а читатель. Строка лежит где-то в бинарнике, ты получаешь ссылку, ничего не аллоцируешь и не освобождаешьStringкогда появляется владелец. Память выделяется в куче, у строки есть владелец, при выходе из области память освобождается
name (в стеке) -> [ ptr | len | capacity ] -> "Stas" (в куче)
Чтобы не терять переменную используется заимствование (borrow) и в коде появляется &, временный доступ. В функцию передается указатель, владельцем остается main.
Но для Rust этого мало. Данные через обычную ссылку & нельзя изменить, требуется &mut при этом других ссылок в момент изменения быть не должно: либо много читающих &, либо один пищущий &mut.
fn main() {
let mut name = String::from("Stas");
let r1 = &name; // обычная ссылка
let r2 = &mut name; // ошибка
println!("{}", r1);
println!("{}", r2);
}
Все это проверяется в процессе компиляции, а не в момент выполнения кода:
rustc hello.rs
error[E0502]: cannot borrow `name` as mutable because it is also borrowed as immutable
--> hello.rs:5:14
|
4 | let r1 = &name; // обычная ссылка
| ----- immutable borrow occurs here
5 | let r2 = &mut name; // mutable ссылка — ошибка!
| ^^^^^^^^^ mutable borrow occurs here
6 |
7 | println!("{}", r1);
| -- immutable borrow later used here
При этом сама ссылка не может жить дольше, чем владелец:
fn main() {
let r;
{
let name = String::from("Stas");
r = &name;
}
println!("{}", r); // ошибка
}
rustc hello.rs
error[E0597]: `name` does not live long enough
--> hello.rs:6:13
|
5 | let name = String::from("Stas");
| ---- binding `name` declared here
6 | r = &name;
| ^^^^^ borrowed value does not live long enough
7 | }
| - `name` dropped here while still borrowed
8 |
9 | println!("{}", r); // ошибка
| - borrow later used here
В Python отсутствие значений часть повседневных AttributeError:
def get_user_name(user):
return user.get("name")
user = {"id": 1}
name = get_user_name(user)
print(name.upper())
В Rust “забыли значение” уже тип:
fn get_user_name() -> Option<String> {
None
}
Второй культурный шок последняя строка функции это и есть return в Python. А Option это либо String либо ничего. Под капотом это выглядит как или или:
Some("Stas".to_string()) // есть значение
None // значения нет
Компилятор Rust на это мягонько намекнет:
fn get_user_name() -> Option<String> {
None
}
fn main() {
let name = get_user_name();
println!("{}", name.to_uppercase()); // ошибка
}
rustc hello.rs
error[E0599]: no method named `to_uppercase` found for enum `Option<T>` in the current scope
--> hello.rs:8:25
|
8 | println!("{}", name.to_uppercase()); // не скомпилируется
| ^^^^^^^^^^^^ method not found in `Option<String>`
|
note: the method `to_uppercase` exists on the type `String`
--> /rustc/254b59607d4417e9dffbc307138ae5c86280fe4c\library\alloc\src\str.rs:466:5
help: consider using `Option::expect` to unwrap the `String` value, panicking if the value is an `Option::None`
|
8 | println!("{}", name.expect("REASON").to_uppercase()); // не скомпилируется
| +++++++++++++++++
error: aborting due to 1 previous error
И ты просто обязан с этим что-то сделать:
fn main() {
let name = get_user_name();
match name {
Some(n) => println!("{}", n.to_uppercase()),
None => println!("Гость"),
}
}
Либо, что используется чаще:
fn main() {
let name = get_user_name().unwrap_or("Гость".to_string());
println!("{}", name.to_uppercase());
}