Если предыдущую яму мы копали маленькой лопатой, то сегодня яму копали экскаватором.
Сегодня мы собираемся представить основную концепцию Rust: владение. Полный текст будет разделен на две части: что такое право собственности и тип передачи собственности.
Что такое право собственности
Каждый язык программирования имеет свой собственный набор методов управления памятью. Некоторые требуют явного выделения и освобождения памяти (например, C), а некоторые языки полагаются на сборщик мусора для освобождения неиспользуемой памяти (например, Java). И Rust не принадлежит ни к одному из вышеперечисленных, у него есть свой набор правил управления памятью, который называется Ownership.
Прежде чем вдаваться в подробности права собственности, я хочу сделать одно замечание.Руководство Rust по попаданию в яму: обычные процедурыТип данных, описанный в статье, хранится в стеке. В виде строки или какой-либо пользовательской сложной структуры данных (мы представим их позже), а ее данные хранятся в памяти стека. После этого давайте посмотрим на правила владения.
Правила владения
- В Rust каждому значению соответствует переменная, которая называется владельцем значения.
- У значения может быть только один владелец одновременно
- Когда владелец выходит за рамки, значение будет уничтожено
Эти три правила очень важны, и их запоминание поможет вам лучше понять эту статью.
переменная область видимости
Одно из правил владения состоит в том, что когда владелец выходит за рамки, значение будет уничтожено. Так как же определяется область действия владельца? В Rust фигурные скобки часто являются признаком переменной области видимости. Чаще всего в функции область действия переменной s действует с момента определения до конца функции, переменная недействительна.
fn main() { // s is not valid here, it’s not yet declared
let s = "hello"; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
Это очень похоже на большинство других языков программирования.Для большинства языков программирования он начинается с определения переменной и выделяет память для переменной. А переработка памяти — это Восемь Бессмертных, пересекающих море, чтобы показать свои магические силы. Для языков, которые полагаются на GC, не нужно заботиться о высвобождении памяти. Некоторые языки требуют явного высвобождения памяти. Явная переработка будет иметь определенные проблемы, такие как забывание или повторная переработка. Чтобы быть более дружелюбным к разработчикам, Rust использует метод автоматического высвобождения памяти, то есть, когда переменная выходит за пределы области видимости, память, выделенная для переменной, высвобождается.
Движение собственности
Ранее мы упоминали, что фигурные скобки обычно являются признаком изоляции области видимости переменной (т. е. право собственности недопустимо). Помимо фигурных скобок, есть и другие ситуации, которые меняют право владения.Давайте сначала рассмотрим два фрагмента кода.
let x = 5;
let y = x;
println!("x: {}", x);
let s1 = String::from("hello");
let s2 = s1;
println!("s1: {}", s1);
Примечание автора: двойное двоеточие — это знак ссылки на функцию в Rust, что означает ссылку на функцию from в String, которая обычно используется для создания строкового объекта.
Единственная разница между двумя фрагментами кода, по-видимому, заключается в типе переменной, первый использует целочисленный тип, а второй использует строковый тип. Результатом выполнения является то, что значение x может быть напечатано нормально в первом абзаце, но во втором абзаце сообщается об ошибке. Что является причиной этого?
Проанализируем код. Для первого кода сегмента имеется значение шарнира 5, которое присваивается переменной X, затем значение X копируется, а значение присваивается Y. Наконец, мы успешно напечатали X. Это выглядит логичнее. На самом деле, RUST тоже работает.
Для второго куска кода мы представляем, что этот процесс тоже можно сделать, но на самом деле Rust этого не делает. Начнем с причины: для более крупных объектов такое копирование занимает очень много места и времени. Так какова реальная ситуация в Rust?
Во-первых, нам нужно понять структуру типа String в Rust:
В левой части рисунка выше показана структура объекта String, включая указатель на содержимое, длину и емкость. Длина и вместимость здесь одинаковые, поэтому пока не будем на это обращать внимание. Разница между ними будет упомянута позже, когда будет подробно описан тип String. Эта часть содержимого хранится в памяти стека. Правая часть — это содержимое строки, которое хранится в куче памяти.
Некоторые друзья могли подумать, что, поскольку копирование контента приведет к пустой трате ресурсов, я буду копировать только часть структуры.Независимо от того, сколько контента, длина копируемого контента контролируема, и он также копируется в стек , аналогично целочисленному типу. . Этот метод звучит хорошо, давайте проанализируем его. Согласно приведенному выше утверждению, структура памяти, вероятно, такова.
В чем проблема с этим? Помните правила собственности? Когда владелец выходит из области видимости, память, занятая его данными, освобождается. В этом примере, когда выполнение функции завершается, s1 и s2 одновременно выходят из области видимости, тогда память в правой части рисунка выше будет освобождена дважды. Это также создает непредсказуемые ошибки.
Чтобы решить эту проблему, Rust реализуетlet s2 = s1;
При использовании этого кода считается, что s1 вышел за рамки, то есть владельцем контента справа стал s2.Также можно сказать, что право собственности на s1 было передано s2. Именно такая ситуация показана на рисунке ниже.
Другая реализация: клон
Если вам действительно нужна глубокая копия, т.е. скопируйте данные в куче памяти. Rust тоже может это сделать, он предоставляет общедоступный метод под названием clone.
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
После выполнения метода клонирования структура памяти выглядит следующим образом:
переход между функциями
Ранее мы говорили о том, что право собственности передается между строками, и то же самое верно и между функциями.
fn main() {
let s = String::from("hello"); // s 作用域开始
takes_ownership(s); // s's 的值进入函数
// ... s在这里已经无效
} // s在这之前已经失效
fn takes_ownership(some_string: String) { // some_string 作用域开始
println!("{}", some_string);
} // some_string 超出作用域并调用了drop函数
// 内存被释放
Есть ли способ заставить s продолжать действовать после выполнения функции take_ownership? Как правило, мы будем думать о возврате права собственности в функцию. Затем естественно подумать о возвращаемом значении функции, которую мы представили ранее. Поскольку передаваемые параметры могут передавать права собственности, возвращаемое значение также должно быть в порядке. Итак, мы можем сделать это:
fn main() {
let s1 = String::from("hello"); // s2 comes into scope
let s2 = takes_and_gives_back(s1); // s1 被转移到函数中
// takes_and_gives_back,
// 将ownership还给s2
} // s2超出作用域,内存被回收,s1在之前已经失效
// takes_and_gives_back 接收一个字符串然后返回一个
fn takes_and_gives_back(a_string: String) -> String { // a_string 开始作用域
a_string // a_string 被返回,ownership转移到函数外
}
Это может удовлетворить наши потребности, но это слишком громоздко, и, к счастью, Rust также находит это громоздким. Это дает нам еще один метод: цитирование (references).
Цитировать и заимствовать
Эталонный метод очень прост, просто добавьте&
символ.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
Эта форма может получить доступ к значению без владения. Принцип заключается в следующем:
Этот пример очень похож на тот, который мы написали ранее. Более пристальный взгляд покажет некоторые подсказки. Есть два основных отличия:
- При передаче параметров перед s1 ставится амперсанд. Это означает, что мы создаем ссылку на s1, который не является владельцем данных и, следовательно, не уничтожит данные, когда они выйдут за рамки.
- Когда функция получает параметры, переменной типа String также предшествует амперсанд. Это указывает на то, что параметр для получения является строковым объектом ссылки.
Мы называем параметры в функции, которые получают ссылки, заимствованием. Как и в реальной жизни, когда я закончу свою домашнюю работу, я могу одолжить ее вам для копирования, но она вам не принадлежит, и вы должны вернуть ее мне после копирования. (Дружеское напоминание: не копируйте домашнюю работу, если только это не экстренный случай)
Кроме того, следует отметить, что моя домашняя работа может быть одолжена вам для копирования, но вы не можете изменить домашнюю работу, которую я написал.Я изначально написал ее правильно, а вы меня исправили.Как я могу одолжить ее вам в будущем? Следовательно, в calculate_length невозможно изменить s.
Изменяемые ссылки
Что, если я обнаружу, что допустил ошибку, и попрошу вас исправить ее для меня? Я разрешаю вам помочь вам изменить его, и вы также должны выразить, что вы можете помочь мне изменить его. У ржавчины тоже есть способ. Помните изменяемые и неизменяемые переменные, о которых мы говорили ранее? Ссылки аналогичны, мы можем использовать ключевое слово mut, чтобы сделать ссылку модифицируемой.
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Таким образом, мы можем изменить значение ссылки в функции. Однако здесь следует отметить, что в одной и той же области может быть только одна модифицируемая ссылка на одно и то же значение. Это также связано с тем, что Rust не хочет иметь одновременные изменения данных.
Если нам нужно использовать несколько модифицируемых ссылок, мы можем сами создать новые области видимости:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 超出作用域
let r2 = &mut s;
Другой конфликт — это «конфликт чтения-записи», ограничение между неизменяемыми и изменяемыми ссылками.
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{}, {}, and {}", r1, r2, r3);
Такой код также будет выдавать ошибку при компиляции. Это связано с тем, что неизменяемые ссылки не хотят, чтобы значение, на которое они указывают, изменялось перед использованием. Здесь немного обработки:
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// r1 和 r2 不再使用
let r3 = &mut s; // no problem
println!("{}", r3);
Компилятор Rust определит, что r1 и r2 больше не будут использоваться после первого оператора печати, в это время r3 не создан, и их области видимости не будут пересекаться. Так что этот код является законным.
нулевой указатель
Пожалуй, самая неприятная проблема для языков программирования, манипулирующих указателями, — это нулевой указатель. Обычно после освобождения памяти снова используется указатель на эту память. А компилятор Rust помогает нам избежать этой проблемы (еще раз спасибо компилятору Rust).
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
Взгляните на пример выше. В функции dangle возвращаемое значение является ссылкой на строку s. Но в конце функции память s была восстановлена. Таким образом, ссылка на s становится нулевым указателем. В этот момент будет сообщено об ошибке компиляции ожидаемого параметра времени жизни.
Еще цитата: Слайс
В дополнение к ссылкам существует еще один тип данных, не являющийся владельцем, который называется Slice. Срез — это ссылка, которая использует последовательность в коллекции.
Вот простой пример, иллюстрирующий использование Slice. Предположим, нам нужно получить первое слово в заданной вам строке. Что вы будете делать? На самом деле это очень просто, пройтись по каждому символу и, если встречается пробел, вернуть набор ранее пройденных символов.
Позволю себе испортить метод обхода строк, функция as_bytes умеет разлагать строки в байтовые массивы, iter — метод возврата каждого элемента в коллекции, enumerate — извлекать эти элементы, и возвращать (позиция элемента, значение элемента) такой бинарник метод. Можно ли это написать.
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
Приходите, почувствуйте этот пример, хотя он возвращает позицию первого пробела, пока строку можно перехватить, она все же может достичь цели. Однако перехват строки нельзя испортить, иначе проблема не будет раскрыта.
Где проблема так написана? Обратите внимание на основную функцию.
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear();
}
Здесь после получения пробела над строкой s выполняется четкая операция, т. е. s очищается. Но слово по-прежнему 5. В это время, если мы перехватим первые 5 символов s, возникнет проблема. Некоторые люди могут подумать, что они не такие глупые, но готовы ли вы поверить, что ваш хороший (жу) партнер (дуй) партнер (вы) не сделает того же? Я не верю. тогда что нам делать? В это время появится срез.
Используйте slice для получения последовательности символов из строки. Например&s[0..5]
Вы можете получить первые 5 символов строки s. Где 0 — индекс позиции начального символа, а 5 — индекс позиции конечного символа плюс 1. Другими словами, интервал среза является закрытым слева и открытым справа интервалом.
Есть также некоторые правила для срезов:
- Если начальная позиция равна 0, ее можно опустить. то есть
&s[0..2]
и&s[..2]
эквивалентность - Его также можно опустить, если начальная позиция является конечной позицией последовательности сбора. который
&s[3..len]
и&s[3..]
эквивалентность - Из двух предыдущих мы также можем получить
&s[0..len]
и&s[..]
эквивалентность
Здесь следует отметить, что когда мы перехватываем строку, ее границы должны быть символами UTF-8.
С помощью slice мы можем решить нашу проблему
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
Теперь, когда мы очищаем s в основной функции, компилятор не соглашается. Правильно, это универсальный компилятор.
В дополнение к тому, что ломтик может действовать на струнах, он также может действовать на других коллекциях, например:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
Что касается коллекции, у нас будет более подробные введения в будущем.
Суммировать
Функция владения, представленная в этой статье, очень важна для понимания Rust. Мы представляем, что такое право собственности, передача права собственности и типы данных Reference и Slice, которые не занимают право собственности.
Как насчет этого? Вы чувствуете, что сегодняшняя яма очень мощная? Если раньше он был на первом подвале, то теперь на третьем подвале. Поэтому, пожалуйста, обратите внимание на безопасность и приземляйтесь организованно.