Конспектируем Книгу Rust:: Владение

от автора

Nurked предложил оригинальный способ прочтения "the book" — читать главы надо не по порядку, а в последовательности [4, 3, 5, 6, 8, 4, 9, 7, 10, 4, 13, 17, 15, 16].

Можно пойти дальше. Мне представляется, что читать будет гораздо легче и быстрее, если выкинуть бо́льшую часть текста и заменить некоторые примеры. Ниже представлен "пробник" в виде сильно сокращенной главы 4. Чтение его подразумевает наличие опыта разработки на других языках — объяснения типа "чем отличается стек от кучи", естественно, попали под оптимизацию.

Лирика

Из тех языков, с которыми я плотно работал, Rust ближе всего, КМК, к Go. Их роднит отсутствие "нормального ООП", отсутствие "нормальных исключений", концепция срезов (slice), наличие как объектов, так и ссылок/указателей на них, возможность возвращать несколько значений из функций, ну и, конечно, кросс-компиляция "из коробки". В Go пока нет обобщенных типов, но про них знают и их ждут. Поэтому опытного гофера ржавчиной не испугать.

Управление памятью

Когда владелец (owner) покидает область видимости (variable scope), его "финализируют" через вызов drop():

    {         let s = String::from("hello"); // s is valid from this point forward          // do stuff with s     }                           // This scope is now over, and s is no longer valid     // Rust calls s.drop() automatically at the closing curly bracket.

The book утверждает, что управление памятью осуществляется через "владение" с набором правил, которые компилятор проверяет во время компиляции программы. Полезно сразу иметь в виду (напомню — материал для опытных камикадзе), что есть некие:

- Box<T> для распределения значений в куче (памяти) - Rc<T> тип счётчика ссылок, который допускает множественное владение - Типы Ref<T> и RefMut<T>, доступ к которым осуществляется через тип RefCell<T>, который обеспечивает правила заимствования во время выполнения, вместо времени компиляции - Rust допускает утечки памяти, используя типы Rc<T> и RefCell<T> можно создавать связи, где элементы ссылаются друг на друга в цикле

Короче, если память выделяется в куче, а полученные объекты ссылаются друг на друга — будь готов к граблям, засадам и к поиску утечек памяти.

Воспоминание о будущем

В силу исключительной важности вопроса забегу вперед.

Интересно, что сначала the book утверждает, мол, последствия зацикливания ссылок не очень страшны, а следующим предложением идет такой текст:

However, if a more complex program allocated lots of memory in a cycle and held onto it for a long time, the program would use more memory than it needed and might overwhelm the system, causing it to run out of available memory.

Таким образом, при разработке систем с высокой нагрузкой надо быть начеку — может "натечь" много и быстро. Rust предоставляет определенный инструментарий для борьбы с этим, типа замены умного указателя Rc на Weak, но все это, конечно же, заставляет разработчика выполнять танцы с замысловатым рисунком. Например, добавление ссылки на ребенка к родителю выглядит так:

use std::cell::RefCell; use std::rc::{Rc, Weak};  // The `derive` attribute automatically creates the implementation // required to make this `struct` printable with `fmt::Debug`. #[derive(Debug)] struct Node {     value: i32,     parent: RefCell<Weak<Node>>,     children: RefCell<Vec<Rc<Node>>>, }  fn main() {     let leaf = Rc::new(Node {         value: 3,         parent: RefCell::new(Weak::new()),         children: RefCell::new(vec![]),     });      println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());      let branch = Rc::new(Node {         value: 5,         parent: RefCell::new(Weak::new()),         children: RefCell::new(vec![Rc::clone(&leaf)]),     });      *leaf.parent.borrow_mut() = Rc::downgrade(&branch);      println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); }

Может показаться, что уж больно много букв надо сгруппировать и расставить в нужном порядке, неужели человек может осилить сие и не наломать дров? Хорошие новости в том, что в этом непростом деле всегда можно и нужно рассчитывать на прочное виртуальное плечо компилятора Rust, который будет жестко пресекать попытки небрежно отнестись к мелкому растопочному материалу.

Тип String

Без него дальше никак:

fn intro_string_type(){     let immutable_string = String::from("I'm immutable");     // immutable_string.push_str("!"); // ERR: ^ cannot borrow as mutable      let mut mutable_string = String::from("I'm mutable");     mutable_string.push_str("!");      println!("immutable_string: {}", immutable_string);     println!("mutable_string: {}", mutable_string); }

  • Примеры можно покрутить тут
  • intro_string_type(), ого, змеиная нотация — так надо
  • Переменные, объявленные через let, изменять нельзя
  • Если надо менять используем let mut
  • Вывод на печать осуществляется причудливой конструкцией println! (тут может возникнуть справедливое подозрение, что это не "обычная функция")

Лирика

Я думаю, Rust имеет потенциал находить отклик в сердцах многих. let как в Бейсике, {} как в Java, :: как в С++, объявление функции похоже на таковое из Go (только там func)

Владение и его передача присваиванием (Move)

Каждое значение имеет одного и только одного владельца-переменную. После операции присваивания переменная типа String перестает владеть своим бывшим значением, и ее нельзя больше использовать:

fn once_assigned_string_may_not_be_used_anymore(){     let s1 = String::from("5");     let s2 = s1; // Ownership is moved here     // let s3 = s1; // ERR: value used here after move     // println!("{}, world!", s1); // ERR: value borrowed here after move }

Для простых типов (primitive types), однако, значение копируется, а не передается, и для них многократное присваивание выглядит обычным образом:

fn primitives_are_copied(){     let i1 = 5;     let i2 = i1;     let i3 = i1; }

  • Есть trait (как бы interface) Copy, если тип его реализует, при присваивании/передаче в функцию/возврате значения происходит копирование
  • Copy реализован для простых скалярных типов, а также для неких кортежей (tuples), при условии, что эти загадочные пока tuples содержат только типы, реализующие Copy
  • Тип String не реализует Copy
  • Из неочевидного: Copy несовместим с Drop (это где drop()). Несовместим, даже если не сам тип, а только его некоторые части реализуют Drop

Передача и возврат параметра по значению

При передаче переменной в функцию по значению происходит и передача владения (если тип не реализует интерфейс trait Copy):

fn use_str_by_value(s: String){     println!("{}", s); }  fn passing_by_value_moves_ownership(){     let s1 = String::from("5");     use_str_by_value(s1);     // let s2 = s1; // ERR: value used here after move }

Возврат значения (пример для расширения кругозора):

 fn calculate_length(s: String) -> (String, usize) {     let length = s.len();     return (s, length) }  fn calculate_length2(s: String) -> (String, usize) {     let length = s.len();     (s, length) } 

  • Из функции можно вернуть несколько значений (тот самый кортеж, или tuple)
  • usize означает целый беззнаковый тип, который вмещает указатель (the pointer-sized unsigned integer type)
  • return можно не писать
  • При возврате переменной "по значению" функция возвращает и владение (опять же, если не реализован Copy)

Ссылки и заимствование (References and Borrowing)

Необязательно брать значение во владение, его можно "занять" (borrow):

fn borrow(){     let mut s = String::from("hello");      let r1 = &s; // First immutable borrow occurs here     let r2 = &s; // Second immutable borrow occurs here     // let r3 = &mut s; // Err: mutable borrow occurs here     // r3.push_str(" world");     let r3 = &s; // Third immutable borrow occurs here      println!("{}, {}, and {}", r1, r2, r3); }

  • Занять можно как для чтения (&), так и для записи (&mut)
  • Занять для чтения (immutable borrow) можно сколько угодно раз в "области видимости переменной" (variable scope)
  • Занять для записи (mutable borrow) — только один раз
  • Нельзя занимать одновременно для чтения и записи (все это похоже на read/write locks)
  • Результат заимствования называется ссылкой (reference)

От перестановки строк из примера выше результат меняется и становится компилируемым:

fn borrow2(){     let mut s = String::from("hello");      let r1 = &s; // First immutable borrow occurs here     let r2 = &s; // Second immutable borrow occurs here     println!("{}, {}", r1, r2); // hello, hello      let r3 = &mut s; // Mutable borrow occurs here, r1 & r2 are not used anymore and out of scope     r3.push_str(" world");     println!("{}", r3); // hello world  }

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

Висячие ссылки (Dangling References)

Компилятор Rust гарантирует, что эта проблема искоренена полностью. Компилятор откажется работать с попытками подвесить ссылки, в качестве причины отказа он приведет загадочную формулировку:

// fn dangling_reference() -> &String { // ERR: expected named lifetime parameter //     let s = String::from("hello"); //     return &s // }

Загадка lifetime parameter получит раскрытие в следующих главах.

Лирика

В свое время был удивлен, что в Go можно вернуть указатель на локальную переменную, и за это тебе ничего не будет:

//go:noinline func ReturnPointerToLocal() *int{     a := 10     return &a }

На деле хитрый компилятор в этом случае выделяет память в куче:

;*** main.go#9    >func ReturnPointerToLocal() *int{ ... ;*** main.go#10   >     a := 10 0x4a7a04     488d0575b00000         LEAQ type.*+43648(SB), AX 0x4a7a0b     48890424               MOVQ AX, 0(SP) 0x4a7a0f     e84c5bf6ff             CALL runtime.newobject(SB) 0x4a7a14     488b442408             MOVQ 0x8(SP), AX 0x4a7a19     48c7000a000000         MOVQ $0xa, 0(AX)  ;*** main.go#11   >     return &a 0x4a7a20     4889442420             MOVQ AX, 0x20(SP) 0x4a7a25     488b6c2410             MOVQ 0x10(SP), BP 0x4a7a2a     4883c418               ADDQ $0x18, SP 0x4a7a2e     c3                     RET

Срезы (Slices)

Очевидное:

fn string_slice(){     let s = String::from("Yandex");     let the_third_and_fourth_bytes_slice  = &s[2..4]; // nd     let the_whole_string_slice  = &s[..]; // Yandex     let first_two_bytes_slice  = &s[..2]; // Ya     let from_the_third_byte_slice  = &s[2..]; // ndex }

Невероятное:

fn string_slice_multibyte(){     let s = String::from("y̆andex");     // let slice = &s[0..1]; // ERR: thread 'main' panicked at 'byte index 1 is not a char boundary...     // let slice = &s[0..2]; // ERR: thread 'main' panicked at 'byte index 1 is not a char boundary...     let valid_slice = &s[0..3];     println!("valid_slice: {}", valid_slice); // y̆ }

  • Срезы строк можно делать только по труъ-unicode-границам, иначе паника
  • Подробнее тут или here, кому что любо

Но то строки, с байтами ситуация попроще:

fn byte_slice(){     let s = String::from("y̆andex");     let bytes = s.as_bytes();     let slice = &bytes[0..2];     println!("{:?}, len: {}", slice, slice.len()); // [121, 204], len: 2 }

Теперь, собственно, про владение. Взятие среза "одалживает" всю последовательность:

fn slice_borrow_the_whole_sequence() {     let mut s = String::from("hello");     let first_two_bytes_slice  = &s[..2]; // he     // s.push_str(" world"); // Err: cannot borrow `s` as mutable because it is also borrowed as immutable     println!("{}", first_two_bytes_slice); }

  • Не зря это делается при помощи &
  • the book, раскрывая внутреннюю кухню, указывает, что в основе String лежат три значения (ptr, len, capacity), а slice довольствуется первыми двумя

Явное указание типов для slice не совсем очевидно:

fn return_slices(s: &String) -> (&str, &[u8]){     let bytes = s.as_bytes();     return (&s[..], &bytes[..]) }

  • Срез строки имеет особенный тип &str
  • Срез для as_bytes() имеет тип &[u8], что соответствует руководству: "slice type is &[T]"

Ссылки

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


Комментарии

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

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