Как управлять памятью в Rust

Вы когда-нибудь задумывались о том, что происходит с вашей оперативной памятью , когда вы запускаете программу ? И как то , как вы пишете код, может влиять на многие другие вещи в вашей системе?

Эта статья поможет вам больше понять об управлении памятью и о том, КАК RUST РАБОТАЕТ С НЕЙ.

Стек и куча

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

Память: стек

Стек, как следует из названия, работает как стек , следуя принципу «Последним пришел — первым ушел» (LIFO). Может быть проще будет объяснить по шагам:

  • Представьте себе стопку посуды;
  • Первое блюдо, которое вы поставите, убирается последним;
  • Когда вызывается функция , блок памяти «укладывается» поверх стека;
  • Когда функция завершается , этот блок «раскладывается» , освобождая эту память.

Обычно значения , которые будут сохранены в стеке, известны компилятору (во время компиляции), поскольку он знает, сколько памяти необходимо для хранения. Этот процесс происходит автоматически , все значения удаляются из памяти.

Ниже приведен пример этого:

fn main() {
    let number = 12; // at this moment the variable has created

    println!("{}", number); // 12 
} // When the owner (main function) goes out of scope, the value will be dropped

В Rust мы можем создать некоторые области видимости, просто используя {}, и это добавляет в наш стек слой с ограниченным сроком службы. После того, как вы выходите из этой конкретной области ,память очищается, и вы теряете соответствующую информацию. Хороший и простой пример:

fn main() {
    {
        let number = 12;

        println!("{}", number); // 12
    }

    println!("{}", number); // Cannot find value `number` in this scope
}

Память: куча

В двух словах: память кучи — это свободное пространство памяти для размещения данных, которые могут измениться.

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

Если одна из приведенных выше возможностей совпадает, мы знаем, что у нас есть память Heap вместо Stack . Куча имеет более гибкую память и большое пространство. Взглянем:

let number = Box::new(12); // alocate a integer in heap

let name = String::from("Canhassi"); // alocate a String in heap

В Rust у нас есть две types строки &str и String.

  • &str : имеет фиксированный размер в зависимости от написанного текста;
  • Строка : имеет размер, который можно увеличивать, уменьшать и удалять.

... и поэтому String хранится в куче и &str в стеке.

Один из способов освободить память кучи: когда переменная, хранящая что-то, находящееся в куче, выходит из области действия функции (достигает конца функции), она освобождается так же, как и стек.

Хорошо, теперь у нас есть четкое представление об обоих типах памяти, но какая разница в управлении памятью между стеком и кучей? Давайте посмотрим на эти различия!

Borrow Checker

Borrow Checker — это часть компилятора Rust , которая проверяет и обеспечивает соблюдение правил владения, заимствования и срока действия.

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

fn main() {
    let name = String::from("Canhassi"); // Creating a string variable

    print_name(name); // calling the print_name function passing the variable

    println!("{}", name); // Error: name borrowed to print_name()
}

fn print_name(name: String) {
    println!("{}", name); // print "Canhassi"
}

Если вы запустите компилятор с этим кодом, вы увидите следующую ошибку:

borrow of moved value: name

Когда мы вызываем print_name функцию, name переменная будет перемещена в другую область видимости , и эта область станет ее новым владельцем . И правило владельца выйдет из области действия и будет применено снова. Borrow Checker гарантирует, что после передачи права собственности исходная переменная больше не может использоваться для доступа к этому значению. это произошло в приведенном выше коде.

Другой способ использования исходной переменной — использование подобных ссылок.

fn main() {
    let name = String::from("Canhassi"); // Creating a string variable

    print_name(&name); // calling the print_name function passing the variable

    println!("{}", name); // print "Canhassi"
}

fn print_name(name: &String) {
    println!("{}", name); // print "Canhassi"
}

PS: Эти правила проверки заимствований работают только с объектами, расположенными в куче .

Предотвращение ошибок

Borrow Checker имеет фундаментальное значение для обеспечения безопасности памяти Rust без необходимости использования сборщика мусора.

В других языках, таких как C и C++, нам (как разработчикам) приходится вручную освобождать память с помощью некоторых функций. C++ известен тем, что допускает ошибки управления памятью, включая утечки памяти. C++ сам по себе не вызывает утечек памяти. Вместо этого C++ предоставляет программисту большую гибкость и контроль, и эта свобода может привести к ошибкам ,если ее неправильно использовать.

Известная ошибка — Undefine Behavior . Например, представьте себе функцию, которая возвращает ссылку на такую ​​переменную.

fn main() {
    let number = foo(); // calling foo function
}

fn foo() -> &i32 {
    let number = 12; // creating a var number

    &number // try return a reference of var number
}

Этот код не работает, поскольку в компиляторе Rust есть правила ограничения управления памятью. Мы не можем вернуть ссылку на var, number потому что область действия этой функции выйдет за пределы, и var number исчезнет, ​​поэтому Rust не позволяет этому случиться. В C++ мы можем это сделать, и это позволяет избежать пресловутых утечек памяти.

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

Другой пример: Double free , который возникает, когда вы дважды пытаетесь освободить один и тот же объект, например, в коде C.

char* ptr = malloc(sizeof(char));

*ptr = 'a';
free(ptr);
free(ptr);

Эта тема действительно обширна, и существует множество возможностей возникновения подобных ошибок при работе с другими языками.

Вывод

Цель этой статьи в более общем виде представить, как управление памятью работает в Rust.