Съдържание на курса
Урок 2 — Ownership
От Dorian Chávez · основател на Hábil и архитект на интеграции ·
Време: 2 × 45 мин.
Какво изграждаш: програмите, които нарочно се сблъскват с компилатора
Какво научаваш: трите правила на собствеността, преместване срещу копиране, заемания & и &mut, двете правила на заеманията, защо няма събирач на боклук
След края ще можеш да
- Обясниш трите правила на собствеността (ownership) и да ги използваш, за да предвиждаш кога Rust освобождава една стойност.
- Разграничиш неявното копиране от преместването и да обосновеш кога да използваш
clone(). - Избереш дали една функция трябва да получи стойност, референция
&Tили променлива референция&mut T. - Приложиш правилото „много четения или един запис“ и да поправиш грешките
E0382иE0502. - Обясниш защо Rust не се нуждае нито от събирач на боклук, нито от ръчни извиквания на
free. - Напишеш функция, която получава
&strи връща заета част от текста. - Решиш упражненията
move_semanticsиprimitive_typesна Rustlings.
Защо, преди как
Урок 2 е точката, в която Rust престава да изглежда просто като компилиран език с различен от Go синтаксис. Променливите, функциите, типовете и контролът на потока от предишния урок са разпознаваеми. Собствеността променя един въпрос, който много езици крият: когато една програма създаде данна в паметта, кой трябва да я унищожи и кога?
revisor ще съхранява имена на услуги, URL адреси, съобщения за повреди, списъци и доклади. Всички тези данни могат да растат по време на изпълнение. Името на една услуга, прочетено от YAML, няма известен размер, когато компилираш; има нужда от динамична памет. Крайната таблица на доклада също се строи постепенно. В C или C++ този, който пише програмата, би трябвало да резервира и освобождава тази памет ръчно. Ако освободи два пъти, програмата може да се повреди. Ако не я освободи, губи памет. Ако запази указател, след като я е освободил, може да чете област, която вече принадлежи на нещо друго.
Go взима друго решение. Програмата може да създава данни и да забрави да освободи памет, защото събирачът на боклук (garbage collector) наблюдава кои обекти все още са достижими и възстановява останалите. Това прави писането на програми по-пряко, но добавя компонент по време на изпълнение, който управлява паметта, решава кога да работи и консумира ресурси, за да намира обекти, които вече не вършат работа. В повечето програми на Go това решение е отлично: намалява сложността и избягва тежки грешки.
Rust търси друга комбинация: безопасна памет без събирач на боклук и без ръчно освобождаване. Предложението му е да се провери, преди да се генерира изпълнимият файл, кой притежава всяка стойност и кой може да я достъпва. Ако компилаторът може да докаже, че една стойност вече няма да се използва, вмъква подходящото освобождаване при излизане от областта ѝ на видимост. Ако не може да докаже, че една референция ще остане валидна, отхвърля програмата. Ако открие два несъвместими достъпа до една и съща данна, също я отхвърля.
Това не означава, че Rust „налучква“ какво си искал да направиш. Напротив: изисква намерението ти да е видимо в сигнатурите и в присвояванията. Функция, която получава String, взема стойността; такава, която получава &str, само я консултира; такава, която получава &mut String, може да я променя по време на изключително заемане. Информацията, която в Go понякога остава в конвенция, в коментар или в преглед на код, в Rust е част от типа.
Цената е реална. В началото ще пишеш код, който изглежда разумен, но не се компилира. Нормалната реакция е да се опиташ да добавяш .clone(), докато грешката изчезне. Понякога едно копие е правилното решение; много пъти е знак, че функцията е поискала повече собственост, отколкото ѝ е трябвала. Научаването на собствеността се състои в това да престанеш да третираш тези грешки като препятствия и да започнеш да ги четеш като въпроси на дизайна: кой трябва да запази тази стойност? колко време трябва да живее? кой може да я променя?
Глава 4 на The Rust Programming Language обяснява собствеността, референциите, заеманията и slice-овете. Прочети я цялата по време на този урок. Не се опитвай да запомниш всички съобщения на компилатора. Целта е да изградиш прост мисловен модел: всяка стойност има собственик; преместването предава тази отговорност; заемането позволява използване на една стойност, без да се предава отговорността; а правилата за заеманията не позволяват четенето да види една данна, докато някой я променя.
Истинският проект вече използва тази идея, макар още да не си написал всичките му части. Servicio притежава своите полета String, защото revisor трябва да запази име и URL отвъд функцията, която ги е прочела. Напротив, функциите, които отпечатват доклад, получават референции към услугите и към състоянията им: те трябва само да ги консултират, не да ги присвояват. По-нататък, в уроци 3, 4 и 5, същите тези решения ще се появят в struct-ове, колекции, грешки и lifetime-ове.
Понятията
Собственост, област на видимост и детерминирано освобождаване
Собствеността се обобщава в три правила.
- Всяка стойност в Rust има собственик.
- Може да има само един собственик на стойност в даден момент.
- Когато собственикът излезе от областта си на видимост, Rust освобождава стойността.
Областта на видимост (scope) е частта от програмата, където едно име съществува. Фигурните скоби ограничават областите, също като в урок 1. Разликата сега е, че излизането от една област не само прави променливата недостъпна: определя и кога се унищожава свързаната стойност. За типове, които резервират ресурси, Rust извиква drop автоматично. String, например, освобождава блока памет, където съхранява символите си.
Фиг. 2.1 | Областта на видимост на една променлива.
// fig02_01.rs
fn main() {
{
let s = String::from("hola"); // s es el dueño
println!("{s}");
} // aquí termina el ámbito: se libera. Sin free(), sin GC
}
$ rustc --edition 2024 fig02_01.rs && ./fig02_01
hola
String::from("hola") създава String, който притежава динамична памет. Докато s е във вътрешната област, може да се използва за отпечатване на текста. Когато се стигне до затварящата скоба, s престава да съществува и Rust освобождава паметта му. Не си писал free, не си изчислявал размери и не си чакал събирач да реши да мине. Компилаторът вмъква необходимата работа, защото знае обхвата на s.
Думата „собственост“ не описва физическото местоположение на една данна; описва отговорност. Стойността може да е в стека, в хийпа (heap) или да съдържа референции към други стойности. Важното е, че Rust може да идентифицира собственик, отговорен за почистването на ресурса. Много прости типове, като u64, bool или char, се побират изцяло в стека и не изискват да се освобождава нищо специално. Един String, един Vec<T> или един HashMap<K, V> управляват динамична памет и наистина се нуждаят от подреден край.
Това освобождаване се нарича детерминирано, защото става в точка, за която можеш да разсъждаваш, докато четеш програмата: в края на областта на видимост, освен ако стойността не е била преместена преди това. Важно е да го различаваш от ръчното управление. Не избираш кога да извикаш drop за всяка стойност, нито трябва да го правиш при нормални условия. Rust познава типа и генерира правилното освобождаване. Ако един тип съдържа други стойности, неговият деструктор освобождава и това, което съответства вътре в него.
Тази гаранция не означава, че Rust забранява всяко мислимо изтичане на памет. Например е възможно да се запазят данни с цикли от броени референции или да се използват нарочно механизми, които избягват освобождаването. Централната гаранция е друга: безопасният код не може да използва по-късно стойност, която Rust вече е освободил, нито да освободи два пъти една и съща памет. За програма като revisor това елиминира цял клас грешки, без да добавя събирач на боклук по време на изпълнение.
В Go една локална променлива също престава да бъде полезна, когато излезе от блока си, но паметта, която е останала недостъпна, се възстановява по-късно, когато събирачът го определи. В Rust краят на областта на видимост е пряка част от модела на ресурсите. Тази разлика не прави автоматично по-добър единия или другия език. Go опростява много приложения; Rust позволява да се знае по-точно кога се освобождават памет, файлове, сокети или ключалки (locks).
revisor притежава текста, който трябва да запази. Една услуга не може да зависи от това дали една временна променлива на YAML парсера е все още жива: затова полетата ѝ са String, а не референции към временен текст.
pub struct Servicio {
pub nombre: String,
pub url: String,
#[serde(default = "timeout_por_omision")] // si falta en el YAML
pub timeout_ms: u64,
}
nombre и url са собственост на всяка Servicio. Когато векторът със услуги бъде унищожен, елементите му ще бъдат унищожени; когато всеки елемент бъде унищожен, неговите String ще бъдат унищожени; и всеки String ще освободи паметта си. Няма ръчен списък с ресурси за почистване. Структурата от стойности описва и структурата на отговорността.
Преместване, копиране и клониране
Второто правило казва, че една стойност има само един собственик в даден момент. Затова едно присвояване не винаги означава копиране. При типове, които притежават ресурси, Rust обикновено премества стойността (move): новата променлива става собственик, а предишното име вече не може да се използва.
Това изненадва, ако идваш от Go. В Go присвояването на string на друга променлива копира неговия непроменлив заглавен блок и двете променливи могат да се четат. Присвояването на struct копира полетата му; ако съдържа slice или map, и двете копия могат да продължат да сочат към споделени данни. В Rust компилаторът изисква тази връзка да е изрична, защото едно плитко копие на тип-собственик може да остави две стойности, които се опитват да освободят един и същ ресурс.
Фиг. 2.2 | Двата изхода: копиране или заемане.
// fig02_02.rs
fn main() {
let a = String::from("hola");
let b = a.clone(); // copia explícita: pagas la copia y lo dices
let c = &a; // PRESTAR en vez de mover ← esto es lo normal
println!("{a} {b} {c}");
}
$ rustc --edition 2024 fig02_02.rs && ./fig02_02
hola hola hola
a.clone() създава втори String със собствена памет и собствени символи. Затова a и b могат да живеят независимо. Копирането има цена, пропорционална на размера на текста: да копираш „hola“ е малко; да копираш голям HTTP отговор или списък с хиляди услуги може да не е. Rust прави тази цена видима с метода clone().
Не бива да тълкуваш това като забрана да клонираш. Едно копие е правилно, когато програмата наистина се нуждае от две независими стойности: да запази едно име за доклада и друго, за да го изпрати на задача, да запази оригиналната конфигурация, преди да я трансформира, или да раздели данни, които ще живеят различно време. Проблемът се появява, когато clone() се използва механично, за да заглуши грешка, без да се отговори кой трябва да притежава данната.
Третото име, c, е референция. &a не копира символите, нито предава собствеността. Създава заемане само за четене. Затова могат да се отпечатат a, b и c: a продължава да е собственикът; b е собственик на друго копие; c само временно сочи към a.
Типовете, които имплементират trait-а Copy, се държат различно. Целите числа, булевите стойности, символите и кортежите, съставени изключително от стойности Copy, се копират неявно, защото дублирането им е евтино и не изискват освобождаване на памет. Ако присвоиш let b = a, когато a е u64, можеш да използваш и двете имена. Не е така, че собствеността изчезва: всяка променлива получава собствено копие на стойността.
String не имплементира Copy, защото неявното копиране на трите му вътрешни данни — указател, дължина и капацитет — би произвело двама управители за един и същ блок на хийпа. Rust би могъл да копира и символите, но тогава всяко присвояване потенциално би скривало скъпа работа. Затова различава преместването от клонирането.
revisor клонира само когато трябва да построи изход, който трябва да притежава собствен текст. JSON докладът не може да пази референции към локални услуги във функция, която вече е приключила. Затова превръща заетото име на услугата в независим String.
let lineas: Vec<EstadoJson> = ordenadas(servicios, estados)
.into_iter()
.map(|(s, e)| EstadoJson {
servicio: s.nombre.clone(),
codigo: match e {
Estado::Ok { codigo, .. } | Estado::Lento { codigo, .. } => Some(*codigo),
_ => None,
},
ms: match e {
Estado::Ok { ms, .. } | Estado::Lento { ms, .. } | Estado::Falla { ms, .. } => *ms,
Estado::NoIntentado => 0,
},
error: match e {
Estado::Falla { motivo, .. } => Some(motivo.clone()),
Estado::NoIntentado => Some("sin revisar".to_string()),
_ => None,
},
})
.collect();
Тук servicios и estados се заемат на функцията за доклад. s.nombre.clone() и motivo.clone() са необходими решения: EstadoJson трябва да оцелее като елемент на lineas и после да се превърне в JSON. Кодът не клонира от страх от компилатора; клонира, защото резултатът има собствен собственик.
Непроменливи референции: заемане за четене
Референцията е начин да се позволи достъп до една стойност, без да се предава собствеността ѝ. Пише се &T: „референция към T“. Ако имаш String и една функция трябва само да узнае дължината му, подаването на &String избягва създаването на копие и не позволява на функцията да консумира текста.
Фиг. 2.3 | Заемане за четене.
// fig02_03.rs
fn largo(s: &String) -> usize { s.len() } // presta, no toma posesión
fn main() {
let s = String::from("hola");
let n = largo(&s);
println!("{s} mide {n}"); // sigue siendo mía ✓
}
$ rustc --edition 2024 fig02_03.rs && ./fig02_03
hola mide 4
Функцията largo получава референция. Вътре в нея s.len() консултира дължината, но не може да задържи String, нито да го промени. Когато извикването приключи, заемането приключва и първоначалният собственик остава променливата s на main. Това обяснява защо последният ред може да отпечата както текста, така и дължината му.
Примерът запазва &String, защото показва директно контраста между String собственик и референция към него. В общ API е уместно да се получава &str, когато трябва само да четеш текст. &str е изглед към UTF-8 последователност; приема както литерал, така и референция към String. Урок 4 ще задълбочи това разграничение, но още оттук можеш да използваш практическо правило: съхранявай текст, който притежаваш, като String; получавай текст само за четене като &str.
Една референция не е копие на стойността. Има полезен живот, ограничен от стойността, към която сочи. Rust не позволява да се върне референция към локална променлива, която ще изчезне при излизане от функцията, нито да се запази референция, когато собственикът ѝ вече е преместен. Тази част от анализа е позната като проверка на заеманията или borrow checking.
Референцията прави видим договора на една функция. Сигнатура, която получава String, съобщава „трябва да взема този текст“. Такава, която получава &str, съобщава „трябва само да го чета“. В Go подаването на string е евтино, защото представянето му се копира; подаването на голяма структура по стойност или по указател изисква да се прочете документацията и да се познава имплементацията. Rust прави тази разлика част от сигнатурата.
Функциите на доклада получават заети slice-ове. Не консумират вектора със услуги, нито вектора със състояния, защото main все още има нужда от тях, за да реши изходния код. Сигнатурата изразява това намерение без допълнителни коментари.
pub fn tabla(servicios: &[Servicio], estados: &[Estado]) -> String {
let filas = ordenadas(servicios, estados);
let ancho = filas
.iter()
.map(|(s, _)| s.nombre.chars().count())
.max()
.unwrap_or(0)
.max("SERVICIO".len());
&[Servicio] означава „зает slice от услуги“. Един slice заема съседна част от колекция и знае колко елемента съдържа. Функцията може да я обхожда, да консултира имена и да изчислява ширината на таблицата, но не може да изпразни вектора, да добави услуги, нито да ги задържи. Същото решение важи и за &[Estado].
Променливи референции: заемане за промяна
Една променлива референция се пише &mut T. Служи, когато една функция трябва да промени стойност, чиято собственост остава на този, който извиква. За да я създадеш, са ти нужни две неща: собственикът трябва да е деклариран с mut, а заемането трябва да е написано като &mut.
Фиг. 2.4 | Изключително заемане за промяна.
// fig02_04.rs
fn agregar_puerto(etiqueta: &mut String) {
etiqueta.push_str(":443");
}
fn main() {
let mut servicio = String::from("catalogo");
agregar_puerto(&mut servicio);
println!("{servicio}");
}
$ rustc --edition 2024 fig02_04.rs && ./fig02_04
catalogo:443
servicio е с mut, защото съдържанието му ще се промени. agregar_puerto не получава String по стойност: получава изключително заемане и добавя символи към същия текст. Когато извикването приключи, заемането приключва и main отново използва собственика, за да го отпечата.
Изключителността е важното условие. По време на заемане &mut никой друг не може да чете или променя същата данна чрез друга референция. Това не е произволно ограничение: ако една част от програмата променя низ, докато друга допуска, че го чете стабилно, резултатът може да зависи от реда на изпълнение. В конкурентните програми тази ситуация е състезание за данни (data race).
Двете правила за заемания са:
- Можеш да имаш произволен брой непроменливи референции към една стойност.
- Можеш да имаш точно една променлива референция към една стойност или непроменливи референции, но не и двете едновременно.
Краткият начин да ги запомниш е: много четения или един запис. Едно четене не променя данната и може да се споделя. Един запис се нуждае от изключителност, защото може да промени коя да е част от стойността.
Rust анализира и последното истинско използване на една референция. Не е нужно непременно да чакаш до крайната скоба на блока, за да поискаш променливо заемане. Ако една непроменлива референция вече няма да се използва повече, Rust може да счете, че заемането ѝ е приключило. Това е познато като нелексикални заемания (non-lexical borrows). Не бива да разчиташ на него, за да пишеш объркващ код, но обяснява защо разделянето на четене и промяна на ясни стъпки обикновено се компилира.
В revisor таблицата се строи с променлива, декларирана с mut. Собствеността на salida остава в tabla, но push_str се нуждае от временно променливо заемане, за да добави всеки ред. При излизане от функцията tabla връща пълния String и собствеността преминава към този, който е извикал.
let mut salida = format!(
"{:<ancho$} {:<6} {:>8} DETALLE\n",
"SERVICIO", "ESTADO", "TIEMPO"
);
for (s, e) in filas {
salida.push_str(&format!(
"{:<ancho$} {:<6} {:>8} {}\n",
s.nombre,
etiqueta(e),
tiempo(e),
detalle(e)
));
}
salida
push_str променя salida; затова променливата е декларирана с mut. Напротив, s и e са референции за четене към данните на всеки ред. Компилаторът позволява и двете, защото функцията променя новия доклад, а не услугите и състоянията, които консултира.
Slice-ове, &str и референции, които връщат референции
Собствеността не принуждава да копираш, когато искаш да получиш част от една стойност. Една функция може да получи референция и да върне друга референция към част от същата информация, стига Rust да може да провери, че изходът няма да живее по-дълго от входа. Този шаблон се появява при slice-ове на масиви, slice-ове на вектори и &str.
Фиг. 2.5 | Връщане на зает изглед към текст.
// fig02_05.rs
fn primera_palabra(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
fn main() {
let texto = String::from("revisor listo");
let palabra = primera_palabra(&texto);
println!("primera: {palabra}");
}
$ rustc --edition 2024 fig02_05.rs && ./fig02_05
primera: revisor
primera_palabra не строи нов String. Връща изглед към част от s. Затова е ефективна: не копира символите. Има и здравословно ограничение: palabra не може да надживее texto, защото сочи вътре в него. Ако texto се промени по начин, който променя съхранението му, една стара референция би могла да престане да е валидна; Rust не позволява да използваш двете неща по несъвместим начин.
Сигнатурата fn primera_palabra(s: &str) -> &str използва правило за извеждане на lifetime-ове. Компилаторът разбира, че изходната референция е свързана с входната. В урок 5 ще видиш случаите, в които трябва да напишеш анотация като 'a; засега запомни важната идея: функцията не притежава върнатата дума, така че не може да обещае, че тя ще съществува по-дълго от заетия текст.
revisor използва изрични lifetime-ове, когато сглобява редове, които само заемат данни от два slice-а. Не дублира всяка услуга и всяко състояние, преди да ги подреди; запазва валидни референции, докато оригиналните вектори са все още живи.
pub type Fila<'a> = (&'a Servicio, &'a Estado);
fn ordenadas<'a>(servicios: &'a [Servicio], estados: &'a [Estado]) -> Vec<Fila<'a>> {
let mut filas: Vec<Fila<'a>> = servicios.iter().zip(estados).collect();
filas.sort_by(|a, b| a.0.nombre.cmp(&b.0.nombre));
filas
}
Анотацията 'a казва, че референциите във Fila не могат да живеят по-дълго от slice-овете, заети на ordenadas. filas е собственик на вектора с референции, но не и на услугите, нито на състоянията. Това разграничение е основата на много ефективни програми на Rust: да притежаваш колекцията не означава да притежаваш всички данни, към които тя сочи.
Грешката, която ще видиш
E0382: използване на стойност, след като е преместена
Следващата програма нарочно не се компилира. Присвояването let b = a премества String от a в b. Последният ред се опитва да заеме a, за да го отпечата, но a вече не е собственик и не може да се заема.
Фиг. 2.6 | Преместване, не копиране.
// fig02_06.rs
fn main() {
let a = String::from("hola");
let b = a; // NO copia: MUEVE. Ahora b es el dueño
println!("{a}"); // ← error: valor movido
}
$ rustc --edition 2024 fig02_06.rs
error[E0382]: borrow of moved value: `a`
--> fig02_06.rs:5:16
|
3 | let a = String::from("hola");
| - move occurs because `a` has type `String`, which does not implement the `Copy` trait
4 | let b = a; // NO copia: MUEVE. Ahora b es el dueño
| - value moved here
5 | println!("{a}"); // ← error: valor movido
| ^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
4 | let b = a.clone(); // NO copia: MUEVE. Ahora b es el dueño
| ++++++++
warning: unused variable: `b`
--> fig02_06.rs:4:9
|
4 | let b = a; // NO copia: MUEVE. Ahora b es el dueño
| ^ help: if this is intentional, prefix it with an underscore: `_b`
|
= note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default
error: aborting due to 1 previous error; 1 warning emitted
For more information about this error, try `rustc --explain E0382`.
E0382 означава, че си се опитал да използваш стойност, след като си прехвърлил собствеността ѝ. Съобщението посочва три места: къде е родено a, къде е станало преместването и къде си се опитал да я използваш отново. Тази последователност е по-полезна от запомнянето на кода на грешката: следвай стрелките и питай кой е собственикът след всеки ред.
Има три възможни поправки и те не са взаимозаменяеми. Ако вече не ти трябва a, отпечатай b. Ако ти трябват две независими стойности, използвай a.clone() и приеми цената на копирането. Ако втората част трябва само да чете стойността, промени дизайна, за да заемеш &a, вместо да я преместваш. Третата възможност обикновено е най-добрата, когато пишеш помощни функции за revisor.
Предупреждението за b се появява, защото програмата не стига до използването му. То не е основната грешка; то е следствие от това, че примерът използва a нарочно, за да предизвика E0382. Програмите, които трябва да се компилират в курса, се проверяват с -D warnings, така че и една неизползвана променлива се превръща в проблем, който трябва да поправиш.
E0502: искане на запис, докато съществуват четения
Следващата грешка представлява второто правило за заемания. r1 и r2 са живи непроменливи референции, защото се използват в последния println!. Докато тези четения съществуват, Rust не може да създаде r3, променлива референция към същия String.
Фиг. 2.7 | Четения и запис едновременно.
// fig02_07.rs
fn main() {
let mut s = String::from("hola");
let r1 = &s; // lectura, ok
let r2 = &s; // otra lectura, ok
let r3 = &mut s; // ← error: ya hay lecturas vivas
println!("{r1} {r2} {r3}");
}
$ rustc --edition 2024 fig02_07.rs
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> fig02_07.rs:6:14
|
4 | let r1 = &s; // lectura, ok
| -- immutable borrow occurs here
5 | let r2 = &s; // otra lectura, ok
6 | let r3 = &mut s; // ← error: ya hay lecturas vivas
| ^^^^^^ mutable borrow occurs here
7 | println!("{r1} {r2} {r3}");
| -- immutable borrow later used here
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0502`.
E0502 означава, че си поискал променливо заемане, докато съществува активно непроменливо заемане. Rust не допуска, че „сигурно нищо не се случва“. Едно променливо заемане би могло да замени, скъси, изпразни или премести съдържанието на s, така че референциите за четене вече да нямат съгласуван изглед.
Поправката не е да копираш s по рефлекс. Първо реши дали наистина трябва да четеш и променяш едновременно. Ако не, завърши четенията, преди да поискаш записа: отпечатай или изчисли с r1 и r2, спри да ги използваш и после създай променливата референция. Ако трябва да запазиш информация за четене, докато променяш, запази малко копие на нужната данна, като дължина или флаг, а не непременно пълно копие на структурата.
Тази грешка е локална версия на една гаранция, която ще бъде решаваща в урок 7. В Go две goroutine, които четат и пишат споделени данни без координация, могат да имат състезание за данни, което се проявява само при изпълнение. Rust установява тези правила, преди да говори за нишки; когато стигнеш до Arc, Mutex и канали, същият модел ще продължи да защитава споделените достъпи.
Какво се прави погрешно
Използването на clone(), за да се заглушава всяка грешка
Компилаторът предлага clone() в няколко съобщения, защото е механично и безопасно решение: създава независима стойност. Предложението обаче не познава дизайна на програмата ти, нито размера на данните ти. Ако клонираш малък String веднъж, вероятно няма значение. Ако клонираш списък със услуги във всяка функция или дублираш големи HTTP тела в цикъл, добавяш време и памет, без да е нужно.
Преди да напишеш .clone(), попитай дали функцията трябва само да чете. Ако отговорът е да, получавай &T или &str. Ако трябва да промени нещо, но собственикът трябва да го запази, получавай &mut T. Клонирай, когато ти трябват двама истински собственици, като JSON докладът и оригиналните данни, които остават живи за друга операция.
Получаването на String по стойност, когато само ще четеш
Сигнатура, която получава String, кара извикващия да предаде собствеността. Това може да е правилно за функция, която нормализира, консумира или съхранява текста. Ненужно е за функция, която само печата, мери или търси дума. Цената не винаги е копие: понякога извикващият може да премести стойността. Проблемът е, че сигнатурата намалява възможностите за употреба на този, който извиква.
За функции за справка предпочитай &str, ако работиш с текст, и &T, ако работиш с друг тип. API-то ще е по-гъвкаво: ще приема литерали, String и части от текст, без да принуждава да се създават нови собственици. Урок 4 ще покаже защо &str обикновено е по-добра публична граница от &String.
Мисленето, че mut означава „мога да заемам променливо, когато поискам“
let mut s позволява да се променя s, но не премахва правилата за заемания. Променливостта принадлежи на собственика; изключителността принадлежи на всяко заемане. Можеш да декларираш променлив низ и пак да получиш E0502, ако съществуват активни референции за четене. Можеш също да имаш непроменлива променлива, която съдържа променлива референция, създадена в друг контекст; двете понятия са различни.
Използвай mut само когато името трябва да се променя или когато ще поискаш променливо заемане. Ако една променлива никога не се променя, премахването на mut оставя по-ясно намерение и избягва предупреждения на компилатора.
Борбата срещу заемането, вместо да се намали обхватът му
Една референция живее до последното си използване, не непременно до визуалния край на блока. Ако компилаторът не приема променливо заемане, провери къде за последен път се използва предишната референция. Много поправки се състоят в пренареждане на няколко реда: довърши четенето, запази резултата, който ти трябва, и после промени. Разделянето на фазите на четене и запис подобрява както четимостта, така и съвместимостта с borrow checker.
Не крий проблема зад дълга референция, unsafe или глобална структура. revisor все още е малък; ако моделът на собственост става труден за обяснение, това обикновено е знак, че една функция има твърде много отговорности или че една данна се споделя повече от необходимото.
Бъркането на String с &str
String притежава текст и може да расте; &str е изглед към текст, който принадлежи на нещо друго. Преобразуването на &str в String с to_string() или String::from() е правилно, когато ще го съхраняваш. Да го правиш само защото една функция би могла да получи референция, е избегнимо копие. От другата страна, връщането на &str, когато текстът е построен вътре във функцията, не може да работи: локалният текст изчезва, когато функцията свърши.
Полезният въпрос винаги е един и същ: кой трябва да притежава тези символи след тази операция? Ако отговорът е „структурата, която ги съхранява“, използвай String. Ако отговорът е „никой нов; трябва само да ги наблюдавам сега“, използвай &str.
Упражнения
Упражнение 1 — Проследи собственика
Прочети следните ситуации и запиши, преди да компилираш, кое име може да се използва накрая: присвояване на u64; присвояване на String; и присвояване на String, последвано от clone(). После създай три малки файла и провери отговорите си с rustc --edition 2024.
Обясни с едно изречение защо цялото число се копира, защо String се премества и защо clone() произвежда двама собственици. Не използвай Copy като магическа дума: свържи го с цената и с необходимостта да се освобождава памет.
Упражнение 2 — Една функция, която взема, и друга, която заема
Напиши две функции върху име на услуга. Първата трябва да получи String по стойност и да върне дължината му. Втората трябва да получи &str и да върне същата дължина. В main покажи, че след извикването на първата функция вече не можеш да отпечаташ String, а след извикването на втората — можеш.
Първо остави активния ред, който предизвиква E0382, и прочети цялата диагностика. После коментирай този ред, за да се компилира програмата. Не поправяй първата функция с clone(): целта е да наблюдаваш разликата между вземане на собственост и заемане.
Упражнение 3 — Обнови една услуга, без да сменяш собственика
Напиши fn agregar_puerto(etiqueta: &mut String), за да прикачиш :443 към един етикет. Декларирай променлив String в main, заеми го на функцията и провери, че main може да отпечата резултата накрая.
После предизвикай E0502: създай референция само за четене към същия текст, използвай я, след като поискаш променлива референция, и наблюдавай реда, посочен от компилатора. Пренареди програмата така, че четенето да приключи, преди да се промени.
Упражнение 4 — Заетата първа дума
Имплементирай fn primera_palabra(s: &str) -> &str. Тя трябва да върне първата дума на една фраза или празен низ, ако получи само интервали. Изпробвай я с String на име texto, отпечатай резултата и после отпечатай и texto.
Направи упражненията move_semantics и primitive_types на Rustlings. По-специално, не напредвай чрез проба и грешка с clone(): във всяко решение установи дали Rust ти иска да преместиш, да копираш или да заемеш.
Решения
Решение 1
Един u64 имплементира Copy, така че след let b = a съществуват две независими стойности и двете имена са използваеми. Един String не имплементира Copy; същото присвояване премества собствеността на b, затова a престава да е използваемо. Ако напишеш let b = a.clone(), a и b притежават два различни блока текст и двете могат да се използват.
Проверката не се състои в запомняне кои типове имплементират Copy, а в това да направиш предположение и да го провериш. Когато имаш съмнение за собствен тип, компилаторът ще ти каже дали имплементира Copy. В struct-овете на revisor, които съдържат String, приеми първоначално, че стойността се премества.
Решение 2
Функцията, която получава String, консумира аргумента. Сигнатурата ѝ трябва да изглежда като fn largo_tomando(s: String) -> usize; след извикването оригиналният String вече не е наличен. Функцията, която получава &str, трябва да изглежда като fn largo_prestando(s: &str) -> usize; извикай я с &nombre и после отпечатай nombre.
Разликата не е в числото, което връщат, а в договора на входа. За функция, която само изчислява дължината, втората сигнатура е подходящата. Първата съществува, за да наблюдаваш изрично преместването и за реалните случаи, в които една функция наистина трябва да задържи стойността.
Решение 3
Решението е това от фигура 2.4: собственикът се декларира като let mut servicio, функцията се извиква с &mut servicio и се отпечатва, след като извикването приключи. Функцията не връща String, защото никога не го е получавала като собственост.
За да поправиш конфликта на заеманията, използвай изцяло референцията за четене, преди да създадеш променливата референция. Важното не е да слагаш двете референции в изкуствени блокове, а да направиш видимо, че фазата на четене е приключила преди фазата на запис.
Решение 4
Решението е това от фигура 2.5. split_whitespace() игнорира началните интервали и разделя думите; next() произвежда Option<&str>; unwrap_or("") връща празен низ, ако не е имало нито една дума. Резултатът е референция, взета от входа, а не нов String.
Правилната проверка отпечатва първо думата и после оригиналния String. Това показва, че primera_palabra не е взела собствеността на texto. Ако се опиташ да върнеш референция към String, създаден вътре във функцията, Rust би го отхвърлил, защото този String би бил унищожен при завършването на извикването.
Как да разбера, че съм успял
-
rustc --edition 2024 fig02_01.rs && ./fig02_01отпечатваhola. -
rustc --edition 2024 fig02_02.rs && ./fig02_02отпечатва три пътиholaи мога да обясня коя стойност е клонирана и коя е заета. -
rustc --edition 2024 fig02_06.rsсе проваля сE0382и знам да обясня на кой ред е преместена собствеността. -
rustc --edition 2024 fig02_07.rsсе проваля сE0502и знам да го поправя, като първо приключа четенията. -
rustc --edition 2024 fig02_04.rs && ./fig02_04отпечатваcatalogo:443. -
rustc --edition 2024 fig02_05.rs && ./fig02_05отпечатваprimera: revisor. - Завърших
move_semanticsиprimitive_typesна Rustlings, без да използвамclone()като автоматично решение. - Мога да обясня с едно изречение защо Rust освобождава памет при излизане от областта на видимост, без да изисква събирач на боклук.
За по-нататъшно четене
- The Rust Programming Language, глава 4: Understanding Ownership, консултирано на 2 октомври 2026 г.
- The Rust Programming Language, референции и заемания, консултирано на 2 октомври 2026 г.
- Официална документация на
String, консултирано на 2 октомври 2026 г. - Rustlings, упражнения
move_semanticsиprimitive_types, консултирано на 2 октомври 2026 г.
Предпочитате имейл? Пишете ни на hola@habil.mx