ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1135
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
У значения может быть только один владелец в один момент времени,
Когда владелец покидает область видимости, значение удаляется.
Область видимости переменной
Теперь, когда мы прошли базовый синтаксис Rust, мы не будем включать весь код fn main() {
в примеры. Поэтому, если вы будете следовать этому курсу, убедитесь, что следующие примеры помещены в функцию main вручную. В результате наши примеры будут более лаконичными, что позволит нам сосредоточиться на реальных деталях, а не на шаблонном коде.
В качестве первого примера владения мы рассмотрим область видимости некоторых переменных. Область видимости — это диапазон внутри программы, для которого допустим элемент. Возьмём следующую переменную:
Переменная s
относится к строковому литералу, где значение строки жёстко прописано в тексте нашей программы. Переменная действительна с момента её объявления до конца текущей области видимости. В листинге 4-1 показана программа с комментариями, указывающими, где допустима переменная s
Листинг 4-1: переменная и область действия, в которой она допустима
Другими словами, здесь есть два важных момента:
Когда переменная s
появляется в области видимости, она считается действительной,
Она остаётся действительной до момента выхода за границы этой области.
На этом этапе объяснения взаимосвязь между областями видимости и допустимостью переменных аналогична той, что существует в других языках программирования. Теперь мы будем опираться на это понимание, введя тип
String
Тип данных String
Чтобы проиллюстрировать правила владения, нам нужен тип данных более сложный,
чем те, которые мы рассмотрели в разделе
«Типы данных»
главы 3. Все рассмотренные let s =
"hello"
;
{
// 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
ранее типы имеют известный размер, могут храниться в стеке и извлекаться из стека,
когда их область действия заканчивается. Также они могут быть быстро и легко скопированы для создания нового независимого экземпляра, если другая часть кода должна использовать то же значение в другой области видимости. Но мы хотим посмотреть на данные, хранящиеся в куче, и выяснить, как Rust узнает, когда нужно очистить эти данные, поэтому тип
String
— отличный пример.
Мы сконцентрируемся на тех частях
String
, которые связаны с владением. Эти аспекты также применимы к другим сложным типам данных, независимо от того, предоставлены они стандартной библиотекой или созданы вами. Более подробно мы обсудим
String в
главе 8
Мы уже видели строковые литералы, где строковое значение жёстко прописано в нашей программе. Строковые литералы удобны, но они подходят не для каждой ситуации, где мы можем хотеть использовать текст. Одна из причин заключается в том, что они неизменны. Кроме того, не каждое строковое значение может быть известно во время написания кода: что, если мы захотим принять и сохранить пользовательский ввод? Для таких ситуаций в Rust есть ещё один строковый тип —
String
. Этот тип управляет данными, выделенными в куче, и поэтому может хранить объём текста, который во время компиляции неизвестен. Также вы можете создать
String из строкового литерала,
используя функцию from
, например:
Оператор двойного двоеточия
::
позволяет нам использовать пространство имён функции from под типом
String
, вместо какого-то имени вроде string_from
. Мы обсудим этот синтаксис более подробно в разделе
«Синтаксис метода»
главы 5 и когда мы будем говорить о пространствах имён с модулями в
«Пути для обращения к элементу в дереве модулей»
в главе 7.
Строка такого типа может быть изменяема:
В чем здесь разница? Почему
String можно менять, а литерал — нельзя? Разница в том,
как эти два типа работают с памятью.
Память и способы её выделения
В случае строкового литерала мы знаем его содержимое во время компиляции, и оно жёстко прописано в итоговом исполняемом файле. Причина того, что строковые let s =
String
::from(
"hello"
); let mut s =
String
::from(
"hello"
); s.push_str(
", world!"
);
// push_str() appends a literal to a String println!
(
"{}"
, s);
// This will print `hello, world!`
когда их область действия заканчивается. Также они могут быть быстро и легко скопированы для создания нового независимого экземпляра, если другая часть кода должна использовать то же значение в другой области видимости. Но мы хотим посмотреть на данные, хранящиеся в куче, и выяснить, как Rust узнает, когда нужно очистить эти данные, поэтому тип
String
— отличный пример.
Мы сконцентрируемся на тех частях
String
, которые связаны с владением. Эти аспекты также применимы к другим сложным типам данных, независимо от того, предоставлены они стандартной библиотекой или созданы вами. Более подробно мы обсудим
String в
главе 8
Мы уже видели строковые литералы, где строковое значение жёстко прописано в нашей программе. Строковые литералы удобны, но они подходят не для каждой ситуации, где мы можем хотеть использовать текст. Одна из причин заключается в том, что они неизменны. Кроме того, не каждое строковое значение может быть известно во время написания кода: что, если мы захотим принять и сохранить пользовательский ввод? Для таких ситуаций в Rust есть ещё один строковый тип —
String
. Этот тип управляет данными, выделенными в куче, и поэтому может хранить объём текста, который во время компиляции неизвестен. Также вы можете создать
String из строкового литерала,
используя функцию from
, например:
Оператор двойного двоеточия
::
позволяет нам использовать пространство имён функции from под типом
String
, вместо какого-то имени вроде string_from
. Мы обсудим этот синтаксис более подробно в разделе
«Синтаксис метода»
главы 5 и когда мы будем говорить о пространствах имён с модулями в
«Пути для обращения к элементу в дереве модулей»
в главе 7.
Строка такого типа может быть изменяема:
В чем здесь разница? Почему
String можно менять, а литерал — нельзя? Разница в том,
как эти два типа работают с памятью.
Память и способы её выделения
В случае строкового литерала мы знаем его содержимое во время компиляции, и оно жёстко прописано в итоговом исполняемом файле. Причина того, что строковые let s =
String
::from(
"hello"
); let mut s =
String
::from(
"hello"
); s.push_str(
", world!"
);
// push_str() appends a literal to a String println!
(
"{}"
, s);
// This will print `hello, world!`
литералы более быстрые и эффективные, в их неизменяемости. К сожалению, нельзя поместить неопределённый кусок памяти в выполняемый файл для текста, размер которого неизвестен при компиляции и может меняться во время выполнения программы.
Чтобы поддерживать изменяемый, увеличивающийся текст типа
String
, необходимо выделять память в куче для всего содержимого, объем которого неизвестен во время компиляции. Это означает, что:
Память должна запрашиваться у операционной системы во время выполнения программы,
Необходим способ возврата этой памяти операционной системе, когда мы закончили в программе работу со
String
Первая часть выполняется нами: когда мы вызываем
String::from
, его реализация запрашивает необходимую память. Это работает довольно похоже во всех языках программирования.
Однако вторая часть отличается. В языках со сборщиком мусора (GC), память, которая больше не используется, отслеживается и очищается с его помощью — нам не нужно об этом думать. В большинстве языков без сборщика мусора мы обязаны сами определять,
когда память больше не используется, и вызывать код для явного её освобождения,
точно так же, как мы делали это для её запроса. Правильное выполнение этого процесса исторически было сложной проблемой программирования. Если мы забудем освободить память, она будет потеряна. Если мы сделаем это слишком рано, у нас будет недопустимая переменная. Сделать это дважды тоже будет ошибкой. Нам нужно соединить ровно один allocate ровно с одним free
Rust выбирает другой путь: память автоматически возвращается, как только владеющая памятью переменная выходит из области видимости. Вот версия примера с областью видимости из листинга 4-1, в котором используется тип
String вместо строкового литерала:
Существует естественный момент, когда мы можем вернуть память, необходимую нашему
String
, обратно распределителю — когда s
выходит за пределы области видимости. Когда переменная выходит за пределы области видимости, Rust вызывает для нас специальную функцию. Эта функция называется drop
, и именно здесь автор
String может поместить код для возврата памяти. Rust автоматически вызывает 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
Чтобы поддерживать изменяемый, увеличивающийся текст типа
String
, необходимо выделять память в куче для всего содержимого, объем которого неизвестен во время компиляции. Это означает, что:
Память должна запрашиваться у операционной системы во время выполнения программы,
Необходим способ возврата этой памяти операционной системе, когда мы закончили в программе работу со
String
Первая часть выполняется нами: когда мы вызываем
String::from
, его реализация запрашивает необходимую память. Это работает довольно похоже во всех языках программирования.
Однако вторая часть отличается. В языках со сборщиком мусора (GC), память, которая больше не используется, отслеживается и очищается с его помощью — нам не нужно об этом думать. В большинстве языков без сборщика мусора мы обязаны сами определять,
когда память больше не используется, и вызывать код для явного её освобождения,
точно так же, как мы делали это для её запроса. Правильное выполнение этого процесса исторически было сложной проблемой программирования. Если мы забудем освободить память, она будет потеряна. Если мы сделаем это слишком рано, у нас будет недопустимая переменная. Сделать это дважды тоже будет ошибкой. Нам нужно соединить ровно один allocate ровно с одним free
Rust выбирает другой путь: память автоматически возвращается, как только владеющая памятью переменная выходит из области видимости. Вот версия примера с областью видимости из листинга 4-1, в котором используется тип
String вместо строкового литерала:
Существует естественный момент, когда мы можем вернуть память, необходимую нашему
String
, обратно распределителю — когда s
выходит за пределы области видимости. Когда переменная выходит за пределы области видимости, Rust вызывает для нас специальную функцию. Эта функция называется drop
, и именно здесь автор
String может поместить код для возврата памяти. Rust автоматически вызывает 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
Примечание: в C++ этот паттерн освобождения ресурсов в конце времени жизни элемента иногда называется «Получение ресурса есть инициализация» (англ. Resource
Acquisition Is Initialization (RAII)). Функция drop в Rust покажется вам знакомой, если вы использовали шаблоны RAII.
Этот шаблон оказывает глубокое влияние на способ написания кода в Rust. Сейчас это может казаться простым, но в более сложных ситуациях поведение кода может быть неожиданным, например когда хочется иметь несколько переменных, использующих данные, выделенные в куче. Изучим несколько таких ситуаций.
1 2 3 4 5 6 7 8 9 10 ... 62
Способы взаимодействия переменных и данных: перемещение
Несколько переменных могут по-разному взаимодействовать с одними и теми же данными в Rust. Давайте рассмотрим пример использования целого числа в листинге 4-
2.
Листинг 4-2: присвоение целочисленного значения переменной
x
к переменной
y
Мы можем догадаться, что делает этот код: «привязать значение
5
к x
; затем сделать копию значения в x
и привязать его к y
». Теперь у нас есть две переменные: x
и y
, и обе равны
5
. Это то, что происходит на самом деле, потому что целые числа — это простые значения с известным фиксированным размером, и эти два значения
5
помещаются в стек.
Теперь рассмотрим версию с типом
String
:
Это выглядит очень похоже, поэтому мы можем предположить, что происходит то же самое: вторая строка сделает копию значения в s1
и привяжет его к s2
. Но это не совсем так.
Взгляните на рисунок 4-1, чтобы увидеть, что происходит со
String под капотом.
String состоит из трёх частей, показанных слева: указатель на память, в которой хранится содержимое строки, длина и ёмкость. Эта группа данных хранится в стеке. Справа —
память в куче, которая хранит содержимое.
let x =
5
; let y = x; let s1 =
String
::from(
"hello"
); let s2 = s1;
s1
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o
Рисунок 4-1: представление в памяти
String
, содержащей значение
"hello"
, привязанное к
s1
Длина — это количество байт памяти, которое использует содержимое
String в данный момент. Ёмкость — это общее количество байт памяти, которое
String получил от операционной системы. Разница между длиной и ёмкостью имеет значение, но не в данном контексте — сейчас можно игнорировать ёмкость.
Когда мы присваиваем s1
значение s2
, данные
String копируются, то есть мы копируем указатель, длину и ёмкость, которые находятся в стеке. Мы не копируем данные в куче, на которую указывает указатель. Другими словами, представление данных в памяти выглядит так, как показано на рисунке 4-2.
s1
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o s2
name value ptr len
5
capacity
5
Рисунок 4-2: представление в памяти переменной
s2
, имеющей копию указателя, длины и ёмкости
s1
Представление не похоже на рисунок 4-3, как выглядела бы память, если бы вместо этого
Rust также скопировал данные кучи. Если бы Rust сделал это, операция s2 = s1
могла бы быть очень дорогой с точки зрения производительности во время выполнения, если бы данные в куче были большими.
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o
Рисунок 4-1: представление в памяти
String
, содержащей значение
"hello"
, привязанное к
s1
Длина — это количество байт памяти, которое использует содержимое
String в данный момент. Ёмкость — это общее количество байт памяти, которое
String получил от операционной системы. Разница между длиной и ёмкостью имеет значение, но не в данном контексте — сейчас можно игнорировать ёмкость.
Когда мы присваиваем s1
значение s2
, данные
String копируются, то есть мы копируем указатель, длину и ёмкость, которые находятся в стеке. Мы не копируем данные в куче, на которую указывает указатель. Другими словами, представление данных в памяти выглядит так, как показано на рисунке 4-2.
s1
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o s2
name value ptr len
5
capacity
5
Рисунок 4-2: представление в памяти переменной
s2
, имеющей копию указателя, длины и ёмкости
s1
Представление не похоже на рисунок 4-3, как выглядела бы память, если бы вместо этого
Rust также скопировал данные кучи. Если бы Rust сделал это, операция s2 = s1
могла бы быть очень дорогой с точки зрения производительности во время выполнения, если бы данные в куче были большими.
s2
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o s1
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o
Рисунок 4-3: другой вариант того, что может сделать
s2 = s1
, если Rust также скопирует данные кучи
Ранее мы сказали, что когда переменная выходит из области видимости, Rust автоматически вызывает функцию drop и очищает память в куче для данной переменной. Но картинка 4-2 показывает, что теперь оба указателя указывают на одно и тоже место. Это проблема: когда переменная s2
и переменная s1
выходят из области видимости, они обе будут пытаться освободить одну и ту же память в куче. Это известно как «ошибка двойного освобождения» (double free), и является одной из ошибок безопасности памяти, упоминаемых ранее. Освобождение памяти дважды может привести к повреждению памяти, что потенциально может привести к уязвимостям безопасности.
Чтобы обеспечить безопасность памяти, после строки let s2 = s1
Rust считает s1
более недействительным. Следовательно, Rust не нужно ничего освобождать, когда s1
выходит за пределы области видимости. Посмотрите, что происходит, когда вы пытаетесь использовать s1
после создания s2
; это не сработает:
Вы получите похожую ошибку, потому что Rust не даст использовать недействительную ссылку s1
:
let s1 =
String
::from(
"hello"
); let s2 = s1; println!
(
"{}, world!"
, s1);
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o s1
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o
Рисунок 4-3: другой вариант того, что может сделать
s2 = s1
, если Rust также скопирует данные кучи
Ранее мы сказали, что когда переменная выходит из области видимости, Rust автоматически вызывает функцию drop и очищает память в куче для данной переменной. Но картинка 4-2 показывает, что теперь оба указателя указывают на одно и тоже место. Это проблема: когда переменная s2
и переменная s1
выходят из области видимости, они обе будут пытаться освободить одну и ту же память в куче. Это известно как «ошибка двойного освобождения» (double free), и является одной из ошибок безопасности памяти, упоминаемых ранее. Освобождение памяти дважды может привести к повреждению памяти, что потенциально может привести к уязвимостям безопасности.
Чтобы обеспечить безопасность памяти, после строки let s2 = s1
Rust считает s1
более недействительным. Следовательно, Rust не нужно ничего освобождать, когда s1
выходит за пределы области видимости. Посмотрите, что происходит, когда вы пытаетесь использовать s1
после создания s2
; это не сработает:
Вы получите похожую ошибку, потому что Rust не даст использовать недействительную ссылку s1
:
let s1 =
String
::from(
"hello"
); let s2 = s1; println!
(
"{}, world!"
, s1);
Если вы слышали термины поверхностное копирование и глубокое копирование при работе с другими языками, концепция копирования указателя, длины и ёмкости без копирования данных, вероятно, звучит как создание поверхностной копии. Но поскольку
Rust также аннулирует первую переменную, вместо того, чтобы называть её
поверхностной копией, это называется перемещением. В этом примере мы бы сказали,
что s1
был перемещён в s2
. Что происходит на самом деле, показано на рисунке 4-4.
s1
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o s2
name value ptr len
5
capacity
5
Рисунок 4-4: представление в памяти после того, как
s1
был признан недействительным
Это решает нашу проблему! Действительной остаётся только переменная s2
. Когда она выходит из области видимости, то она одна будет освобождать память в куче.
$
cargo run
Compiling ownership v0.1.0 (file:///projects/ownership) error[E0382]: borrow of moved value: `s1`
-->
src/main.rs:5:28
|
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!("{}, world!", s1);
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`. error: could not compile `ownership` due to previous error
Дополнительно присутствует дизайнерский выбор, который подразумевает следующее:
Rust никогда не будет автоматически создавать «глубокие» копии ваших данных.
Следовательно любое такое автоматическое копирование можно считать недорогим с точки зрения производительности во время выполнения.
Способы взаимодействия переменных и данных: клонирование
Если мы хотим глубоко скопировать данные кучи
String
, а не только данные стека, мы можем использовать общий метод, называемый clone
. Мы обсудим синтаксис методов в главе 5, но поскольку методы являются общей чертой многих языков программирования, вы, вероятно, уже встречались с ними.
Вот пример работы метода clone
:
Код работает отлично и явно выполняет поведение, показанное на картинке 4-3, где данные в куче действительно скопированы.
Когда вы видите вызов clone
, вы знаете о выполнении некоторого кода, который может быть дорогим. В то же время использование clone является визуальным индикатором того, что тут происходит что-то нестандартное (глубокое копирование вместо обыденного перемещения).
Стековые данные: копирование
Это ещё одна особенность о которой мы ранее не говорили. Этот код, часть которого была показа ранее в листинге 4-2, использует целые числа. Он работает без ошибок:
Но это, кажется, противоречит тому, что мы только что изучили: тут не нужно вызывать clone
, но x
является все ещё действительной переменной и не перемещена в y
Причина в том, что такие типы, как целые числа, размер которых известен во время компиляции, полностью хранятся в стеке, поэтому копии фактических значений создаются быстро. Это означает, что нет причин, по которым мы хотели бы предотвратить допустимость x
после того, как создадим переменную y
. Другими словами, здесь нет разницы между глубоким и поверхностным копированием, поэтому вызов clone ничем не отличается от обычного поверхностного копирования, и мы можем его опустить.
let s1 =
String
::from(
"hello"
); let s2 = s1.clone(); println!
(
"s1 = {}, s2 = {}"
, s1, s2); let x =
5
; let y = x; println!
(
"x = {}, y = {}"
, x, y);
В Rust есть специальная аннотация, называемая типажом
Copy
, которую мы можем размещать на типах, хранящихся в стеке, как и целые числа (подробнее о типах мы поговорим в главе 10
). Если тип реализует типаж
Copy
, переменные, которые его используют, не перемещаются, а тривиально копируются, что делает их действительными после присвоения другой переменной.
Rust не позволит нам аннотировать тип с помощью
Copy
, если тип или любая из его частей реализует
Drop
. Если для типа нужно, чтобы произошло что-то особенное, когда значение выходит за пределы области видимости, и мы добавляем аннотацию
Copy к
этому типу, мы получим ошибку времени компиляции. Чтобы узнать, как добавить аннотацию
Copy к вашему типу для реализации типажа, смотрите раздел «Производные типажи»
в приложении С.
Но какие же типы имеют типаж
Copy
? Можно проверить документацию любого типа для уверенности, но как правило любая группа простых скалярных значений может быть с типажом
Copy
, и ничего из типов, которые требуют выделения памяти в куче или являются некоторой формой ресурсов, не имеет типажа
Copy
. Вот некоторые типы,
которые реализуют типаж
Copy
:
Все целочисленные типы, такие как u32
,
Логический тип данных bool
, возможные значения которого true и false
,
Все числа с плавающей запятой, такие как f64
,
Символьный тип char
,
Кортежи, но только если они содержат типы, которые также реализуют
Copy
Например,
(i32, i32)
будет с
Copy
, но кортеж
(i32, String)
уже нет.
Владение и функции
Механика передачи значения функции аналогична тому, что происходит при присвоении значения переменной. Передача переменной в функцию приведёт к перемещению или копированию, как и присваивание. В листинге 4-3 есть пример с некоторыми аннотациями, показывающими, где переменные входят в область видимости и выходят из неё.
Файл: src/main.rs