Автор: Сяо Мэн; позже редактор: Гао Сяньфэн.
Об авторе: Сяо Мэн, 20-летний опыт работы в области архитектуры программного обеспечения от настольных компьютеров до облачных и встроенных, охватывающих несколько отраслей связи, игр, финансов и интеллектуальных подключенных транспортных средств, эксперт по системному анализу предметной области, эксперт по архитектуре программного обеспечения с полным стеком. В настоящее время работаю над разработкой базового программного обеспечения для интеллектуального вождения. Он работал директором программной платформы Geely Yigatong для автономного вождения, а также директором по исследованиям и разработкам программного обеспечения автономного вождения China Auto Intelligent Control. Сильный интерес к продвижению стека Rust в автомобильной сфере с реальной практикой массового производства.
1. Введение
право собственностиижизненный циклДа Rust
Язык является очень основным содержанием. На самом деле не толькоRust
С помощью этих двух понятий вC/C++
также существует в . И почти все проблемы с безопасностью памяти также возникают из-за неправильного использования прав собственности и времени жизни. Эта проблема существует до тех пор, пока язык программирования не использует сборку мусора для управления памятью. ТолькоRust
Эти две концепции разъясняются на уровне языка, и предоставляются связанные языковые функции, позволяющие пользователям явно управлять передачей права собственности и декларацией жизненного цикла. В то же время компилятор проверит различные варианты неправильного использования, что улучшит безопасность памяти программы.
Собственность и жизненный цикл включают в себя множество языковых концепций. В этой статье в основном разбираются концепции, связанные с «собственностью иUML
Диаграмма классов выражает отношения между понятиями, чтобы помочь лучше понять и освоить.
иллюстрация
Прилагаемые чертежиUML
Диаграмма классов,UML
Диаграммы классов можно использовать для представления анализа понятий. Выразите зависимости, наследование, агрегирование, композицию и другие отношения между понятиями. Каждый прямоугольник на рисунке представляет собой семантическое понятие, некоторые из них являются абстрактными языковыми понятиями, некоторые являютсяRust
структуры в библиотеке иTrait
.
Символы, используемые на всех рисунках, являются лишь самыми основными. Рисунок 1 кратко поясняет систему символов, в основном объясняя символический язык, выражающий отношение между понятиями.
Рис. 1. Нотация UMLЗависимости:
ЗависимостьUML
Самая основная реляционная семантика в . Обозначается пунктирной линией со стрелкой,A
зависит отB
Экспресс, как показано ниже. Интуитивное понимание может бытьA
"вижу-вижу"B
,иB
даA
Ничего не знаю. Например, в структуре кодаA
со структуройB
переменная-член , илиA
Код реализации имеетB
локальных переменных. Так что, если вы не можете найти егоB
,A
не может быть скомпилирован.
отношение соединения:
Сплошная соединяющая линия означает, что два типа напрямую связаны, стрелка означает одностороннюю «видимость», а отсутствие стрелки означает, что они видны друг другу. Ассоциация — это тоже зависимость, но более конкретная. Иногда ассоциация между двумя типами слишком сложна и должна быть выражена типом, называемым типом ассоциации, как в примере на рисунке.H
.
Комплектация и состав:
Агрегация и композиция представляют отношения между целым и частями. Разница в том, что «агрегированные» целые могут быть отделены от частей, а части могут быть разделены между несколькими целыми. В отношении «композиция» целое имеет более сильную исключительность по отношению к части, часть не может быть разобрана, и часть имеет тот же жизненный цикл, что и целое.
Наследование и реализация интерфейса:
Наследование и реализация интерфейса являются отношениями обобщения.C
унаследовано отA
,ВыражатьA
является более общим понятием.UML
Различные реляционные семантики могут также использоваться вUML
Чтобы выразить себя, как показано на рисунке 2: «Ассоциация» и «Наследование» являются конкретными проявлениями «зависимости».
Генеральный план
Рисунок 3 представляет собой общую картину этой статьи, а последующие разделы представлены подробно.
Рис. 3. Владение Rust и обзор жизненного цикла2. Право собственности и предполагаемые проблемы жизненного цикла, которые необходимо решить
Начиная с середины рисунка, так называемое «владение» означает наличие «области памяти» для переменной. Эта область памяти может находиться в куче, в стеке или в сегменте кода, а некоторые адреса памяти напрямую используются дляI/O
сопоставление адресов. Это все возможные расположения областей памяти.
В высокоуровневых языках доступ к этой памяти можно получить там, где вы хотите быть в программе, это неизбежно будет строить отношения с одной или несколькими переменными (низкоуровневые языки, такие как ассемблер, прямой доступ к адресу памяти). Другими словами, через какую одну или несколько переменных можно получить доступ к этому адресу памяти.
Это приводит к трем вопросам:
- Проблемы безопасности памяти, вызванные неправильным доступом к памяти
- Проблемы согласованности данных, вызванные несколькими переменными, указывающими на одну и ту же область памяти
- Гонки данных, вызванные передачей переменных между несколькими потоками
Существует пять типичных случаев проблем с безопасностью памяти, вызванных первой проблемой:
- использовать неинициализированную память
- Разыменование нулевого указателя
- Висячие указатели (с использованием памяти, которая уже была освобождена)
- переполнение буфера
- Недопустимое освобождение памяти (освобождение нераспределенного указателя или многократное освобождение указателя)
Эти проблемыC/C++
В середине разработчики должны быть очень осторожны, чтобы справиться с этим самостоятельно. Например, мы можем написать абзацC++
кода, совершите все пять ошибок безопасности памяти.
#include <iostream>
struct Point {
int x;
int y;
};
Point* newPoint(int x,int y) {
Point p { .x=x,.y=y };
return &p; //悬垂指针
}
int main() {
int values[3]= { 1,2,3 };
std::cout<<values[0]<<","<<values[3]<<std::endl; //缓冲区溢出
Point *p1 = (Point*)malloc(sizeof(Point));
std::cout<<p1->x<<","<<p1->y<<std::endl; //使用未初始化内存
Point *p2 = newPoint(10,10); //悬垂指针
delete p2; //非法释放内存
p1 = NULL;
std::cout<<p1->x<<std::endl; //对空指针解引用
return 0;
}
Этот код можно скомпилировать, конечно, компилятор все равно выдаст предупреждающее сообщение. Этот код также может выполняться, а также будет выводить информацию и не будет segfault, пока выполнение не достигнет последней ошибки «при разыменовании нулевого указателя».
Rust
Языковые функции , обеспечивают решение вышеуказанных проблем, как показано в следующей таблице:
проблема | решение |
---|---|
использовать неинициализированную память |
Компилятор запрещает переменным читать непредсказуемые переменные |
Разыменование нулевого указателя |
Используйте перечисления Option вместо нулевых указателей |
оборванный указатель |
Идентификаторы жизненного цикла и проверка компилятора |
переполнение буфера |
Компилятор проверяет, чтобы запретить доступ к данным за пределами буфера |
Недопустимая свободная память |
Механизм RAII на уровне языка, только единственный владелец имеет право на освобождение памяти |
Несколько переменных изменяют одну и ту же область памяти |
Множественным переменным разрешено заимствовать владение, но одновременно разрешено заимствование только одной переменной. |
Проблемы безопасности, когда переменные передаются в несколько потоков |
Два признака Sync и Send используются для базовых типов данных, чтобы определить их характеристики потокобезопасности, то есть можно ли передать владение или передать заимствование переменных, и это считается основным фактом. Затем используйте общий синтаксис квалификации и синтаксис реализации типажей, чтобы описать правила безопасности типов потоков. Во время компиляции используется механизм, похожий на обработчик правил, для проверки логического вывода для передачи данных между потоками в пользовательском коде на основе достоверных данных и предопределенных правил. |
3. Привязка переменных и присвоение права собственности
Rust
Почему это называется «связывание переменных», а не «назначение переменных». Давайте сначала посмотримC++
код и соответствующийRust
код.
C++:
#include <iostream>
int main()
{
int a = 1;
std::cout << &a << std::endl; /* 输出 0x62fe1c */
a = 2;
std::cout << &a << std::endl; /* 输出 0x62fe1c */
}
Rust:
fn main() {
let a = 1;
println!("a:{}",a); // 输出1
println!("&a:{:p}",&a); // 输出0x9cf974
//a=2; // 编译错误,不可变绑定不能修改绑定的值
let a = 2; // 重新绑定
println!("&a:{:p}",&a); // 输出0x9cfa14地址发生了变化
let mut b = 1; // 创建可变绑定
println!("b:{}",b); // 输出1
println!("&b:{:p}",&b); // 输出0x9cfa6c
b = 2;
println!("b:{}",b); // 输出2
println!("&b:{:p}",&b); // 输出0x9cfa6c地址没有变化
let b = 2; // 重新绑定新值
println!("&b:{:p}",&b); // 输出0x9cfba4地址发生了变化
}
Мы можем видеть это вC++
код, переменнаяa
Сначала ему присваивается значение 1, а затем присваивается значение 2, но его адрес не изменился.Rust
в коде,a
является неизменяемой привязкой, которая выполняетсяa=2
Действие отклонено компилятором. Но вы можете использоватьlet
пересвязать, но на этот разa
Адрес a изменился по сравнению с предыдущим, что указывает на то, что a привязан к другому адресу памяти.b
является изменяемой привязкой, вы можете использоватьb = 2
переназначить память, на которую он указывает,b
Адрес без изменений. Но используйтеlet
После перепривязки,b
указывает на новую область памяти.
Видно, что «присваивание» — это запись значения в область памяти, связанную с переменной, «связывание» — это установление связи между переменной и областью памяти,Rust
, также присваивает этой переменной владение этой областью памяти.
Смысл неизменяемой привязки: привязать переменную к адресу памяти и передать право собственности, только данные этого адреса могут быть прочитаны через переменную, и данные этого адреса не могут быть изменены. Соответственно, привязка переменной может изменять данные связанной области памяти через переменную. Грамматически существуютlet
Ключевое слово является обязательным, а не назначением.
Здесь мы можем видетьRust
иC++
разница.C++
Нет понятия "привязка".Rust
Концепция связывания переменных является ключевой концепцией, это отправная точка владения. Только при четкой привязке право собственности может быть атрибутировано, а время отмены привязки также определяет время освобождения ресурса.
Правила владения:
- Каждое значение имеет свою переменную-владельца
- Одновременно может быть только одна переменная владельца
- Владелец выходит за рамки, значение отбрасывается (освобождается/уничтожается)
Как владелец, он имеет следующие права:
- Контролируйте высвобождение ресурсов
- Одолжить название
- передача права собственности
4. Передача права собственности
Одним из важных прав собственника является «передача права собственности». Это вызывает три вопроса:
- Зачем передавать?
- Когда переводить?
- Как передать?
Связанные концепции языка показаны на рисунке ниже.
Рисунок 5. Передача права собственностиЗачем передавать право собственности?
Мы знаем, что переменные в C/C++/Rust связаны с определенной областью памяти, но переменные всегда обрабатываются в выражениях, а затем присваиваются другой переменной или передаются между функциями. На самом деле ожидается, что будет передано содержимое области памяти, к которой привязана переменная.Если эта область памяти относительно велика, копирование данных памяти в новую переменную является дорогостоящей операцией. Таким образом, право собственности должно быть передано новой переменной, а текущая переменная отказывается от владения. Таким образом, в конце концов, передача права собственности по-прежнему связана с производительностью.
Сроки перехода права собственности можно суммировать в следующих двух ситуациях:
- передать право собственности, когда позиционное выражение появляется в контексте значения
- Передача права собственности при передаче переменных между областями
Первое правило — это точное академическое выражение, включающее лингвистические понятия, такие как позиционные выражения, выражения значения, позиционный контекст, контекст значения и т. д. Его простое понимание заключается в разнообразии поведения присваивания. Выражение, однозначно указывающее на место в области памяти, является позиционным выражением, а все остальное — выражением значения. Различные операции с семантикой присваивания имеют позиционный контекст слева и контекст значения справа.
Когда позиционное выражение появляется в контексте значения, его программная семантика заключается в присвоении данных, на которые указывает позиционное выражение, новой переменной, и право собственности передается.
Второе правило — «Передача права собственности, когда переменные пересекают области действия».
На рисунке перечислены несколько распространенных вариантов поведения в разных областях, которые могут охватывать большинство ситуаций, а также простой пример кода.
- Переменные используются внутри фигурных скобок
- совпадение
- если пусть и пока пусть
- Переместить передачу параметров семантической функции
- Замыкания захватывают семантические переменные перемещения
- Переменные возвращаются изнутри функции
Почему переменные передают право собственности между областями?
существуетC/C++
В коде, передавать ли право собственности, неявно или явно указывает сам программист.
Вы только представьте, вC/C++
код, функцияFun1
Создать тип в стекеA
экземплярa
, возьмите его указатель&a
передается в функциюvoid fun2(A* param)
мы бы не надеялисьfun2
освободить эту память, потому чтоfun1
При возврате место в стеке автоматически освобождается.
еслиfun1
создать в кучеA
экземплярa
, возьмите его указатель&a
передается в функциюfun2(A* param)
, тогда оa
освобождение памяти,fun1
иfun2
Должны быть переговоры между тем, кто выпустит его.fun1
можно ожидать отfun2
освободить, если поfun2
отпустите, затемfun2
Невозможно сказать, находится ли указатель в куче или в стеке. Ведь кто владеетa
Указание на принадлежность области памяти.C/C++
На уровне языка нет принудительных ограничений.fun2
При разработке функции необходимо сделать предположения о контексте, в котором она вызывается, и договориться в документе о том, кто будет освобождать память этой переменной. Это затрудняет для компилятора фактическое предупреждение о неправильном использовании.
Rust
Когда требуется, чтобы переменные явно передавали право собственности при пересечении области видимости, компилятор может четко знать, какая переменная имеет право собственности внутри и вне границ области видимости, и может выполнять четкие и недвусмысленные проверки на предмет незаконного использования переменных, что повышает безопасность код.
Есть два способа передачи владения:
- Переместить семантику - выполнение передачи владения
- семантика копирования - переход не выполняется, только переменная копируется побитно
Здесь я определяю «семантику копирования» как один из методов передачи права собственности, то есть «непередача» также является методом передачи. Это выглядит странно. На самом деле логика непротиворечива, потому что время запуска копирования совпадает с временем запуска передачи. Просто этот тип данных помеченCopy
Этикеткаtrait
, вместо этого компилятор выполняет побитовое копирование, когда необходимо выполнить переход.
Rust
реализован в стандартной библиотеке для всех примитивных типовCopy Trait
.
Здесь следует отметить, что в стандартной библиотеке
impl<T: ?Sized> Copy for &T {}
реализовано для всех типов ссылокCopy
, что означает, что когда мы вызываем функцию со ссылочным параметром, сама ссылочная переменная копируется побитно. Стандартная библиотека не заимствует для изменчивых&mut T
Реализовать «Копировать»Trait
, потому что может быть только одно изменяемое заимствование. Мы увидим примеры позже, когда будем говорить о замыканиях, фиксирующих владение переменными.
5. Заимствование титула
Переменная имеет право собственности на область памяти, и одним из прав ее владельца является «предоставление права собственности».
Концептуальные отношения, связанные с владением кредитом, показаны на рисунке 6.
Рисунок 6 Название кредитаПеременная, у которой есть право собственности, предоставляет право собственности двумя способами: «ссылка» и «умный указатель»:
-
Ссылки (включая изменяемые заимствования и неизменяемые заимствования)
-
умный указатель
- Эксклюзивная умная указка
Box<T>
- Не потокобезопасные интеллектуальные указатели с подсчетом ссылок
Rc<T>
- Поточно-ориентированные интеллектуальные указатели с подсчетом ссылок
Arc<T>
- слабый указатель
Weak<T>
- Эксклюзивная умная указка
Ссылки на самом деле также являются указателями, указывающими на фактические ячейки памяти.
Есть два важных правила безопасности при заимствовании:
- Представляет заимствованную переменную, чья всю жизнь не может быть длиннее срока службы заимствованной переменной (владельца)
- Может быть только одно изменяемое заимствование одной и той же переменной
Первое правило состоит в том, чтобы убедиться, что нет проблем с безопасностью памяти, связанных с "висячими указателями". Если это правило нарушено, например: переменнаяa
имеет право собственности на область хранения, переменныеb
даa
какая-то заимствованная форма , еслиb
коэффициент жизненного циклаa
долго, тоa
После уничтожения место для хранения освобождается, иb
можно еще использовать, тоb
становится висячим указателем.
Во-вторых, два изменяемых заимствования не допускаются во избежание проблем с согласованностью данных.
Struct Foo{v:i32}
fn main(){
let mut f = Foo{v:10};
let im_ref = &f; // 获取不可变引用
let mut_ref = & mut f; // 获取可变引用
//println!("{}",f.v);
//println!("{}",im_ref.v);
//println!("{}",mut_ref.v);
}
Переменнаяf
Стоимость владения,im_ref
Что является непреложным заимствованием,mut_ref
является его изменяемым заимствованием. Приведенный выше код можно скомпилировать, но эти переменные не используются, в этом случае компилятор не запрещает вам иметь как изменяемое, так и неизменяемое заимствование. Последние три закомментированные строки кода (6,7,8) используют эти переменные. Откройте одну или несколько строк кода с этими комментариями, и компилятор сообщит об ошибках разной формы:
открыть строку комментариев | отчет компилятора |
---|---|
6 | правильный |
7 | Ошибка в строке 5: невозможно получить изменяемое заимствование f, поскольку неизменяемое заимствование уже существует. |
8 | правильный |
6, 7 | Ошибка в строке 5: невозможно получить изменяемое заимствование f, поскольку неизменяемое заимствование уже существует. |
6,8 | Ошибка в строке 6: невозможно получить неизменное заимствование f, поскольку изменяемое заимствование уже существует. |
абстрактное выражение для "занять"
Rust
В базовом пакете есть два дженерика.trait
,core::borrow::Borrowиcore::borrow::BorrowMut, который можно использовать для выражения абстрактного значения «заимствования», представляя изменяемое заимствование и неизменяемое заимствование соответственно.
Как упоминалось ранее, «заимствование» имеет множество выражений.(&T,Box<T>,Rc<T> 等等)
, соответствующее выражение заимствования будет выбрано в различных сценариях использования. В их абстрактной форме вы можете использоватьcore::borrow::Borrowпредставлять.Из отношения типа,Borrow
является абстрактной формой понятия «заимствование». В практических приложениях мы надеемся в некоторых случаях получить определенный тип «заимствования» и в то же время надеемся поддерживать все возможные формы «заимствования».Borrow Trait
Это полезно.
Заем определяется следующим образом:
pub trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}
Он имеет только один метод, который требуется для возврата ссылки указанного типа.
Borrow
Примеры приведены в документации
use std::borrow::Borrow;
fn check<T: Borrow<str>>(s: T) {
assert_eq!("Hello", s.borrow());
}
fn main(){
let s: String = "Hello".to_string();
check(s);
lets: &str = "Hello";
check(s);
}
check
Аргумент функции указывает, что она хочет получить любую форму «заимствования» типа «str», а затем извлечь значение и сравнить его с «Hello».
Стандартная библиотекаString
Тип реализованBorrow<str>
, код показывает, как показано ниже
impl Borrow<str> for String{
#[inline]
fn borrow(&self) -> &str{
&self[..]
}
}
такString
тип можно использовать какcheck
параметры функции.
Как видно из рисунка стандартная библиотека есть для всех типовT
ДостигнутоBorrow Trait
, также для&T
ДостигнутоBorrow Trait
.
Код такой, как это понимать.
impl<T: ?Sized> Borrow<T> for T {
fn borrow(&self) -> &T { // 是 fn borrow(self: &Self)的缩写,所以 self 的类型就是 &T
self
}
}
impl<T: ?Sized> Borrow<T> for &T {
fn borrow(&self) -> &T {
&**self
}
}
это точноRust
Что интересно в языке, так это то, что он тонко отражает последовательность языка. теперь, когдаBorrow<T>
метод заключается в полученииT
, то типT
и&T
Конечно, это тоже можно сделать. существуетBorrow for T
в реализации
fn borrow(&self)->&T
даfn borrow(self: &Self)->&T
аббревиатура, такself
Тип&T
, можно вернуть напрямую. существуетBorrow for &T
в реализацииfn borrow(&self)->&T
даfn borrow(self: &Self)->&T
аббревиатура, такself
Тип&&T
, необходимо дважды разыменовать, чтобы получитьT
и вернуть его ссылку.
умный указательBox<T>
,Rc<T>
,Arc<T>
, все понялBorrow<T>
, который получает&T
Например, дважды разыменовать, чтобы получить ссылку.Weak<T>
не реализованыBorrow<T>
, его необходимо обновить доRc<T>
для получения данных.
6. Параметры жизненного цикла
Жизненный цикл переменной в основном связан с областью действия переменной, которая неявно определена в большинстве языков программирования.Rust
Это очень уникальный дизайн, который может явно объявлять параметры жизненного цикла переменных в системе, и его синтаксические характеристики вряд ли можно увидеть в других языках. Ниже приведена иллюстрация, связанная с концепцией жизненного цикла.
Роль параметров жизненного цикла
Основная функция параметра времени жизни — решить проблему висячего указателя. Это позволяет компилятору помочь проверить жизненный цикл переменной, чтобы предотвратить проблему, связанную с тем, что переменная все еще может использоваться после освобождения области памяти, на которую указывает переменная. Итак, при каких обстоятельствах компилятор не сможет оценить жизненный цикл и должен ввести специальный синтаксис для определения жизненного цикла?
Давайте рассмотрим наиболее распространенную проблему с оборванным указателем, когда функция возвращает локальную переменную внутри функции по ссылке:
struct V{v:i32}
fn bad_fn() -> &V{ //编译错误:期望一个命名的生命周期参数
let a = V{v:10};
&a
}
let res = bad_fn();
Этот код представляет собой классическую ошибку висячего указателя,a
является локальной переменной внутри функции, после возврата функцииa
быть уничтоженным,a
Ссылка для присвоенияres
, если выполнение прошло успешно,res
То, что связано, является неопределенным значением.
Но вместо того, чтобы сообщать об ошибке висячего указателя, компилятор говорит, что возвращаемый тип&V
Параметры жизненного цикла не указаны.C++
Компилятор выдаст предупреждение о висячем указателе (содержание предупреждения: возвращен адрес локальной переменной).
Затем мы указываем параметр жизненного цикла, чтобы увидеть:
fn bad_fn<'a>() -> &'a V{
let a = V{v:10};
let ref_a = &a;
ref_a //编译错误:不能返回局部变量的引用
}
На этот раз компилятор сообщил об ошибке висячего указателя. Итак, какова логика анализа компилятора?
Во-первых, давайте проясним, какова точная семантика 'a здесь?
Ссылка, которую вернет функция, будет представлять данные памяти, которые имеют свою область действия.'a
Параметры — это требования, предъявляемые к этой области жизненного цикла. как&V
Это похоже на требования к типу возвращаемого значения,'a - требование для жизненного цикла возвращаемого значения. Что компилятору нужно проверить, так это фактически возвращаемые данные, соответствует ли их жизнь требованиям.
Так что же именно параметр a влияет на жизненный цикл возвращаемого значения?
Давайте сначала разберемся между «контекстом функции» и «контекстом вызывающей стороны».Контекст функции относится к области действия тела функции, а контекст вызывающей стороны относится к месту, где вызывается функция. Вышеупомянутая ошибка висячего указателя на самом деле не влияет на выполнение программы в области контекста функции.Проблема в том, что когда контекст вызывающей стороны получает недопустимую ссылку и использует ее, возникает непредсказуемая ошибка.
Ссылке, возвращаемой функцией, присваивается переменная в «контексте вызывающей стороны», например:
let res = bod_fn();
res
получил возвращенную ссылку внутри функцииref_a
Ссылка побитно копируется в переменнуюres
(в стандартной библиотекеimpl<T: ?Sized> Copy for &T {}
указано это правило)res
будет указывать на функциюres_a
одни и те же данные. Чтобы гарантировать, что в будущем в контексте вызывающего объекта не будет висячих указателей, компилятор действительно должен убедиться, чтоres
Жизненный цикл указанных данных не корочеres
Переменная имеет свое время жизни. В противном случае, если жизненный цикл данных короткий, они будут выпущены первыми,res
становится висячим указателем.
можно положить сюда'a
Под аргументами понимаются переменные в контексте вызывающей стороны, которые получают возвращаемое значение функции.res
жизненный цикл, то'a
Требования для возврата ссылки внутри тела функции:Время жизни данных, на которые ссылается возвращаемая ссылка, не меньше, чем у 'a , то есть не меньше, чем время жизни переменной, получившей возвращаемое значение в контексте вызывающей стороны.
В приведенном выше примере функцияref_a
Упомянутый жизненный цикл данных является областью действия функции.Перед возвратом функции данные уничтожаются, а жизненный цикл меньше, чем у контекста вызывающего объекта.res
, компилятор сравнивает возвращаемое значение с фактическим возвращаемым значением в соответствии с требованиями жизненного цикла возвращаемого значения и находит ошибку.
На самом деле возвращаемая ссылка либо является статическим жизненным циклом, либо получена через преобразование операции по заданному функцией параметру ссылки, иначе это результат, потому что это ссылка на локальные данные.
статический жизненный цикл
см. функцию
fn get_str<'a>() -> &'a str {
let s = "hello";
s
}
Эта функция может быть скомпилирована и передана.Хотя возвращаемая ссылка не выводится из входных параметров, она имеет статический жизненный цикл и может быть проверена.
Поскольку статический жизненный цикл можно понимать как семантику «бесконечности», он фактически согласуется с жизненным циклом процесса, то есть всегда действует во время выполнения программы.
Rust
Строковый литерал хранится в коде программы и всегда действителен в кодовом пространстве после загрузки программы. В этом можно убедиться с помощью простого эксперимента:
let s1="Hello";
println!("&s1:{:p}", &s1);//&s1:0x9cf918
let s2="Hello";
println!("&s2:{:p}",&s2);//&s2:0x9cf978
//s1,s2是一样的值但是地址不一样,是两个不同的引用变量
let ptr1: *const u8 = s1.as_ptr();
println!("ptr1:{:p}", ptr1);//ptr1:0x4ca0a0
let ptr2: *const u8 = s2.as_ptr();
println!("ptr2:{:p}", ptr2);//ptr2:0x4ca0a0
s1
,s2
Необработанные указатели всех указывают на один и тот же адрес, что означает, что компилятор сохраняет копию только для литерала «Hello», и все ссылки указывают на него.
get_str
Статическое время жизни в функции больше, чем требуется для возвращаемого значения.'a
, так что это законно.
если поставитьget_str
изменить на
fn get_str<'a>() -> &'static str
То есть требования к жизненному циклу возвращаемого значения изменены на бесконечность, поэтому они могут возвращать только ссылку на статическую строку.
Жизненный цикл параметров функции
Предыдущий пример не имеет входных параметров для простоты, что не является типичной ситуацией. В большинстве случаев ссылка, возвращаемая функцией, преобразуется операцией, основанной на входном параметре ссылки. Например следующий пример:
fn remove_prefix<'a>(content:&'a str,prefix:&str) -> &'a str{
if content.starts_with(prefix){
let start:usize = prefix.len();
let end:usize = content.len();
let sub = content.get(start..end).unwrap();
sub
}else{
content
}
}
let s = "reload";
let sub = remove_prefix(&s0,"re");
println!("{}",sub); // 输出: load
remove_prefix
функция со входаcontent
Определить, есть ли в строкеprefix
Представляет префикс. вернуть, если естьcontent
Разрезать без префикса, вернуть, если нетcontent
сам.
Эта функция все равно не вернет префиксprefix
,такprefix
Для переменных не нужно указывать время жизни.
Обе ветви функции возвращаются черезcontent
Переменная преобразуется и используется как возвращаемое значение функции. такcontent
Параметры жизненного цикла должны быть отмечены, а компилятор долженcontent
Параметры жизненного цикла сравниваются с требованиями возвращаемого значения, чтобы определить, соответствуют ли они требованиям. который:Жизненный цикл фактически возвращаемых данных больше или равен жизненному циклу, требуемому возвращаемым параметром.
Как было сказано ранее, мы ставим параметры жизненного цикла, указанные в возвращаемых параметрах'a
Думайте об этом как о времени жизни переменной в контексте вызывающей стороны, которая получает возвращаемое значение, в данном случае ссылку на строку.sub
, то что означает «а» во входном параметре?
это вRust
Дизайн синтаксиса очень запутан, жизненный цикл входных параметров и выходных параметров помечен как'a
, который, кажется, требует одинаковых требований к жизненному циклу для обоих, но это не так.
Давайте сначала посмотрим, что произойдет, если жизненный цикл входного параметра отличается от того, что ожидает выходной параметр, например, в следующих двух примерах:
fn echo<'a, 'b>(content: &'b str) -> &'a str {
content //编译错误:引用变量本身的生命周期超过了它的借用目标
}
fn longer<'a, 'b>(s1: &'a str, s2: &'b str) -> &'a str {
if s1.len() > s2.len()
{ s1 }
else
{ s2 }//编译错误:生命周期不匹配
}
echo
Время жизни входного параметра функции аннотируется как'b
, Возвращает ожидаемое значение'a
Сообщение об ошибке компилятора является типичной ошибкой "висячего указателя". Но содержание кажется неясным. Компилятор указывает, чтобы ознакомиться с деталями--explain E0312, интерпретация здесь такова: «срок жизни заимствованного содержимого не такой, как ожидалось». Это описание ошибки соответствует фактическому состоянию ошибки.
longer
Два параметра функции имеют жизненные циклы соответственно.'a
и'b
, ожидается возвращаемое значение'a
, при возвращенииs2
, компилятор сообщает о несоответствии времени жизни. Пучокlonger
Жизненный цикл в функции'b
определяется как отношение'a
long, его можно правильно скомпилировать.
fn longer<'a, 'b: 'a>(s1: &'a str, s2: &'b str) -> &'a str {
if s1.len() > s2.len()
{ s1 }
else
{ s2 }//编译通过
}
Возвращаясь к нашему предыдущему вопросу, что означает «а» во входном параметре?
Мы знаем, что жизненный цикл проверяет, что делает компилятор в контексте определения функции, чтобы удостовериться».Жизненный цикл фактически возвращаемых данных, больший или равный жизненному циклу, требуемому возвращаемым параметром.". Когда входному параметру присваивается тот же параметр времени жизни, что и возвращаемому значению'a
на самом деле является искусственной гарантией для компилятора:В контексте вызывающей стороны время жизни фактического заданного входного параметра функции не меньше времени жизни переменной, используемой для получения возвращаемого значения в будущем.
Когда есть два параметра жизненного цикла'a
'b
, и'b
больше, чем'a
, конечно, также гарантирует, что в вызывающем контексте'b
Время жизни входного параметра делегата также достаточно велико.
В определении функции компилятор не знает, в каком контексте функция будет фактически вызываться в будущем. Параметры времени жизни — это, скорее, соглашение между контекстом функции и контекстом вызывающей стороны относительно времени жизни параметра.
Точно так же, как объявление типа в сигнатуре функции, объявление типа определяет типы входных и выходных параметров с вызывающей стороной.Когда компилятор компилирует функцию, он проверяет, соответствует ли тип данных, возвращаемый телом функции, объявленному результату. ценность. Аналогично, для жизненного цикла параметров и возвращаемых значений функция также проверяет, соответствует ли жизненный цикл переменных, возвращаемых в теле функции, объявлению.
Как упоминалось ранее, компилятор находится в "Проверки жизненного цикла для контекстов определения функций"механизм, это только одна часть проверки жизненного цикла, есть еще одна часть,Контекст вызывающего объекта проверяет жизненный цикл” Правила для обеих проверок следующие:
Проверки жизненного цикла для контекстов определения функции:
Аннотацией жизненного цикла возвращаемого значения в сигнатуре функции может быть любая из входных аннотаций, при условии, что жизненный цикл возвращаемой временной переменной, полученной из входных параметров, гарантированно равен или превышает жизненный цикл аннотация возвращаемого значения в сигнатуре функции. Это гарантирует, что в контексте вызывающего объекта переменная, получающая возвращаемое значение, не станет висячим указателем из-за недопустимых входных параметров.
Контекст вызывающего объекта проверяет жизненный цикл:
В контексте вызывающей стороны принимающая функция возвращает заимствованную переменную.res
, время жизни которого не может быть больше, чем время жизни возвращенного заимствования (фактически выведенное из входных параметров заимствования). в противном случаеres
Станет висячим указателем после того, как входной параметр станет недействительным.
Фронтremove_prefix
Компилятор функции был проверен, затем мы строим следующий пример в контексте вызывающей стороны
let res: &str;
{
let s = String::from("reload");
res = remove_prefix(&s, "re") //编译错误:s 的生命周期不够长
}
println!("{}", res);
В этом примереremove_prefix
При вызове этой строки компилятор сообщит об ошибке «Время жизни s недостаточно велико». Фигурные скобки в коде создают новую лексическую область видимости, в результате чегоres
имеет более длительный срок службы, чем тот, что находится внутри фигурных скобокs
дольше. Это не соответствует требованию времени жизни в сигнатуре функции. Сигнатура функции требует, чтобы время жизни входных параметров было не короче, чем время жизни, требуемое возвращаемым значением.
Жизненный цикл в определении структуры
Когда в структуре есть ссылочные элементы, существует потенциальная проблема с оборванным указателем, и необходимо определить параметр жизненного цикла, чтобы компилятор мог помочь в проверке.
struct G<'a>{ m:&'a str}
fn get_g() -> () {
let g: G;
{
let s0 = "Hi".to_string();
let s1 = s0.as_str(); //编译错误:借用值存活时间不够长
g = G{ m: s1 };
}
println!("{}", g.m);
}
В приведенном выше примере структураG
Содержит ссылочные элементы и не компилируется без указания параметра времени жизни. функцияget_g
Демонстрирует, как могут возникать несоответствия продолжительности жизни в контексте потребителей.
Определение жизненного цикла структуры состоит в том, чтобы гарантировать, что в экземпляре структуры жизненный цикл ее ссылочных элементов не короче, чем жизненный цикл самого экземпляра структуры. В противном случае, если данные ссылочного элемента уничтожаются первыми во время жизни экземпляра структуры, то доступ к ссылочному элементу представляет собой доступ к висячему указателю.
Фактически параметры жизненного цикла конструкции можно сравнить с параметрами жизненного цикла функций, жизненный цикл членов эквивалентен жизненному циклу входных параметров функции, а жизненный цикл всей структуры эквивалентен к жизненному циклу возвращаемого значения функции. Таким образом, можно применить весь предыдущий анализ параметров жизненного цикла функции.
Если в структуре есть элементы метода, которые будут возвращать ссылочные параметры, метод также должен заполнить параметры жизненного цикла. Источником возвращаемой ссылки может быть входной параметр ссылки метода или элемент ссылки структуры. При анализе жизненного цикла как «входные ссылочные параметры методов», так и «ссылочные элементы структур» могут рассматриваться как входные параметры обычных функций, так что предыдущие методы анализа жизненного цикла для обычных параметров функций и возвращаемых значений могут быть Продолжайте подавать заявку.
Общее ограничение срока службы
Как упоминалось ранее, параметры жизненного цикла очень похожи на ограничения типа, например, в коде.
fn longer<'a>(s1:&'a str, s2:&'a str) -> &'a str
struct G<'a>{ m:&'a str }
середина,'a
Рядом с типом отображаемых позиционных параметров один определяет статический тип параметра, а другой определяет динамическое время параметра.'a
Его необходимо объявить перед использованием.Позиция объявления такая же, как и у параметра шаблона.<>
Круглые скобки также используются для указания параметров универсального типа.
Итак, это в порядке, чтобы заменить тип с помощью общего и какой семантика? Что такое сценарий использования?
Давайте посмотрим на пример кода:
use std::cmp::Ordering;
#[derive(Eq, PartialEq, PartialOrd, Ord)]
struct G<'a, T:Ord>{ m: &'a T }
#[derive(Eq, PartialEq, PartialOrd, Ord)]
struct Value{ v: i32 }
fn longer<'a, T:Ord>(s1: &'a T, s2: &'a T) -> &'a T {
if s1 > s2 { s1 } else { s2 }
}
fn main(){
let v0 = Value{ v:12 };
let v1 = Value{ v:15 };
let res_v = longer(&v0, &v1);
println!("{}", res_v.v);//15
let g0 = G{ m: &v0 };
let g1 = G{ m: &v1 };
let res_g = longer(&g0, &g1);//15
println!("{}", res_g.m.v);
}
Этот пример расширяетlonger
функция, которая может быть реализована для любогоOrd trait
вид операции.Ord
встроенный в основной пакет для реализации операций сравненияtrait
, Здесь не подробно.longer
Функция сравнивается с предыдущей версией, толькоstr
Типы заменены общими параметрамиT
, и датьT
Добавлена квалификация типаT:Ord
.
структураG
Также расширен для размещения дженериковT
, но требуетT
ДостигнутоOrd trait
.
Из кода и результатов выполнения следуйтеT
В обычном типе нет ничего особенного, а параметры времени жизни по-прежнему имеют свою изначальную семантику.
Но на самом деле "&'a T
" также подразумевает еще один слой семантики:еслиT
Если внутри есть ссылочные элементы, жизненный цикл ссылочных элементов не должен быть короче, чемT
Жизненный цикл экземпляра.
Как обычно, построим контрпример. структураG
содержит общий ссылочный элемент внутри, мы будемG
используется дляlonger
Функция, но пустьG
Срок службы внутреннего эталонного элемента меньше, чемG
. код показывает, как показано ниже:
fn main(){
let v0 = Value{ v:12 };
let v1_ref: &Value; // 将 v1 的引用定义在下面大括号之外,有意延长变量的生命周期范围
let res_g: &G<Value>;
{
let v1 = Value{ v:15 };
v1_ref = &v1; //编译错误:v1的生命周期不够长。
let res_v = longer(&v0,v1_ref);
println!("{}",res_v.v);
}
let g0 = G{ m:&v0 };
let g1 = G{ m:v1_ref }; // 这时候 v1_ref 已经是悬垂指针
res_g = longer(&g0, &g1);
println!("{}", res_g.m.v);
}
Переменнаяg1
Собственный жизненный цикл удовлетворенlonger
Функция требует, но ее внутренний элемент ссылки имеет короткий жизненный цикл.
Эта парадигма срабатывает, когда проверяется «контекст вызывающего объекта», и трудно разработать парадигму, срабатывающую в «контексте определения функции или структуры» для ограничения времени жизни общих параметров. после всегоT
Это просто обозначение типа, оно определяется без конкретного типа.
На самом деле вставить "struct G<'a,T>{m:&'a T}
середина,T
Время жизни всех упомянутых членов не меньше, чем'a
"Это семантически точное выражение следует записать так:
struct G<'a,T:'a>{m:&'a T}
так какT:'a
Это явное выражение этой семантики. Но достаточно и первого выражения (я доказал его от противного). Таким образом, компилятор также принимает первое более простое выражение.
В целом, ограничение жизненного цикла универсальных параметров имеет два значения: одно — это значение универсального типа как нормального типа, а другое — ограничение жизненного цикла для внутренних ссылочных членов универсального.
Жизненный цикл трейт-объекта
См. следующий код
trait Foo{}
struct Bar{v:i32}
struct Qux<'a>{m:&'a i32}
struct Baz<'a,T>{v:&'a T}
impl Foo for Bar{}
impl<'a> Foo for Qux<'a>{}
impl<'a,T> Foo for Baz<'a,T>{}
структураBar
,Qux
,Baz
все сбудетсяtrait Foo
, Так&Foo
Тип может принимать ссылочные типы из любой из этих трех структур.
мы кладем&Foo
называетсяTrait
объект.
Trait
Объекты можно понимать как указатели или ссылки на интерфейсы или базовые классы, аналогичные другим объектно-ориентированным языкам. разноеOO
Указатель языка на базовый класс определяет его фактический тип во время выполнения.Rust
Нет наследования классов, указывая наtrait
Указатель или ссылка имеют аналогичный эффект, а конкретный тип определяется во время выполнения. Таким образом, размер не известен во время компиляции.
Rust
изTrait
не может иметь нестатических элементов данных, поэтомуTrait
Не похоже, что жизненный цикл ссылочного члена меньше, чем сам объект, поэтомуTrait
Время жизни объекта по умолчанию — статическое время жизни. Рассмотрим следующие три функции:
fn check0() -> &'static Foo { // 如果不指定 'static , 编译器会报错,要求指定生命周期命参数, 并建议 'static
const b:Bar = Bar{v:0};
&b
}
fn check1<'a>() -> &'a Foo { //如果不指定 'a , 编译器会报错
const b:Bar = Bar{v:0};
&b
}
fn check2(foo:&Foo) -> &Foo {//生命周期参数被省略,不要求静态生命周期
foo
}
fn check3(foo:&'static Foo) -> &'static Foo {
foo
}
fn main(){
let bar= Bar{v:0};
check2(&bar); //能编译通过,说明 chenk2 的输入输出参数都不是静态生命周期
//check3(&bar); //编译错误:bar的生命周期不够长
const bar_c:Bar =Bar{v:0};
check3(&bar_c); // check3 只能接收静态参数
}
check0
иcheck1
Описание будетTrait
Когда ссылка на объект возвращается как параметр функции, необходимо указать параметр времени жизни, как и при возврате других типов ссылок. функцияcheck2
Параметр времени жизни просто опущен (что компилятор может сделать вывод), но в этой функцииTrait
Объекты не имеют статического времени жизни, которое можно изменить сmain
Функция может быть успешно выполненаcheck2(bar)
проанализировано, потому чтоbar
Не статичное время жизни.
На самом деле во время выполненияTrait
Объекты всегда динамически связаны с реализацией, реализующейTrait
Конкретный тип структуры (например,Bar
,Qux
,Baz
д.), данный конкретный тип имеет свой жизненный цикл в своем контексте, который может быть статичным, чаще нестатическим жизненным циклом'a
,ТакTrait
Жизненный цикл объекта также'a
.
жизненный цикл структуры или члена | Жизненный цикл трейт-объекта | |
---|---|---|
Foo | никто | 'static |
Bar | 'a | 'a |
Qux<'a>{m:&'a str} | 'a | 'a |
Baz<'a,T>{v:&'a T} | 'a | 'a |
fn qux_update<'a>(qux: &'a mut Qux<'a>, new_value: &'a i32)->&'a Foo {
qux.v = new_value;
qux
}
let value = 100;
let mut qux = Qux{v: &value};
let new_value = 101;
let muted: &dyn Foo = qux_update(& mut qux, &new_value);
qux_update 函数的智能指针版本如下:
fn qux_box<'a>(new_value: &'a i32) -> Box<Foo +'a> {
Box::new(Qux{v:new_value})
}
let new_value = 101;
let boxed_qux:Box<dyn Foo> = qux_box(&new_value);
В возвращенном интеллектуальном указателеBox
Коробочный тип содержит ссылочные элементы, а также необходимо указать жизненный цикл для упакованных данных.Синтаксическая форма состоит в том, чтобы добавить параметр жизненного цикла в позицию коробочного типа и соединить его знаком «+».
Эти две версии кода на самом деле показывают проблему, т.е.Trait
Хотя по умолчанию используется статический жизненный цикл, на самом деле его жизненный цикл определяется конкретной реализацией этогоTrait
Определяется жизненный цикл структуры, и метод вывода мало чем отличается от жизненного цикла параметров функции, описанных ранее.
7. Право собственности и время жизни интеллектуальных указателей
Как показано на рисунке 6, вRust
И ссылки, и интеллектуальные указатели являются формой «указателя», поэтому их можно реализоватьstd::borrow::Borrow Trait
. При нормальных обстоятельствах мы получаем ссылки на переменные в стеке, а длительность переменных в стеке, как правило, относительно коротка.Когда текущая область действия выходит, переменные стека в области действия будут переработаны. Если мы хотим, чтобы время жизни переменной охватывало текущую область или даже передавалось между потоками, лучше создать связанную область данных переменной в куче.
Объем переменных в стеке ясен во время компиляции, поэтому компилятор может определить, когда переменные в стеке будут освобождены.В сочетании с параметрами жизненного цикла компилятор может найти большинство неправильных ссылок на переменные в стеке.
Управление памятью переменных в куче намного сложнее, чем управление переменными стека. После того, как часть памяти выделена в куче, компилятор не может определить время выживания этой части памяти на основе области действия и должен быть явно указан пользователем.C
На языке это для каждого кускаmalloc
Выделенная память должна использоваться явноfree
освободить.C++
средний даnew / delete
. но когда звонитьfree
илиdelete
это проблема. Особенно, когда код сложный, а код выделения памяти и код освобождения памяти находятся не в одном файле кода или даже в одном потоке, неизбежно совершать ошибки, вручную отслеживая логическую связь кода с поддерживать распределение и выпуск.
Основная идея интеллектуальных указателей состоит в том, чтобы система автоматически помогала нам решать, когда освобождать память. Основными его средствами являютсяВыделить память в куче, но переменная-указатель на эту память сама находится в стеке, поэтому компилятор может поймать, когда переменная-указатель выходит за пределы области видимости. В это время принимается решение о выделении памяти. Если переменная-указатель владеет областью памяти, память будет освобождена. Если это указатель счетчика ссылок, значение счетчика будет уменьшено. Если счетчик равен 0, память будет восстановлена.".
Rust
изBox<T>
является исключительным указателем владения,Rc<T>
указатели подсчитываются по ссылкам, но их процесс подсчета не является потокобезопасным,Arc<T>
Обеспечивает потокобезопасные действия подсчета ссылок, которые можно использовать в потоках.
мы видимBox<T>
Определение
pub struct Box<T: ?Sized>(Unique<T>);
pub struct Unique<T: ?Sized>{
pointer: *const T,
_marker: PhantomData<T>,
}
Box
сама по себе является кортежной структурой, обертывающейUnique<T>
,Unique<T>
Внутри есть родной указатель.
(Примечание: последняя версия реализации Rust Box также может указывать распределитель памяти с помощью общих параметров, что позволяет пользователям самостоятельно контролировать фактическое выделение памяти. , здесь нет подробностей.)
Box
не реализованыCopy Trait
, который выполняет семантику перемещения при передаче права собственности.
Образец кода:
Struct Foo {v:i32}
fn inc(v:& mut Foo) -> &Foo {//省略了生命周期参数
v.v = v.v + 1;
v
}
//返回Box指针不需要生命周期参数,因为Box指针拥有了所有权,不会成为悬垂指针
fn inc_ptr(mut foo_ptr:Box<Foo>) -> Box<Foo> {//输入参数和返回参数各经历一次所有权转移
foo_ptr.v = foo_ptr.v + 1;
println!("ininc_ptr:{:p}-{:p}", &foo_ptr, &*foo_ptr);
foo_ptr
}
fn main() {
let foo_ptr1 = Box::new(Foo{v:10});
println!("foo_ptr1:{:p}-{:p}", &foo_ptr1, &*foo_ptr1);
let mut foo_ptr2 = inc_ptr(foo_ptr1);
//println!("{}",foo_ptr1.v);//编译错误,f0_ptr所有权已经丢失
println!("foo_ptr2:{:p}-{:p}", &foo_ptr2, &*foo_ptr2);
inc(foo_ptr2.borrow_mut());//获得指针内数据的引用,调用引用版本的inc函数
println!("{}",foo_ptr2.v);
}
inc
для ссылочной версии,inc_ptr
это версия указателя. Вывод измененного кода:
foo_ptr1:0x8dfad0-0x93a5e0
in inc_ptr:0x8df960-0x93a5e0
foo_ptr2:0x8dfb60-0x93a5e0
12
можно увидетьfoo_ptr1
введите функциюinc_ptr
, выполняется передача права собственности, а затем другая, когда функция возвращается. так триBox<Foo>
Адреса переменных разные, но их внутренние адреса данных одинаковы, указывая на одну и ту же область памяти.
Box
Сам тип не имеет ссылочных членов, но еслиT
содержит ссылочные элементы, так каковы связанные с этим проблемы жизненного цикла?
мы кладемFoo
Попробуйте изменить элемент элемента на ссылочный элемент, код выглядит следующим образом:
use std::borrow::BorrowMut;
struct Foo<'a>{v:&'a mut i32}
fn inc<'a>(foo:&'a mut Foo<'a>) ->&'a Foo<'a> {//生命周期不能省略
*foo.v=*foo.v + 1; // 解引用后执行加法操作
foo
}
fn inc_ptr(mut foo_ptr:Box<Foo>) -> Box<Foo> {//输入参数和返回参数各经历一次所有权转移
*foo_ptr.v = *foo_ptr.v + 1; / 解引用后执行加法操作
println!("ininc_ptr:{:p}-{:p}", &foo_ptr, &*foo_ptr);
foo_ptr
}
fn main(){
let mut value = 10;
let foo_ptr1 = Box::new(Foo{v:& mut value});
println!("foo_ptr1:{:p}-{:p}", &foo_ptr1, &*foo_ptr1);
let mut foo_ptr2 = inc_ptr(foo_ptr1);
//println!("{}",foo_ptr1.v);//编译错误,f0_ptr所有权已经丢失
println!("foo_ptr2:{:p}-{:p}", &foo_ptr2, &*foo_ptr2);
let foo_ref = inc(foo_ptr2.borrow_mut());//获得指针内数据的引用,调用引用版本的inc函数
//println!("{}",foo_ptr2.v);//编译错误,无法获取foo_ptr2.v的不可变借用,因为已经存在可变借用
println!("{}", foo_ref.v);
}
ссылочная версияinc
Жизненный цикл функции больше нельзя опускать. потому что возвращениеFoo
, есть два значения времени жизни, одноFoo
жизненный цикл экземпляра, одинFoo
Жизненный цикл члена, на который ссылается компилятор, не может быть выведен и должен быть указан. Но версия умного указателяinc_ptr
Жизненный цикл функции по-прежнему указывать не нужно.Foo
Экземпляр обернут интеллектуальным указателем, а время жизни задаетсяBox
Ответственный за управление.
еслиFoo
ЯвляетсяTrait
, а реализующая его структура имеет ссылочные элементы, тоBox<Foo>
Что происходит с жизненным циклом . Пример кода выглядит следующим образом:
trait Foo{
fn inc(&mut self);
fn value(&self)->i32;
}
struct Bar<'a>{v:&'a mut i32}
impl<'a> Foo for Bar<'a> {
fn inc(&mut self){
*(self.v)=*(self.v)+1
}
fn value(&self)->i32{
*self.v
}
}
fn inc(foo:& mut dyn Foo)->& dyn Foo {//生命周期参数被省略
foo.inc();
foo
}
fn inc_ptr(mut foo_ptr:Box<dyn Foo>) -> Box< dyn Foo> {//输入参数和返回参数各经历一次所有权转移
foo_ptr.inc();
foo_ptr
}
fn main() {
}
Эталонная версия и версия интеллектуального указателя не имеют параметров жизненного цикла и могут быть скомпилированы. ноmain
Функция пустая, то есть эти функции не используются, но определение и компиляция передаются. Сначала я попробую использовать указанную версию:
fn main(){
let mut value = 10;
let mut foo1= Bar{v:& mut value};
let foo2 =inc(&mut foo1);
println!("{}", foo2.value()); // 输出 11
}
Он может быть скомпилирован и выводится нормально. Попробуйте снова разумную версию указателя:
fn main(){
let mut value = 10;
let foo_ptr1 = Box::new(Bar{v:&mut value}); //编译错误:value生命周期太短
let mut foo_ptr2 = inc_ptr(foo_ptr1); //编译器提示:类型转换需要value为静态生命周期
}
Компиляция не удалась. Сообщение об ошибкеvalue
Жизненный цикл слишком короткий и должен быть'static
. так какTrait
объект (Box< dyn Foo>
) по умолчанию имеет статическое время жизни, и компилятор делает вывод, что время жизни возвращаемых данных слишком мало. удалить последнюю строкуinc_ptr
Его можно нормально скомпилировать.
еслиinc_ptr
Приведенный выше код можно скомпилировать и передать, добавив определение параметров жизненного цикла. после редактированияinc_ptr
следующее:
fn inc_ptr<'a>(mut foo_ptr:Box<dyn Foo+'a>) -> Box<dyn Foo+'a> {
foo_ptr.inc();
foo_ptr
}
Почему в версии указателя без параметра времени жизни возникает ошибка, а в эталонной версии без параметра времени жизни все в порядке?
Поскольку в эталонной версии отсутствует параметр жизненного цикла, полная запись выглядит так:
fn inc<'a>(foo:&'a mut dyn Foo)->&'a dyn Foo {
foo.inc();
foo
}
8. Закрытие и право собственности
Использование замыканий здесь не представлено, только содержимое, связанное с владением. По сравнению с обычными функциями замыкания могут захватывать переменные в тексте в дополнение к входным параметрам. Замыкания также поддерживаютmove
ключевое слово, чтобы принудительно передать право собственности на захваченную переменную.
давайте сначала посмотримmove
Влияет ли это на входные параметры:
//结构 Value 没有实现Copy Trait
struct Value{x:i32}
//没有作为引用传递参数,所有权被转移
let mut v = Value{x:0};
let fun = |p:Value| println!("in closure:{}", p.x);
fun(v);
//println!("callafterclosure:{}",point.x);//编译错误:所有权已经丢失
//作为闭包的可变借用入参,闭包定义没有move,所有权没有转移
let mut v = Value{x:0};
let fun = |p:&mut Value| println!("in closure:{}", p.x);
fun(& mut v);
println!("call after closure:{}", v.x);
//可变借用作为闭包的输入参数,闭包定义增加move,所有权没有转移
let mut v = Value{x:0};
let fun = move |p:& mut Value| println!("in closure:{}", p.x);
fun(& mut v);
println!("call after closure:{}", v.x);
Видно, что когда в качестве входного параметра в замыкание передается переменная, правила передачи владения такие же, как и для обычных функций.Ключевое слово move не влияет на форму ссылки входных параметров замыкания, и право собственности на входные параметры не передается.
Для переменных контекста, захваченных замыканиями, передача права собственности немного сложнее.
В таблице ниже приведено более 10 примеров, каждый из которых немного отличается от примера до и после него, анализируя эти отличия, можно сделать более четкие выводы.
Важно знать, какая переменная захватывается первой. Например, в примере 8ref_v
даv
Неизменяемое заимствование , замыкание фиксируетref_v
, то переход права собственности аналогиченv
это не имеет значения,v
Никакие события передачи права собственности, связанные с закрытием, не произойдут.
После указания захваченных переменных на передачу права собственности влияет сочетание трех факторов:
- Как захватывается переменная (значение, неизменное заимствование, изменяемое заимствование)
- Есть ли у закрытия ограничение на перемещение?
- Реализует ли тип захваченной переменной черту «Копировать»
Правила передачи права собственности описаны в псевдокоде следующим образом:
if 捕获方式 == 值传递 {
if 被捕获变量的类型实现了 "Copy"
不转移所有权 // 例 :9
else
转移所有权 // 例 :1
}
}
else { // 捕获方式是借用
if 闭包没有 move 限定
不转移所有权 // 例:2,3,6,10,12
else { // 有 move
if 被捕获变量的类型实现了 "Copy"
不转移所有权 // 例: 8
else
转移所有权 // 例: 4,5,7,11,13,14
}
}
Сначала определите метод захвата. Если он передается по значению, он эквивалентен области действия переменной, которая инициирует синхронизацию передачи права собственности.move
Он работает с заимствованием, требуя, чтобы заимствование также приводило к передаче права собственности. Реализовать ли «Копировать» — это последний шаг. Как упоминалось ранее, мы можемCopy Trait
Ограниченная семантика побитового копирования используется как средство выполнения ветвления.Copy Trait
Не участвует в оценке времени перехода и вступает в силу только при выполнении последнего перехода.
- Разница между Примером 1 и (Примером 2, Примером 3) заключается в способе захвата.
- Разница между (Пример 2, Пример 3) и Примером 4 заключается в ключевом слове перемещения.
- Разница между Примером 6 и Примером 7 демонстрирует влияние ключевого слова move на заимствованный захват.
- Пример 8 иллюстрирует захват неизменяемой заимствованной переменной, которая в любом случае не перемещается, поскольку неизменяемое заимствование реализует копирование.
- Разница между Примером 8 и Примером 11 заключается в том, что «неизменяемое заимствование», зафиксированное в Примере 11, не реализует черту «Копировать».
- Пример 10 и пример 11 фиксируют «изменяемую переменную заимствования» «неизменяемым заимствованным способом».
- Примеры 12, 13 и 14 демонстрируют влияние на интеллектуальные указатели, и логика суждения также непротиворечива.
C++11
замыкания должны явно указывать в объявлении замыкания, следует ли захватывать по значению или по ссылке,Rust
Разные.Rust
То, как замыкание захватывает переменные контекста, зависит не от объявления замыкания, а от того, как захваченные переменные используются внутри замыкания. На самом деле компилятор попытается захватить переменные, заимствуя как можно больше (например, если это невозможно, как в примере 1).
Намеренно не упоминается механизм реализации закрытия, а именноFn
,FnMut
,FnOnce
триTrait
. Потому что, когда мы используем только синтаксис замыкания, мы не можем увидеть конкретную реализацию замыкания компилятором. Таким образом, мы судим о правилах передачи права собственности только по самому синтаксису замыкания.
9. Проблемы владения в многопоточной среде
Снова изменим предыдущий пример 1, ни контекст, ни реализация замыкания не изменились, но замыкание выполняется в другом потоке.
let v = Value{x:1};
let child = thread::spawn(||{ // 编译器报错,要求添加 move 关键字
let p = v;
println!("inclosure:{}",p.x)
});
child.join();
В это время компилятор сообщил об ошибке и попросил добавить в замыканиеmove
ключевые слова. То есть, когда замыкание используется в качестве функции входа в поток, обязательно выполнять семантику перемещения для захваченной переменной контекста. Давайте посмотрим на систему владения в многопоточной среде.
Ни одно из предыдущих обсуждений не касалось совместного использования переменных между потоками.Когда несколько потоков могут получить доступ к одной и той же переменной, ситуация становится немного сложнее. Здесь есть две проблемы, одна все же проблема безопасности памяти, то есть пять типичных проблем безопасности памяти, таких как «висячие указатели», а другая — проблема непредсказуемых результатов выполнения, вызванных порядком выполнения потоков. Здесь мы сосредоточимся только на вопросах безопасности памяти.
Во-первых, как несколько потоков совместно используют переменные? В предыдущем примере показано, как несколько потоков могут совместно использовать переменные, захватывая переменные в контексте с помощью замыканий при запуске нового потока. Это типичная форма, и мы используем ее в качестве основы для решения проблемы владения в многопоточной среде.
Давайте посмотрим на пример кода:
//结构 Value 没有实现Copy Trait
struct Value{x:i32}
let v = Value{x:1};
let child = thread::spawn(move||{
let p = v;
println!("in closure:{}",p.x)
});
child.join();
//println!("{}",v.x);//编译错误:所有权已经丢失
Это правильная реализация предыдущего примера, переменнаяv
передается другому потоку (внутри замыкания), который выполняет передачу права собственности
//闭包捕获的是一个引用变量,无论如何也拿不到所有权。那么多线程环境下所有引用都可以这么传递吗?
let v = Value{x:0};
let ref_v = &v;
let fun = move ||{
let p = ref_v;
println!("inclosure:{}",p.x)
};
fun();
println!("callafterclosure:{}",v.x);//编译执行成功
В этом примере замыкание фиксирует ссылку на переменную,Rust
ссылки реализованыCopy Trait
, будет побитово скопировано в переменную внутри замыканияp.p
просто неизменное заимствование, собственность не приобретается, а переменнаяv
Неизменяемое заимствование передается внутри и снаружи замыкания. Так как насчет того, чтобы изменить его на многопоточность? Это реализация многопоточности и сообщение об ошибке, выдаваемое компилятором:
let v:Value = Value{x:1};
let ref_v = &v; // 编译错误:被借用的值 v0 生命周期不够长
let child = thread::spawn(move||{
let p = ref_v;
println!("in closure:{}",p.x)
}); // 编译器提示:参数要求 v0 被借用时为 'static 生命周期
child.join();
Основное значение компилятораv
жизненный цикл недостаточно длинный. когдаv
Неизменяемый заимствование передается в замыкание и используется в другом потоке, основной поток продолжает выполнение,v
В любой момент она может быть переработана за пределы области видимости, тогда ссылочная переменная в дочернем потоке становится висячим указателем. еслиv
Для статического жизненного цикла этот код может быть скомпилирован и выполнен в обычном режиме. То есть изменить первую строку на:
const v:Value = Value{x:1};
Конечно, практическое использование передачи только статических ссылок времени жизни ограничено, в большинстве случаев мы все еще надеемся передать нестатические данные в другой поток. может быть использованArc<T>
чтобы обернуть данные.Arc<T>
Это интеллектуальный указатель со счетчиком ссылок.Операция увеличения и уменьшения счетчика указателя является потокобезопасной атомарной операцией, а изменение счетчика гарантированно будет потокобезопасным.
//线程安全的引用计数智能指针Arc可以在线程间传递
let v1 = Arc::new(Value{x:1});
let arc_v = v1.clone();
let child = thread::spawn(move||{
let p = arc_v;
println!("Arc<Value>in closure:{}",p.x)
});
child.join();
//println!("Arc<Value>inclosure:{}",arc_v.x);//编译错误,指针变量的所有权丢失
Если вышеArc<T>
заменитьRc<T>
, компилятор сообщит об ошибке "Rc<T>
не может безопасно передаваться между потоками».
Из приведенного выше примера мы можем сделать вывод, что, поскольку определение замыкания вmove
ключевое слово, когда новый поток запускается с замыканием, необходимо передать право собственности на саму переменную, захваченную замыканием. Является ли захваченная переменная «переменной-значением», ссылочной переменной или интеллектуальным указателем (в приведенном выше примереv
,ref_v
,arc_v
право собственности на себя передается). Но со ссылками или указателями право собственности на данные, на которые они ссылаются, не обязательно передается.
Тогда для вышеуказанного типаstruct Value{x:i32}
, Его значение может быть передано между несколькими потоками(переход права собственности), егоНесколько неизменяемых заимствований могут существовать одновременно в нескольких потоках.. в то же время&Value
иArc<Value>
может передаваться между потоками (передача владения ссылочной переменной или самой переменной-указателем), ноRc<T>
нет.
Знать,Rc<T>
иArc<T>
ТолькоRust
Стандартная библиотека (std
), даже в основной библиотеке (core
)внутри. То есть они неRust
частью языкового механизма. Итак, как компилятор решает, что Arc может безопасно передаваться между потоками, а Rc — нет?
Rust
основная библиотекаmarker.rs
В файле определены два тегаTrait
:
pub unsafe auto trait Sync{}
pub unsafe auto trait Send{}
ЭтикеткаTrait
пуст, но компилятор анализирует, реализует ли тип этот тегTrait
.
- если тип
T
Достигнуто"Sync, что значитT
Безопасно делиться по ссылке между несколькими потоками. - если тип
T
Достигнуто"Send, что значитT
Безопасно передавать через границы потока.
Затем введите в приведенном выше примере,Value
,&Value
,Arc<Value>
Все типы должны реализовываться"Send
"Trait
, Давайте посмотрим, как это достигается.
marker.rs
Файл также определяет два правила:
unsafe impl<T:Sync + ?Sized> Send for &T{}
unsafe impl<T:Send + ?Sized> Send for & mut T{}
Их значения:
- Если тип T реализует "Sync", это автоматически тип
&T
выполнить"Send". - Если тип T реализует "Send", это автоматически тип
&mut T
выполнить"Send".
Оба эти правила можно понять интуитивно. Например: для первого правилаT
Достигнуто»Sync”, что означает, что один и тот же поток может появиться во многих потокахT
пример&T
Тип экземпляр. если нитьA
во-первых&T
экземпляр, нитьB
как получить&T
пример? должен быть в сетиA
каким-то образомsend
Да ладно, как переменные контекста захвата замыканий. и&T
Достигнуто»Copy
" Trait
, Нет риска владения, данные доступны только для чтения, нет риска гонки данных, и это очень безопасно. Тоже логически правильно. Так почему бы не пометить его как небезопасный?Давайте пока отложим это в сторону и рассмотрим несколько других правил проектирования интеллектуальных указателей.
impl <T:?Sized>!marker::Send for Rc<T>{}
impl <T:?Sized>!marker::Sync for Rc<T>{}
impl<T:?Sized>!marker::Send for Weak<T>{}
impl<T:?Sized>!marker::Sync for Weak<T>{}
unsafe impl<T:?Sized+Sync+Send>Send for Arc<T>{}
unsafe impl<T:?Sized+Sync+Send>Sync for Arc<T>{}
Эти правила четко определяютRc<T>
иWeak<T>
не может быть достигнуто»Sync"и "Send".
Также укажите, является ли типT
Достигнуто»Sync"и "Send", то автоматическиArc<T>
выполнить "Sync"и "Send".Arc<T>
Увеличение или уменьшение счетчика ссылок является атомарным, поэтому его клоны могут использоваться в нескольких потоках (то есть дляArc<T>
выполнить"Sync"и"Send»), но почему его предварительное условие является требованиемT
также достичь"Sync"и "Send«Шерстяное полотно.
мы знаем,Arc<T>
Достигнутоstd::borrow
, в состоянии пройтиArc<T>
Получать&T
экземпляры в нескольких потокахArc<T>
Экземпляры, конечно, также могут быть получены из нескольких потоков.&T
экземпляр, который требуетT
должен достичь"Sync".Arc<T>
является интеллектуальным указателем с подсчетом ссылок,Arc<T>
Все клоны могут стать последним клоном.Чтобы нести ответственность за освобождение памяти, вы должны получитьArc<T>
указатель завернутыйT
владение экземпляром, что требуетT
Должен быть проходим через потоки, должен реализовывать "Send".
Rust
Компилятор неRc<T>
илиArc<T>
делать специальную обработку, даже на уровне языка, сам компилятор не знает об их существовании, просто исходя из того, реализует ли тип "Sync"и "Send" для вывода. Фактически можно считать, что компилятор реализует обработчик правил, проверяющий безопасность передачи переменных между потоками, и компилятор непосредственно реализует его для базовых типов. "Sync"и "Send", которая существует как "аксиома", а затем добавить в код стандартной библиотеки некоторые "теоремы", то есть правила, перечисленные выше. Тип, реализуемый пользователем, может указывать, следует ли реализовывать его самому"Sync"и "Send", в большинстве случаев компилятор выберет, следует ли реализовать его по умолчанию в зависимости от ситуации. Компилятор может делать выводы на основе этих аксиом и правил при компиляции кода. ЭтоRust
Компилятор поддерживает безопасные секреты владения несколькими потоками.
Для механизма правил "аксиомы" и "теоремы" самоочевидны и не нуждаются в доказательстве. Их декларирует сам конструктор, и сам конструктор гарантирует их безопасность. Компилятор лишь гарантирует, что до тех пор, пока теоремы и аксиомы верны, в его рассуждениях нет ошибки. Итак, «аксиомы» и «теоремы» отмечены какunsafe
, В напоминании говорится о необходимости проверить его безопасность, и пользователи также могут определить свои собственные «теоремы» для обеспечения собственной безопасности. Вместо этого отмените правило класса (реализация!Send
или!Sync
) не отмечены какunsafe
, так как они напрямую отвергают передачу переменных между потоками, проблем с безопасностью нет.
Когда компилятор определяет "Sync"и "Send" автоматически реализуется для типа, когда он подходит.
Например, компилятор по умолчанию использует следующие типы реализаций.Sync
:
- Основные типы, такие как [u8] и [f64], являются [Sync],
- Простые агрегатные типы (такие как кортежи, структуры и имена), которые их содержат, также являются [Sync] .
- «неизменяемые» типы (например, &T)
- Типы с простой изменчивостью наследования, такие как Box , Vec
- Большинство других типов коллекций (если общий параметр — [Sync], его контейнер — [Sync].
Пользователи также могут вручную использоватьunsafe
способ указать напрямую.
На следующей диаграмме показаны концепции и типы, связанные с владением несколькими потоками.UML
рисунок.
Введение редактора:
Гао Сяньфэн (.nil?), инженер-разработчик программного обеспечения, энтузиаст языка Rust, любит спланированную, организованную и эффективную работу, любит культуру открытого исходного кода и готов внести свой вклад в развитие китайского сообщества Rust.