Что такое владение?
Ownership is a set of rules that govern how a Rust program manages memory. All programs have to manage the way they use a computer’s memory while running. Some languages have garbage collection that regularly looks for no-longer-used memory as the program runs; in other languages, the programmer must explicitly allocate and free the memory. Rust uses a third approach: Memory is managed through a system of ownership with a set of rules that the compiler checks. If any of the rules are violated, the program won’t compile. None of the features of ownership will slow down your program while it’s running.
Поскольку идея владения незнакома большинству программистов, нужно некоторое время, чтобы выработался навык. Хорошая новость: чем больше опыта вы будете приобретать с Rust и правилами системы владения, тем легче вам будет разрабатывать безопасный и эффективный код. Выше нос!
Когда вы поймёте владение, вы получите устойчивый фундамент для понимания особенностей, которые делают Rust уникальным языком. В этой главе вы изучите владение, поработав над несколькими примерами с использованием одной из самых распространённых структур данных — строк.
Стек и куча
Многие языки программирования не требуют от вас задумываться о стеке или куче. Но в системных языках программирования (вроде Rust), важно знать разницу между размещением данных на стеке или на куче, знать о том как ведёт себя при этом язык и какие последствия повлечёт ваш выбор. Частично, владение будет рассмотрено в отношении стека, а под конец главы мы коснёмся и кучи; а пока, для подготовки, кратко расскажем о стеке и куче.
Both the stack and the heap are parts of memory available to your code to use at runtime, but they are structured in different ways. The stack stores values in the order it gets them and removes the values in the opposite order. This is referred to as last in, first out (LIFO). Think of a stack of plates: When you add more plates, you put them on top of the pile, and when you need a plate, you take one off the top. Adding or removing plates from the middle or bottom wouldn’t work as well! Adding data is called pushing onto the stack, and removing data is called popping off the stack. All data stored on the stack must have a known, fixed size. Data with an unknown size at compile time or a size that might change must be stored on the heap instead.
The heap is less organized: When you put data on the heap, you request a certain amount of space. The memory allocator finds an empty spot in the heap that is big enough, marks it as being in use, and returns a pointer, which is the address of that location. This process is called allocating on the heap and is sometimes abbreviated as just allocating (pushing values onto the stack is not considered allocating). Because the pointer to the heap is a known, fixed size, you can store the pointer on the stack, but when you want the actual data, you must follow the pointer. Think of being seated at a restaurant. When you enter, you state the number of people in your group, and the host finds an empty table that fits everyone and leads you there. If someone in your group comes late, they can ask where you’ve been seated to find you.
Размещение на стеке быстрее, чем аллокация в куче, поскольку при размещении на стеке распределителю памяти не приходится искать свободное место достаточного размера: таковое всегда находится на вершине стека. Соответственно, резмещение в куче требует куда больше работы, поскольку распределитель памяти должен сначала найти достаточно большой и неиспользуемый участок в памяти, а потом подготовиться к следующему запросу на выделение памяти.
Accessing data in the heap is generally slower than accessing data on the stack because you have to follow a pointer to get there. Contemporary processors are faster if they jump around less in memory. Continuing the analogy, consider a server at a restaurant taking orders from many tables. It’s most efficient to get all the orders at one table before moving on to the next table. Taking an order from table A, then an order from table B, then one from A again, and then one from B again would be a much slower process. By the same token, a processor can usually do its job better if it works on data that’s close to other data (as it is on the stack) rather than farther away (as it can be on the heap).
Когда ваш код вызывает функцию, значения, переданные функции (включая, например, указатели на данные в куче), и локальные переменные функции размещаются на стеке. Когда функция завершается, значения извлекаются из стека и их память освобождается.
Keeping track of what parts of code are using what data on the heap, minimizing the amount of duplicate data on the heap, and cleaning up unused data on the heap so that you don’t run out of space are all problems that ownership addresses. Once you understand ownership, you won’t need to think about the stack and the heap very often. But knowing that the main purpose of ownership is to manage heap data can help explain why it works the way it does.
Правила владения
Для начала, давайте посмотрим на правила владения. Держите их в голове по мере того, как мы будем показывать иллюстрирующие их примеры:
- Каждое значение в Rust имеет владельца.
- В один момент у значения может быть только один владелец.
- Когда владелец покидает свою область видимости, значение высвобождается.
Область видимости переменной
Now that we’re past basic Rust syntax, we won’t include all the fn main() { code in the examples, so if you’re following along, make sure to put the following examples inside a main function manually. As a result, our examples will be a bit more concise, letting us focus on the actual details rather than boilerplate code.
As a first example of ownership, we’ll look at the scope of some variables. A scope is the range within a program for which an item is valid. Take the following variable:
#![allow(unused)]
fn main() {
let s = "hello";
}
The variable s refers to a string literal, where the value of the string is hardcoded into the text of our program. The variable is valid from the point at which it’s declared until the end of the current scope. Listing 4-1 shows a program with comments annotating where the variable s would be valid.
fn main() {
{ // s is not valid here, since it's not yet declared
let s = "hello"; // отсюда и далее s действительна
// здесь можно использовать s
} // область видимости закончилась, s больше не действительна
}
В общем, здесь есть два важных момента:
- Когда
sоказывается в области видимости, она действительна. - Она остаётся действительной, пока не покинет область видимости.
Пока что отношения между областями видимости и действительностью переменных в целом такие же, как и в других языках программирования. Теперь, на этом фундаменте, мы рассмотрим тип String.
Тип String
Чтобы проиллюстрировать правила владения, нам нужен тип данных более сложный, чем те, что мы рассмотрели в разделе “Типы данных” Главы 3. Там мы рассматривали такие типы, которые имеют фиксированный размер; хранятся на стеке и высвобождаются с концом их области видимости; могут быть быстро и просто скопированы, чтобы получить отдельно живущую копию данных, которую можно затем использовать в другой области видимости. Но на этот раз нам нужны данные, которые придётся хранить в куче — чтобы узнать, как Rust выясняет момент для высвобождения этих данных. Тип String нам отлично подойдёт.
We’ll concentrate on the parts of String that relate to ownership. These aspects also apply to other complex data types, whether they are provided by the standard library or created by you. We’ll discuss non-ownership aspects of String in Chapter 8.
We’ve already seen string literals, where a string value is hardcoded into our program. String literals are convenient, but they aren’t suitable for every situation in which we may want to use text. One reason is that they’re immutable. Another is that not every string value can be known when we write our code: For example, what if we want to take user input and store it? It is for these situations that Rust has the String type. This type manages data allocated on the heap and as such is able to store an amount of text that is unknown to us at compile time. You can create a String from a string literal using the from function, like so:
#![allow(unused)]
fn main() {
let s = String::from("hello");
}
The double colon :: operator allows us to namespace this particular from function under the String type rather than using some sort of name like string_from. We’ll discuss this syntax more in the “Methods” section of Chapter 5, and when we talk about namespacing with modules in “Paths for Referring to an Item in the Module Tree” in Chapter 7.
Подобного рода строки могут быть изменены
fn main() {
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() приписывает литерал к String
println!("{s}"); // this will print `hello, world!`
}
В чем же разница? Почему строку String можно изменить, а литералы — нельзя? Разница заключается в том, как эти два типа работают с памятью.
Память и её выделение
В случае литерала строки, мы знаем его содержимое во время компиляции, так что оно будет явно прописано в итоговом исполняемом файле. Причина того, что строковые литералы более быстрые и эффективные, состоит в невозможности их изменять. К сожалению, в исполняемом файле нельзя определить кусок памяти переменного и неизвестного при компиляции размера, который к тому же может ещё и меняться во время исполнения программы.
Чтобы сделать возможным изменяемый, наращиваемый текст типа String, необходимо выделять память в куче для всего его содержимого, объём которого неизвестен во время компиляции. Это означает, что:
- Память должна запрашиваться у распределителя памяти во время исполнения программы.
- Необходим способ вернуть эту память распределителю, когда мы закончили работу с нашей
String.
That first part is done by us: When we call String::from, its implementation requests the memory it needs. This is pretty much universal in programming languages.
Однако второй пункт куда интереснее. В языках со сборщиком мусора (GC) память, которая больше не используется, им отслеживается и очищается — нам не нужно об этом думать. В большинстве языков без сборщика мусора мы обязаны сами определять, когда память больше не используется, и вызывать код, явно её освобождающий: точно так же, как мы делали это для её выделения. Правильные ручные запросы на выделение и высвобождение памяти всегда были сложной проблемой программирования. Если мы забудем освободить память, она будет потеряна и без проку забьёт собой место. Если же мы сделаем это слишком рано, у нас будет недействительная переменная. Сделать это дважды — тоже выйдут проблемы. Нам нужно ровно единожды высвободить память, которую мы единожды выделили.
Rust takes a different path: The memory is automatically returned once the variable that owns it goes out of scope. Here’s a version of our scope example from Listing 4-1 using a String instead of a string literal:
fn main() {
{
let s = String::from("hello"); // отсюда и далее s действительна
// здесь можно использовать s
} // область видимости закончилась,
// и s больше не действительна
}
There is a natural point at which we can return the memory our String needs to the allocator: when s goes out of scope. When a variable goes out of scope, Rust calls a special function for us. This function is called drop, and it’s where the author of String can put the code to return the memory. Rust calls drop automatically at the closing curly bracket.
Примечание: В C++ этот шаблон освобождения ресурсов в конце времени жизни данных иногда называется “Получение ресурса есть инициализация” (Resource Acquisition Is Initialization — RAII). Функция drop в Rust покажется вам знакомой, если вы использовали шаблоны RAII.
Этот шаблон оказывает глубокое влияние на способ написания кода в Rust. Сейчас это всё может казаться простым, но в более сложных случаях поведение кода может оказаться неожиданным: например, когда хочется иметь несколько переменных, использующих данные, выделенные в куче. Изучим несколько таких ситуаций.
Взаимодействие переменных и данных с помощью перемещения
Multiple variables can interact with the same data in different ways in Rust. Listing 4-2 shows an example using an integer.
fn main() {
let x = 5;
let y = x;
}
x to yWe can probably guess what this is doing: “Bind the value 5 to x; then, make a copy of the value in x and bind it to y.” We now have two variables, x and y, and both equal 5. This is indeed what is happening, because integers are simple values with a known, fixed size, and these two 5 values are pushed onto the stack.
Теперь посмотрим на версию с типом String
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}
This looks very similar, so we might assume that the way it works would be the same: That is, the second line would make a copy of the value in s1 and bind it to s2. But this isn’t quite what happens.
Взгляните на Рисунок 4-1, чтобы понять, что со String происходит под капотом. String состоит из трёх частей (показаны слева): указатель на память, в которой хранится содержимое строки; длина; ёмкость. Эта группа данных хранится на стеке. Справа — память в куче, которая содержит сам текст.
Figure 4-1: The representation in memory of a String holding the value "hello" bound to s1
Длина — это объём памяти в байтах, который в настоящее время использует содержимое String. Ёмкость — это общий объём памяти в байтах, который String получил от распределителя. Разница между длиной и ёмкостью имеет значение, но не в этом контексте, поэтому на данный момент можно игнорировать ёмкость.
Когда мы присваиваем s1 значению s2, данные String копируются: под этим имеется в виду, что мы копируем указатель, длину и ёмкость, которые находятся в стеке. Мы не копируем данные в куче, на которые указывает указатель. Иными словами, вид данных в памяти выглядит так, как показано на Рисунке 4-2.
Figure 4-2: The representation in memory of the variable s2 that has a copy of the pointer, length, and capacity of s1
Вид памяти не будет похож на Рисунок 4-3: так выглядела бы память, если бы вместо этого Rust также копировал данные кучи. Если бы Rust делал это, операция s2 = s1 могла бы быть очень дорогой с точки зрения производительности исполнения, если бы копируемые данные в куче были большими.
Рисунок 4-3: Другой вариант того, что могла бы делать операцияs2 = s1, если бы Rust также копировал данные кучи
Earlier, we said that when a variable goes out of scope, Rust automatically calls the drop function and cleans up the heap memory for that variable. But Figure 4-2 shows both data pointers pointing to the same location. This is a problem: When s2 and s1 go out of scope, they will both try to free the same memory. This is known as a double free error and is one of the memory safety bugs we mentioned previously. Freeing memory twice can lead to memory corruption, which can potentially lead to security vulnerabilities.
Чтобы обеспечить безопасность памяти, после строки let s2 = s1; Rust считает s1 более не инициализированной. Следовательно, Rust не нужно ничего освобождать, когда s1 выходит из области видимости. Посмотрите, что произойдёт, если вы попытаетесь использовать s1 после создания s2:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
}
Вы получите ошибку как ту, что ниже, поскольку Rust не позволит вам использовать недействительную ссылку:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:16
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{s1}, world!");
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Если вы слышали про термины поверхностное копирование и глубокое копирование, если работали с другими языками, концепция копирования указателя, длины и ёмкости без копирования данных, вероятно, звучит как создание поверхностной копии. Но поскольку Rust также деинициализирует первую переменную, это называется не поверхностным копированием, а перемещением. В примере выше мы бы сказали, что s1 была перемещён в s2. В конечном счёте, истинная картина происходящего показана на Рисунке 4-4.
Figure 4-4: The representation in memory after s1 has been invalidated
Это решает нашу проблему! Действительной остаётся только переменная s2. Когда она покинет область видимости, то лишь она одна будет освобождать память в куче.
Этот порядок работы с памятью даёт ещё одно преимущество: Rust никогда не будет автоматически создавать “глубокие” копии ваших данных. Следовательно, любое автоматическое копирование, связанное и перемещением, можно считать недорогим с точки зрения производительности.
Область видимости и присвоение
The inverse of this is true for the relationship between scoping, ownership, and memory being freed via the drop function as well. When you assign a completely new value to an existing variable, Rust will call drop and free the original value’s memory immediately. Consider this code, for example:
fn main() {
let mut s = String::from("hello");
s = String::from("ahoy");
println!("{s}, world!");
}
We initially declare a variable s and bind it to a String with the value "hello". Then, we immediately create a new String with the value "ahoy" and assign it to s. At this point, nothing is referring to the original value on the heap at all. Figure 4-5 illustrates the stack and heap data now:
Figure 4-5: The representation in memory after the initial value has been replaced in its entirety
Оригинальная строка, в связи с этим, в этот же момент покинула область видимости. Rust вызовет на ней функцию drop и высвободит её память. Когда исполнение дойдёт до строчки печати, будет выведено "ahoy, world!".
Взаимодействие переменных и данных с помощью клонирования
Если мы всё же хотим глубоко скопировать данные String в куче, а не только стека, мы можем использовать часто реализуемый метод, называемый clone. Мы обсудим синтаксис метода в Главе 5, но поскольку методы являются общей чертой многих языков программирования, вы, вероятно, уже знакомы с ними.
Вот пример работы метода clone:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {s1}, s2 = {s2}");
}
Это отлично работает и, очевидно, приводит к поведению, показанному на Рисунке 4-3, где данные кучи были скопированы.
Если вы видите вызов clone, вы сразу можете понять, что исполняемый здесь некоторый код наверняка будет затратным. В то же время, использование clone является маркером того, что тут происходит что-то необычное.
Данные, размещающиеся только на стеке, всегда копируются
Это ещё одна особенность, о которой мы ранее не говорили. Этот код, часть которого мы ранее показали в Листинге 4-2, полностью корректен:
fn main() {
let x = 5;
let y = x;
println!("x = {x}, y = {y}");
}
But this code seems to contradict what we just learned: We don’t have a call to clone, but x is still valid and wasn’t moved into y.
Причина в том, что такие типы, как целые числа, размер которых известен во время компиляции, полностью хранятся на стеке, поэтому копии фактических значений создаются быстро. Это означает, что нет причин, по которым мы хотели бы предотвратить доступность x после того, как создадим переменную y. Иными словами, для таких типов нет разницы между глубоким и поверхностным копированием, поэтому вызов clone ничем не отличается от обычного поверхностного копирования, и мы можем его опустить.
В Rust есть специальная аннотация, называемая трейтом Copy, которую мы можем приписывать типам, хранящимся на стеке: как это сделано для целых чисел (подробнее о трейтах мы поговорим в Главе 10). Если тип реализует трейт Copy, переменные, к нему принадлежащие, не перемещаются при присваивании, а просто копируются, что оставляет их действительными после присвоения другой переменной.
Rust не позволит нам аннотировать тип с помощью Copy, если тип или любая из его частей реализует трейт Drop. Если для типа нужно, чтобы произошло что-то особенное, когда значение выходит за пределы области видимости, и мы добавляем аннотацию Copy к этому типу, мы получим ошибку компиляции. Чтобы узнать, как добавить аннотацию Copy к вашему типу для реализации трейта, посмотрите раздел “Выводимые трейты” в Приложении C.
Какие же типы реализуют трейт Copy? Чтобы удостовериться, можно проверить документацию интересующего типа, но как правило любая группа простых отдельных значений может быть реализовывать Copy, и никакие типы, которые требуют выделения памяти в куче или являются некоторой формой ресурсов, не реализуют трейта Copy. Вот некоторые типы, которые реализуют Copy:
- Все целочисленные типы, такие как
u32. - Логический тип данных
bool, возможные значения которого —trueиfalse. - Все типы чисел с плавающей точкой, такие как
f64. - Символьный тип
char. - Кортежи; но только если они состоят только из типов, которые также реализуют
Copy. Например,(i32, i32)реализуетCopy, но кортеж(i32, String)— нет.
Владение и функции
Механизм передачи функции значения схож с тем, что происходит при присвоении переменной значения. Передача переменной в функцию приведёт к перемещению или копированию ровно так же, как при присваивании. В Листинге 4-3 есть пример с некоторыми комментариями, поясняющими, где переменные входят в область видимости и где выходят из неё.
fn main() {
let s = String::from("hello"); // s входит в область видимости
takes_ownership(s); // значение s перемещается в функцию...
// ... а потому оно здесь больше не доступно
let x = 5; // x входит в область видимости
makes_copy(x); // Because i32 implements the Copy trait,
// x does NOT move into the function,
// so it's okay to use x afterward.
} // Here, x goes out of scope, then s. However, because s's value was moved,
// nothing special happens.
fn takes_ownership(some_string: String) { // some_string входит в область видимости
println!("{some_string}");
} // Здесь some_string покидает область видимости и вызывается `drop`. Выделенная ей
// память высвобождается.
fn makes_copy(some_integer: i32) { // some_integer входит в область видимости
println!("{some_integer}");
} "// Здесь some_integer покидает область видимости. Ничего критического не происходит.
Если попытаться использовать s после вызова takes_ownership, Rust выбросит ошибку компиляции. Такие статические проверки защищают нас от опечаток. Попробуйте в main добавить код, который использует переменные s и x, чтобы увидеть, где их разрешено использовать и где правила владения предотвращают их использование.
Возвращение значений и область видимости
Возвращение значений также может сопровождаться передачей владения. В Листинге 4-4 показан пример функции, возвращающей некоторое значение, с подобными же комментариями, как в Листинге 4-3.
fn main() {
let s1 = gives_ownership(); // gives_ownership перемещает своё возвращаемое значение
// в s1
let s2 = String::from("hello"); // s2 входит в область видимости
let s3 = takes_and_gives_back(s2); // s2 перемещается в
// takes_and_gives_back, которая также
// перемещает в s3 своё возвращаемое значение
} // Здесь s3 покидает область видимости и её память высвобождается. s2 перемещена: ничего
// не происходит. s1 покидает область видимости и её память высвобождается.
fn gives_ownership() -> String { // gives_ownership перемещает своё
// возвращаемое значение в то,
// что вызвало её
let some_string = String::from("твоё"); // some_string входит в область видимости
some_string // some_string возвращается из функции и
// перемещается в то, что вызвало
// функцию
}
Эта функция принимает String и возвращает String.fn takes_and_gives_back(a_string: String) -> String {
// a_string входит в область
// видимости
a_string // a_string возвращается из функции перемещается в то, что вызвало функцию
}
The ownership of a variable follows the same pattern every time: Assigning a value to another variable moves it. When a variable that includes data on the heap goes out of scope, the value will be cleaned up by drop unless ownership of the data has been moved to another variable.
Хотя всё исправно работает, получать владение, а затем возвращать его из каждой функцией довольно утомительно. Что, если мы хотим, чтобы функция использовала значение, но не становилась владельцем? Очень раздражает, что всё, что мы передаём, также должно быть передано обратно, если мы хотим использовать это снова (помимо любых вычисленных в теле функции данных, которые мы также можем хотеть вернуть).
Rust позволяет нам возвращать из функции несколько значений, используя кортеж, как показано в Листинге 4-5.
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("Длина '{s2}' равна {len}.");
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() возвращает длину String
(s, length)
}
But this is too much ceremony and a lot of work for a concept that should be common. Luckily for us, Rust has a feature for using a value without transferring ownership: references.