Как стать автором
Обновить

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

Время на прочтение8 мин
Количество просмотров16K

Перед вами краткое профессиональное описание особенностей языка Rust для профессионалов.


Что это такое?
  • краткое — информации будет гораздо меньше, чем в Книге (The Rust Programming Language)
  • профессиональное — информации будет гораздо больше, чем в Книге;
  • описание особенностей — фокусируемся на отличиях Rust от других языков;
  • языка — описывается именно язык, а не установка средств разработки, управление пакетами и прочий инструментарий;
  • для профессионалов — подразумевается, что читатель имеет существенный опыт в разработке ПО.

Чего здесь НЕ будет


  • Не будет агитации за Rust
  • Не будет легко. Для освоения потребуется неделя-другая вдумчивого чтения по часу в день, с тщательным разбором примеров, как-то так

Содержание



Мотивация


Моя история изучения языка Rust началась со статьи Как мы ржавели. История внедрения и обучения. В ней Nurked предложил оригинальный способ прочтения Книги — читать главы надо не по порядку, а в последовательности [4, 3, 5, 6, 8, 4, 9, 7, 10, 4, 13, 17, 15, 16]. Со многими тезисами этой статьи я склонен в той или иной степени согласиться, в частности, с тем, что "первая глава этого руководства должна гореть в печи". Конечно же, вместе со второй, в которой разбирается вот этот пример:


use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);
}

Именно на этом примере я в свое время закрыл Книгу — неинтересно. К тому же, за println!(...) явно скрывается некий макроязык, а к ним у меня стойкая аллергия со времен работы с Microsoft Foundation Class. Ну, в общем — нет, спасибо, не надо.


После повторной подачи от Nurked начал с главы 4 и появился интерес. Как пишут сами авторы Книги:


Владение является наиболее уникальной особенностью языка Rust. Благодаря ей в Rust осуществляется безопасная работа с памятью без необходимости использования автоматической системы сборки мусора (garbage collector).

С этого и надо было начинать. Я отношусь к той категории людей, которые несколько измучены нарзаном недовольны нюансами работы с GC в рамках highload, засим текст нашел внимательного читателя в моем лице. В процессе чтения главы 4 я обнаружил, что Книга рассчитана на совсем новичков и переполнена скучными подробностями, поэтому изначально у меня была идея просто законспектировать суть, отсекая все лишнее. Но эта идея потерпела крах, так как оказалось, что для понимания принципов работы с памятью после главы 4 надо читать главу 10.3, потом главу 15, при этом текста много, он содержит ошибки и его совершенно недостаточно. Для понимания пришлось дополнительно читать, например, это, это, это, это и вот это, но нужных мне примеров я так и не нашел.


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


На кого рассчитан материал


Читатель должен владеть на уровне middle+ двумя языками программирования (один с GC, один без него), они послужат донорами абстракций, которые в тексте не поясняются.


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


Что делать, если знаешь только один язык? План Б.


Особенности изложения


Изложение в первую очередь сфокусировано на той самой безопасной работе с памятью, которая и является основной "фишкой" Rust. Остальное часто дается "мимоходом" и подчинено главной цели.


Скачивать и ставить ничего не нужно, с примерами можно работать через play.rust-lang.org.


Поехали.


Финализация через drop()


Когда объект покидает область видимости (variable scope), его "финализируют" через вызов метода drop(). В этом деструкторе можно произвести некие завершающие действия — возвратить память в кучу, закрыть соединение и т.д. Для примера создадим экземпляр типа String с помощью конструктора фабрики String::from():


    {
        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.

String хранит свои данные в куче, и компилятор Rust любезно обеспечит неявный вызов s.drop() после закрывающей скобки, тем самым давая возможность вернуть использованную память.


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


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

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


Изменяемость переменных


Пример:


fn main(){
    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("!");

    dbg!(immutable_string);
    dbg!(mutable_string);
}

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

Лирика

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


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


Ключевое правило: Каждое значение имеет одного и только одного владельца-переменную


После операции присваивания переменная типа String перестает владеть своим бывшим значением, и ее нельзя больше использовать:


fn main(){
    let s1 = String::from("5");
    let s2 = s1; // Ownership is moved here
    // let s3 = s1; // ERR: value used here after move
    dbg!(s2);    
}

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


fn main(){
    let i1 = 5;
    let i2 = i1;
    let i3 = i1;
    dbg!(i1);
    dbg!(i2);
    dbg!(i3);
}

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

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


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


fn use_str_by_value(s: String) {
    dbg!(s);
}

fn main() {
    let s1 = String::from("Hello");
    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();
    (s, length)
}

fn main(){
    let (s1, length) = calculate_length(String::from("Hello"));
    dbg!(s1, length);
}

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

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


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


fn main() {
    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: cannot borrow `s` as mutable because it is also borrowed as immutable
    // 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 main() {
    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
}

  • Здесь вроде как одновременно существуют две ссылки на чтение и одна на запись, но r1 и r2 после вывода на печать больше не используются, так что активных ссылок на момент let r3 =... — нет

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


Компилятор Rust гарантирует, что эта проблема искоренена полностью. Рассмотрим такой пример:


fn dangling_reference() -> &str {
    let s = String::from("hello");
    return &s
}

Здесь мы заводим локальную переменную и затем возвращаем ссылку на нее. Что, конечно, очень плохо, так как переменной и ее значения после возврата из функции больше нет, а ссылка на несуществующее — есть.


Компилятор откажется работать в таких условиях, в качестве причины отказа он приведет загадочную формулировку: "error[E0106]: missing lifetime specifier". Загадка lifetime specifier получит раскрытие в следующих главах.


Лирика

В свое время был удивлен, что в 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)


Очевидное:


#![allow(unused)]
fn main(){
    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 main(){
    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 main(){
    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 main() {
    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);
}

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

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


#![allow(unused)]

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

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

Далее: Времена и функции

Теги:
Хабы:
Всего голосов 19: ↑19 и ↓0+19
Комментарии7

Публикации